Kriget mot 404: Att få gamla saker att fungera igen
Inleiding
När du ' har bloggat sedan ♫ 2004 ♫ ♫ ( ♫ ja ♫, ♫ så samlar du upp en massa digitala skräp ♫ allt var sönder. yttre länkar som pekar på platser som försvann för ett decennium sedan , gamla URL-schemas som inte längre matchar den nuvarande strukturen
NOTET: Det här kommer snart , tillsammans med den semantiska sökserien och funktionaliteten på bloggarna . Det var bara en stor förändring och jag hade några mindre artiklar att få ut först
Problemet bryts ner i tre delar
Interna länkar - Fixerat under importprocessen själv med hjälp av min ArchivOrg-importeringsverktyg. Mina gamla meddelanden referenserade varandra med hjälp av den gamla URL-schematen, så jag skrev de om som en del av migrationen.
Externa länkar (utgående) - Det här är den stora kopplingen till externa resurser som sedan dess har försvunnit , flyttats, , eller blivit helt andra platser.
Inkomande begäran - Människor | ( | och sökmotorer |) | försöker fortfarande att nå gamla URLs som
/archive/2006/05/15/123.aspx. Det semantiska söksystemet kan ofta lista ut vad de var efter
Nu, Jag kunde have baked the archive.org lookups into the import process . But here's the thingM SK3 links break over timeMska4 A site thatMske5s working today might be gone next monthMka6 By handling this at runtime with periodic reMsek7checkingMk8 the system automatically catches framtid bråker, inte bara de som fanns vid införingens tid
Den här artikeln omfattar min metod
- Utgående länkar:
BrokenLinkArchiveMiddleware- ersätter döda yttre länkar med archiv - Inkommande länkar: Handlaren 404 med semantiskt sök | - | hittar rätt innehåll även från gamla URL-schemas
- Lärningssystemet: Hur webbplats blir smartare över tid från användarklicker
- Background-processing: Att kolla länkar utan att blockera begäran
Problemet med gamla innehåll
Här' är grejen med internet : det ' är inte permanentt M SK3 Den fantastiska bloggposten du länkade till i 2006? Försvann Mska5 Den dokumentära platsen Mske6 Restrukturerad tre gångerMska7 ditt eget URL-schema från innan du beslutade dig för ett korrekt slugging-konvention
graph TD
A[User requests old post] --> B{Link valid?}
B -->|Yes| C[Happy user]
B -->|No| D[404 Error]
D --> E[Frustrated user]
E --> F[User leaves]
style D stroke:#ef4444,stroke-width:3px
style F stroke:#ef4444,stroke-width:3px
style C stroke:#10b981,stroke-width:3px
Den naiva tillvägagångssätten är att fixa länkar manuellt. Men när du har hundratals meddelanden med tusentals länkar,
Arkitektur överblick
Systemet har två huvudkomponenter som arbetar i tandem
flowchart TB
subgraph Incoming["Incoming Requests"]
A[User Request] --> B{Page exists?}
B -->|Yes| C[Render Page]
B -->|No| D[404 Handler]
D --> E{Learned redirect?}
E -->|Yes| F[301 Permanent Redirect]
E -->|No| G{High-confidence match?}
G -->|Yes| H[302 Temporary Redirect]
G -->|No| I[Show suggestions]
I --> J[User clicks suggestion]
J --> K[Learn redirect]
end
subgraph Outgoing["Outgoing Links"]
C --> L[BrokenLinkArchiveMiddleware]
L --> M[Extract all links]
M --> N[Register for checking]
L --> O[Replace broken links]
O --> P[Archive.org URLs]
O --> Q[Semantic search results]
O --> R[Remove dead links]
end
style F stroke:#10b981,stroke-width:3px
style H stroke:#f59e0b,stroke-width:3px
style P stroke:#3b82f6,stroke-width:3px
style Q stroke:#8b5cf6,stroke-width:3px
Del 1: Att hantera utgående länkar
BrokenLinkArchiveMiddleware
Denna mellanvara intercepterar HTML-responser och gör tre saker
- Extraherar alla länkar för bakgrundskontroll
- Ersätter kända brutna externa länkar med archive.org versioner
- Använde semantiska sökningar för att hitta ersättningar för brutna inre länkar
Den viktigaste insikten här är att vi vill hitta archive.org snapshots från ronda tiden när postet skrevs. Ett ögonblicksbild från | 2024 | en |2006 | artikel kan länka till en helt annan innehållsmängd.
Här är kärnstrukturen, '.
public partial class BrokenLinkArchiveMiddleware(
RequestDelegate next,
ILogger<BrokenLinkArchiveMiddleware> logger,
IServiceScopeFactory serviceScopeFactory)
{
public async Task InvokeAsync(
HttpContext context,
IBrokenLinkService? brokenLinkService,
ISemanticSearchService? semanticSearchService)
{
// Only process HTML responses for blog pages
if (!ShouldProcessRequest(context))
{
await next(context);
return;
}
// Capture the response so we can modify it
var originalBodyStream = context.Response.Body;
using var responseBody = new MemoryStream();
context.Response.Body = responseBody;
await next(context);
// Process the HTML response
if (IsSuccessfulHtmlResponse(context, responseBody))
{
var html = await ReadResponseAsync(responseBody);
html = await ProcessLinksAsync(html, context, brokenLinkService, semanticSearchService);
await WriteModifiedResponseAsync(originalBodyStream, html, context);
}
else
{
await CopyOriginalResponseAsync(responseBody, originalBodyStream);
}
}
}
Att extrahera länkar
Vi använder en genererad regex för att dra ut alla href attributes. Den [GeneratedRegex] attributet i .NET ger oss att kompilera-tidsreflexgenerering , vilket är både snabbare och fördelningsrikt
[GeneratedRegex(@"<a[^>]*\shref\s*=\s*[""']([^""']+)[""'][^>]*>",
RegexOptions.IgnoreCase | RegexOptions.Compiled)]
private static partial Regex HrefRegex();
private List<string> ExtractAllLinks(string html, HttpRequest request)
{
var links = new List<string>();
var matches = HrefRegex().Matches(html);
foreach (Match match in matches)
{
var href = match.Groups[1].Value;
// Skip special links (anchors, mailto, etc.)
if (SkipPatterns.Any(p => href.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
continue;
if (Uri.TryCreate(href, UriKind.Absolute, out var uri))
{
if (uri.Scheme == "http" || uri.Scheme == "https")
links.Add(href);
}
else if (href.StartsWith("/"))
{
// Convert relative URLs to absolute for tracking
var baseUri = new UriBuilder(request.Scheme, request.Host.Host,
request.Host.Port ?? (request.Scheme == "https" ? 443 : 80));
links.Add(new Uri(baseUri.Uri, href).ToString());
}
}
return links.Distinct().ToList();
}
Nu skulle du kunna använda HtmlAgilityPack / https~://github.comM SK3AngleSharp /Anglesharp eller liknande HTMLMST5~ och andraMSP6 parser här för mer komplexa scenarior men det var överkill för detta syfte.
Registering Links for Background Checking
Vi vill inte blockera svaret medan vi kontrollerar länkar. Istället avfyrar vi en bakgrundsuppgift.
var allLinks = ExtractAllLinks(html, context.Request);
var sourcePageUrl = context.Request.Path.Value;
if (allLinks.Count > 0)
{
// Fire and forget - don't block the response
_ = Task.Run(async () =>
{
using var scope = serviceScopeFactory.CreateScope();
var scopedService = scope.ServiceProvider.GetRequiredService<IBrokenLinkService>();
await scopedService.RegisterUrlsAsync(allLinks, sourcePageUrl);
});
}
Kom ihåg IServiceScopeFactory - vi behöver ett nytt gränssnitt eftersom den ursprungliga begäran 's gränssnitttjänster kommer att bli avlägsna innan vår bakgrundsuppgift är klar
Nyckel: Det här är en slutgiltig konsistens modell. När alla länken är först upptäckt, den kommer i kö för att valideras | - | vi vet inte |' | vet inte om det ♪ ' | är brutet än | nästa Besuchern till den sidan ser den fixera länken.. För en blogg med regelbunden trafik, så är det typiskt så att brutna länkar blir fixade inom några timmar efter första upptäckten.
Att ersätta brutna länkar
När vi väl vet att en länk är bruten och har ett arkiv
foreach (var (originalUrl, archiveUrl) in archiveMappings)
{
if (html.Contains(originalUrl))
{
var tooltipText = $"Original link ({originalUrl}) is dead - archive.org version used";
var originalPattern = $"href=\"{originalUrl}\"";
var archivePattern = $"href=\"{archiveUrl}\" class=\"tooltip tooltip-warning\" " +
$"data-tip=\"{tooltipText}\" data-original-url=\"{originalUrl}\"";
html = html.Replace(originalPattern, archivePattern);
}
}
För inre brutna länkar, försöker vi semantiskt sökande först:
if (isInternal && semanticSearchService != null)
{
var replacement = await TryFindSemanticReplacementAsync(
brokenUrl, semanticSearchService, request, cancellationToken);
if (replacement != null)
{
html = ReplaceHref(html, brokenUrl, replacement);
continue;
}
}
// No replacement found - convert to plain text
html = RemoveHref(html, brokenUrl);
Del 2: bakgrundslinkskontroll
Mittvaran checkar inte länkar under sökningen, det skulle vara alldeles för långsamt. Istället stöter den upp länkar till bakgrundsbearbeitning via en databas Tabella. BrokenLinkCheckerBackgroundService hanterar den faktiska kontrollen.
Servicen körs timmer och gör två saker
- Se länkens Gültighet - HEAD frågar om länkarna är fortfarande levande
- Fetch archive.org URLs - För brutna länkar , hitta den närmaste historiska bilden
Vi är bra med det här.
request.Headers.UserAgent.ParseAdd(
"Mozilla/5.0 (compatible; MostlylucidBot/1.0; +https://www.mostlylucid.net)");
Detta låter webbplatsadministratörer se vad som träffar deras server, ,, och de kan kolla upp om de är nyfikna på oss.
Crucially, länkar är periodiskt re-kontrollerad. En länk som fungerade förra veckan kan vara död idag . Servicen plockar upp länkar som inte varit kontrollerade i de senaste 24 timmarna och kontrollerar dem igen . Hursomhelst, när viM SK6 har funnit ett arkivMska7org lösning för en bruten länkMske8 så gör vi inte, Mska9 gör ingen omställning till, M Ska10 kolla in originalet Mska11 det är Mska12 den är redan död och vi har en fungerande lösning, .
sequenceDiagram
participant BG as Background Service
participant DB as Database
participant Web as External Sites
participant Archive as Archive.org CDX API
loop Every Hour
BG->>DB: Get links not checked in 24h (batch of 20)
loop For each link
BG->>Web: HEAD request
Web-->>BG: Status code
BG->>DB: Update link status + LastCheckedAt
end
BG->>DB: Get broken links needing archive lookup
loop For each broken link
BG->>DB: Look up source post publish date
BG->>Archive: CDX API query (filtered by date)
Archive-->>BG: Closest snapshot
BG->>DB: Store archive URL (permanent)
end
end
Archive.org CDX API
Archive.org erbjuder en CDX (Capture IndexM SK2 API som låter oss söka efter korta bilder . Den smarta biten filtrerar efter datum
private async Task<string?> GetArchiveUrlAsync(
string originalUrl,
DateTime? beforeDate,
CancellationToken cancellationToken)
{
var queryParams = new List<string>
{
$"url={Uri.EscapeDataString(originalUrl)}",
"output=json",
"fl=timestamp,original,statuscode",
"filter=statuscode:200", // Only successful responses
"limit=1"
};
// Find snapshot closest to the blog post's publish date
if (beforeDate.HasValue)
{
queryParams.Add($"to={beforeDate.Value:yyyyMMdd}");
queryParams.Add("sort=closest");
queryParams.Add($"closest={beforeDate.Value:yyyyMMdd}");
}
var apiUrl = $"https://web.archive.org/cdx/search/cdx?{string.Join("&", queryParams)}";
// ... fetch and parse response
return $"https://web.archive.org/web/{timestamp}/{original}";
}
Det betyder att om jag skrev en artikel i 2008 som länkade till någon resurs, ♫ , ♫ Jag ♫' ♫ får archiviten ♫ МSK3 ♫ org-snapshot från runtomkring ♪ 2008, ♫ inte en modern version som kan vara helt annorlunda ♫
Link statusspårning
Vi följer länkar till en korrekt enhet:
[Table("broken_links", Schema = "mostlylucid")]
public class BrokenLinkEntity
{
public int Id { get; set; }
public string OriginalUrl { get; set; } = string.Empty;
public string? ArchiveUrl { get; set; }
public bool IsBroken { get; set; } = false;
public int? LastStatusCode { get; set; }
public DateTimeOffset? LastCheckedAt { get; set; }
public int ConsecutiveFailures { get; set; } = 0;
public string? SourcePageUrl { get; set; } // For publish date lookup
}
Tydliga återkontroller
Nyckeln till att fånga framtida bråk är att: re
public async Task<List<BrokenLinkEntity>> GetLinksToCheckAsync(int batchSize, CancellationToken cancellationToken)
{
var cutoff = DateTimeOffset.UtcNow.AddHours(-24);
return await _dbContext.BrokenLinks
.Where(x => x.LastCheckedAt == null || x.LastCheckedAt < cutoff)
.OrderBy(x => x.LastCheckedAt ?? DateTimeOffset.MinValue) // Oldest first
.Take(batchSize)
.ToListAsync(cancellationToken);
}
Det betyder att varje länk blir validerad minst en gång om dagen. ConsecutiveFailures fält låter oss vara lite förlåtande. - vi markerar inte en länk som bruten efter en tröstlig fel.
Del 3: Att hantera inkomande begäran
På andra sidan av mynten- -finns människor- -och sökmotorer- -som frågar efter URL-sidor som inte existerar.
- Gamla URL-schemas - Min forntida blogg använde vägar som
/archive/2006/05/15/123.aspx. En del av dessa är fortfarande indexerade , bookmarkade M SK2 eller länkade från andra platser . - Typos - Någon fet - fingerar URL: n eller kopierar den fel
- Partiella sluggar - sökmotorer indekserar ibland konstiga fragment
Det semantiska söksystemet är särskilt hjälpsamt här. Även utan att ha en direkt matchning mellan slug-slangar , kan vi extrahera meningsfulla termer från den önskade URL och hitta innehåll som semantiskt matchar.
Varför inte Middleware?
Min första instinkt var att hantera redirectioner i middleware. - intercepta begäran tidigt. ♫ , ♫ kolla efter kända redirektiver. ♪ , ♫ och redirectera innan routingen till och med hamnar i . kunde jobbat.
// DON'T DO THIS - runs on EVERY request
public class SlugRedirectMiddleware(RequestDelegate next)
{
public async Task InvokeAsync(HttpContext context, ISlugSuggestionService? service)
{
if (context.Request.Path.StartsWithSegments("/blog"))
{
var targetSlug = await service.GetAutoRedirectSlugAsync(slug, language);
if (!string.IsNullOrWhiteSpace(targetSlug))
{
context.Response.Redirect($"/blog/{targetSlug}", permanent: true);
return;
}
}
await next(context);
}
}
Problemet? Den kör på varenda enskild begäran till /blog/*. Det är en database-frager på varje sidauppfattning , även för fullständigt korrekta URLs | . | För en blogg med hyfsad trafik
Det mycket bättre sättet att göra det är att ansluta : till 404 handledaren. Om ASPM SK3NET redan har bedömt att sidan inte existerar sedan vi ser efter redirectioner. Inga bortslösade frågor på korrekta sidor.
Fun fact: I början av ASP.NET MVC så gjorde vi vänliga URLs fungerade, detta var hur vi gjorde..~ Scott Guthrie~ '~ flygprojektet som började med MVC använde den här metoden.;~ kopplade in i IIS: n, 404~ hanterade systemet, ,~ läst URL: n och serverade rätt innehåll, M SK7~ Det var ALSO vad jag använde i ett system som jag byggt i WebFormer.
Lärningssystemet
När någon träffar ett 404, så visar vi dem förslag med fuzzy-string matchning och | optional | ( | semantisk sök | . | Om de klickar på ett förslag |, | spelar vi in det |
stateDiagram-v2
[*] --> RequestReceived
RequestReceived --> RoutingCheck
RoutingCheck --> PageFound: Exists
RoutingCheck --> NotFound: 404
PageFound --> [*]
NotFound --> ErrorController
ErrorController --> CheckLearnedRedirect
CheckLearnedRedirect --> Redirect301: Has learned redirect
CheckLearnedRedirect --> CheckHighConfidence: No learned redirect
CheckHighConfidence --> Redirect302: Score >= 0.85 & gap >= 0.15
CheckHighConfidence --> ShowSuggestions: Lower confidence
ShowSuggestions --> UserClicks
UserClicks --> RecordClick
RecordClick --> UpdateWeight
UpdateWeight --> CheckThreshold
CheckThreshold --> EnableAutoRedirect: Weight >= 5 & confidence >= 70%
CheckThreshold --> [*]: Below threshold
EnableAutoRedirect --> [*]
404 Handler
All den omförflyttade logiken lever i ErrorController. ASP UseStatusCodePagesWithReExecute middleware re- löser utmaningen genom vår felbehandlare när en 404 uppstår
public class ErrorController(
BaseControllerService baseControllerService,
ILogger<ErrorController> logger,
ISlugSuggestionService? slugSuggestionService = null) : BaseController(baseControllerService, logger)
{
[Route("/error/{statusCode}")]
[HttpGet]
public async Task<IActionResult> HandleError(int statusCode, CancellationToken cancellationToken = default)
{
var statusCodeReExecuteFeature = HttpContext.Features.Get<IStatusCodeReExecuteFeature>();
switch (statusCode)
{
case 404:
// Check for auto-redirects before showing 404 page
var autoRedirectResult = await TryAutoRedirectAsync(statusCodeReExecuteFeature, cancellationToken);
if (autoRedirectResult != null)
return autoRedirectResult;
var model = await CreateNotFoundModel(statusCodeReExecuteFeature, cancellationToken);
return View("NotFound", model);
case 500:
return View("ServerError");
default:
return View("Error");
}
}
}
Den TryAutoRedirectAsync metod hanterar både lärde redirecter och höga
private async Task<IActionResult?> TryAutoRedirectAsync(
IStatusCodeReExecuteFeature? statusCodeReExecuteFeature,
CancellationToken cancellationToken)
{
if (slugSuggestionService == null || statusCodeReExecuteFeature == null)
return null;
var originalPath = statusCodeReExecuteFeature.OriginalPath ?? string.Empty;
var (slug, language) = ExtractSlugAndLanguage(originalPath);
// First: check for learned redirects (user previously clicked a suggestion)
// These get 301 Permanent Redirect - confirmed patterns
var learnedTargetSlug = await slugSuggestionService.GetAutoRedirectSlugAsync(
slug, language, cancellationToken);
if (!string.IsNullOrWhiteSpace(learnedTargetSlug))
{
var redirectUrl = BuildRedirectUrl(learnedTargetSlug, language);
logger.LogInformation("Learned auto-redirect (301): {Original} -> {Target}", originalPath, redirectUrl);
return RedirectPermanent(redirectUrl);
}
// Second: check for high-confidence first-time matches
// These get 302 Temporary Redirect until confirmed by user clicks
var firstTimeTargetSlug = await slugSuggestionService.GetFirstTimeAutoRedirectSlugAsync(
slug, language, cancellationToken);
if (!string.IsNullOrWhiteSpace(firstTimeTargetSlug))
{
var redirectUrl = BuildRedirectUrl(firstTimeTargetSlug, language);
logger.LogInformation("First-time auto-redirect (302): {Original} -> {Target}", originalPath, redirectUrl);
return Redirect(redirectUrl);
}
return null; // No redirect - show suggestions
}
Sammlingstillgång
Suggestionstjänsterna använder Levenshtein avstånd ( redigera avstånd) plus lite heuristikM SK2
private double CalculateSimilarity(string source, string target)
{
if (string.Equals(source, target, StringComparison.OrdinalIgnoreCase))
return 1.0;
source = source.ToLowerInvariant();
target = target.ToLowerInvariant();
// Levenshtein distance converted to similarity (0-1)
var distance = CalculateLevenshteinDistance(source, target);
var maxLength = Math.Max(source.Length, target.Length);
var levenshteinSimilarity = 1.0 - (double)distance / maxLength;
// Bonus if one string contains the other
var substringBonus = (source.Contains(target) || target.Contains(source)) ? 0.2 : 0.0;
// Bonus for common prefix (catches typos at the end)
var prefixLength = GetCommonPrefixLength(source, target);
var prefixBonus = (double)prefixLength / Math.Min(source.Length, target.Length) * 0.1;
return Math.Min(1.0, levenshteinSimilarity + substringBonus + prefixBonus);
}
Att lära sig av användarbeteenden
När en användare klickar på ett förslag, så registrerar vi det och uppdaterar självförtroende värdena.
public async Task RecordSuggestionClickAsync(
string requestedSlug,
string clickedSlug,
string language,
int suggestionPosition,
double originalScore,
CancellationToken cancellationToken = default)
{
var redirect = await _context.SlugRedirects
.FirstOrDefaultAsync(r =>
r.FromSlug == normalizedRequestedSlug &&
r.ToSlug == normalizedClickedSlug &&
r.Language == language,
cancellationToken);
if (redirect == null)
{
redirect = new SlugRedirectEntity
{
FromSlug = normalizedRequestedSlug,
ToSlug = normalizedClickedSlug,
Language = language,
Weight = 1
};
_context.SlugRedirects.Add(redirect);
}
else
{
redirect.Weight++;
redirect.LastClickedAt = DateTimeOffset.UtcNow;
}
redirect.UpdateConfidenceScore();
await _context.SaveChangesAsync(cancellationToken);
}
Tillförtroendetalberäkningen tar hänsyn till både klickar och intryck.
public void UpdateConfidenceScore()
{
var total = Weight + ShownCount;
ConfidenceScore = total > 0 ? (double)Weight / total : 0.0;
// Enable auto-redirect after 5+ clicks with 70%+ confidence
if (Weight >= AutoRedirectWeightThreshold &&
ConfidenceScore >= AutoRedirectConfidenceThreshold)
{
AutoRedirect = true;
}
}
Två-Tier Auto-Redirecter
Vi har två nivåer av automatiska omsändningar:
Först-tid hög-förtroende M SK2: Om likhetsnoten är >= 0.85 Och det finns ett signifikant mellanrum till den andraM SK4 bäst matchen, så skiftar vi direkt med en ♫ 302. Detta fångar upp tydliga typos
Lärda redirektiveringar (301): Efter att ♫ 5+ ♫ klickar med ♫70%+ ♫ självförtroende ♫
public async Task<string?> GetFirstTimeAutoRedirectSlugAsync(
string requestedSlug,
string language,
CancellationToken cancellationToken = default)
{
var suggestions = await GetSuggestionsWithScoreAsync(requestedSlug, language, 2, cancellationToken);
if (suggestions.Count == 0) return null;
var topMatch = suggestions[0];
// Need high confidence
if (topMatch.Score < 0.85) return null;
// If there's only one suggestion with high score, redirect
if (suggestions.Count == 1) return topMatch.Post.Slug;
// Multiple suggestions - only redirect if there's a clear winner
var scoreGap = topMatch.Score - suggestions[1].Score;
if (scoreGap >= 0.15)
return topMatch.Post.Slug;
return null; // Too close to call - show suggestions instead
}
Att sätta allt samman
När medverkan blir begriplig ( och när det inte gör det 't)
För utgående länkar, mellanverk är rätt val BrokenLinkArchiveMiddleware behöver interceptera HTML: s respons efter it' har skickats men före det' skickas till klienten . Det finns inget annat rimligt ställe att göra detta på ♫ - ♫ behöver vi modifiera reaktionsflödet ♫
För inkommande redirectioner, mellanprogram är fel valet . Vi' skulle köra datauppsökningar på varje /blog/* ställde frågan bara för att kolla om den här URL: s kanske behöver en redirection. . Handlarens greppet driver bara den där logiken när vi redan har sett att sidan inte existerar.
// In Program.cs
app.UseStatusCodePagesWithReExecute("/error/{0}"); // Handles 404s through ErrorController
app.UseStaticFiles();
app.UseRouting();
// ... other middleware
app.UseBrokenLinkArchive(); // Process outgoing links in responses (ONLY for middleware)
Flöden ser ut så här
flowchart LR
subgraph Request["Request Processing"]
direction TB
A[Request] --> B[Static Files]
B --> C[Routing]
C --> D{Page exists?}
D -->|Yes| E[MVC/Endpoints]
D -->|No| F[ErrorController 404]
F --> G{Auto-redirect?}
G -->|Yes| H[Redirect]
G -->|No| I[Show suggestions]
end
subgraph Response["Response Processing"]
direction TB
E --> J[BrokenLinkArchiveMiddleware]
J --> K[Link Extraction]
K --> L[Link Replacement]
L --> M[Response]
end
subgraph Background["Background Processing"]
direction TB
N[BrokenLinkCheckerService] --> O[Check URLs]
O --> P[Fetch Archive.org]
P --> Q[Update Database]
end
K -.->|Register links| N
style F stroke:#ef4444,stroke-width:3px
style J stroke:#8b5cf6,stroke-width:3px
style N stroke:#f59e0b,stroke-width:3px
Slutsatsen
Det här systemet har fungerat ett tag nu och det är rätt tillfredsställande att se det lära sig.
Huvudprinciperna:
- Don' blockerar begäran - använder bakgrundsprocessing för dyra operationer
- Lära sig från användare - deras klickar är värdefulla signaler
- Var konservativ med auto-redirectioner - kan bara skifta om när du är självsäkra
- Time- vet om archivering - hitta archive .org-snapshoter från när innehållet skrevs
- Glädjande nedbrytning - om tjänsterna inte är tillgängliga
Det är inte perfekt. Några länkar är verkligen för alltid borta. , och den semantiska sökningen hittar inte alltid rätt lösning, ' men det är bättre än att låta tusentals brutna länkar ljuga om
Kom snart: Semantisk sök Deep Dive
I den här veckan kommer jag publicera min semantiska sökserie som går in i mycket mer detaljer om hur vektorn- -och sökandet fungerar under kapseln. ISemanticSearchService.SearchAsync faktiskt finner " liknande " innehåll
Den kompletta källan finns i arkiven om du vill använda den för dina egna projekt.