This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
Sunday, 23 November 2025
Quando si è blogging dal 2004 (sì, davvero), si accumulano un sacco di detriti digitali. Recentemente ho importato i miei vecchi messaggi dal 2004-2009 ( https://www.mostlylucid.net/blog/category/Imported) e scoperto che circa tutto Collegamenti esterni che puntano a siti scomparsi un decennio fa, vecchi schemi di URL che non corrispondono più alla struttura attuale, all'intero lotto.
Il problema si articola in tre parti:
Collegamenti interni - Fisso durante il processo di importazione stesso utilizzando il mio Strumento importatore di ArchiveOrg. I miei vecchi messaggi si riferivano l'un l'altro utilizzando il vecchio schema URL, così ho riscritto quelli come parte della migrazione.
Collegamenti esterni (in uscita) - Questo è il grande. Collegamenti a risorse esterne che da allora sono scomparsi, spostati, o diventano siti completamente diversi. Un link a qualche documentazione dal 2006? Andato. Un riferimento a un post del blog di qualcuno che è da tempo preso il loro sito giù? Morto. Questi hanno bisogno di gestione runtime.
Richieste in entrata - Persone (e motori di ricerca) ancora cercare di accedere a vecchi URL come /archive/2006/05/15/123.aspx. Il sistema di ricerca semantico può spesso capire cosa stavano cercando, anche senza una corrispondenza esatta slug.
Ora, io potrebbe Ma ecco la cosa: i link si rompono nel tempo. Un sito che funziona oggi potrebbe essere andato il mese prossimo. Gestindo questo a runtime con periodica ri-controllo, il sistema cattura automaticamente futuro Rotture, non solo quelle che esistevano al momento dell'importazione.
Questo articolo riguarda il mio approccio:
BrokenLinkArchiveMiddleware - sostituisce i collegamenti esterni morti con le istantanee di archive.orgEcco la cosa di internet: non è permanente. Quel blog post eccellente che hai collegato nel 2006? Andato. Quel sito di documentazione? Ristrutturato tre volte. Il tuo schema di URL da prima di stabilirsi su una corretta convenzione di slugging? Imbarazzante.
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
L'approccio ingenuo è quello di risolvere i link manualmente. Ma quando hai centinaia di post con migliaia di link, che non è acceso. Abbiamo bisogno di automazione.
Il sistema ha due componenti principali che lavorano in 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
Questo middleware intercetta le risposte HTML e fa tre cose:
L'intuizione chiave qui è che vogliamo trovare archive.org snapshot da intorno al momento in cui il post è stato scritto. Una snapshot dal 2024 di un articolo 2006 potrebbe fare riferimento a contenuti completamente diversi. Quindi cerchiamo la data di pubblicazione del post del blog e chiediamo ad archive.org la snapshot più vicina.
Ecco la struttura principale:
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);
}
}
}
Usiamo un regex generato per estrarre tutto href attributi. [GeneratedRegex] attributo in .NET ci dà la generazione del regex del tempo di compilazione, che è sia più veloce e allocation-free:
[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();
}
Ora puoi usare abbastanza ragionevolmente HtmlAgilityPack / https://github.com/AngleSharp/AngleSharp o simili HTML (e altri) parser qui per scenari più complessi, ma è stato eccessivo per questo scopo - abbiamo solo bisogno di estrarre hrefs rapidamente.
Non vogliamo bloccare la risposta durante il controllo dei collegamenti. Invece, accendiamo un'attività di sfondo:
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);
});
}
Notare la IServiceScopeFactory - abbiamo bisogno di un nuovo campo di applicazione perché i servizi oggetto della richiesta originale saranno disposti prima che il nostro compito di sfondo completa.
Importante: Questo è un eventuale coerenza modello. Quando qualsiasi il link viene scoperto per primo, viene messo in coda per la convalida - non sappiamo se è ancora rotto. Il servizio di background lo controlla (richiesta HEAD), e se è rotto, prende una sostituzione di archive.org. successivo Visitatore a quella pagina vedrà il link fisso. Per un blog con traffico regolare, questo significa tipicamente collegamenti rotti ottenere fissato entro ore dalla prima scoperta. La bellezza è che non dobbiamo indovinare quali collegamenti potrebbero essere rotti - li convalidiamo tutti automaticamente.
Una volta che sappiamo che un link è rotto e abbiamo una sostituzione di archive.org (da un controllo di sfondo precedente), li scambiamo con un utile suggerimento:
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);
}
}
Per i collegamenti interni rotti, proviamo prima la ricerca semantica:
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);
Il middleware non controlla i collegamenti durante la richiesta - che sarebbe troppo lento. Invece, in coda ha scoperto i collegamenti per l'elaborazione dello sfondo tramite una tabella del database, e un BrokenLinkCheckerBackgroundService gestisce il controllo effettivo.
Il servizio funziona ogni ora e fa due cose:
Siamo buoni cittadini su questo - la pedina utilizza un User-Agent personalizzato che si identifica e link al sito:
request.Headers.UserAgent.ParseAdd(
"Mozilla/5.0 (compatible; MostlylucidBot/1.0; +https://www.mostlylucid.net)");
Questo permette agli amministratori del sito di vedere cosa sta colpendo il loro server, e possono cercarci se sono curiosi. Abbiamo anche acceleratore richieste (2 secondi tra i controlli) per evitare di martellare il server di chiunque.
Fondamentalmente, i collegamenti sono periodicamente ricontrollato. Un link che stava lavorando la scorsa settimana potrebbe essere morto oggi. Il servizio raccoglie i collegamenti che non sono stati controllati nelle ultime 24 ore e li verifica di nuovo. Tuttavia, una volta trovato un sostituto di archive.org per un collegamento rotto, non ricontrolliamo l'originale - è già morto e abbiamo un sostituto di lavoro.
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 fornisce un'API CDX (Capture Index) che ci permette di interrogare le istantanee. Il bit intelligente filtra per data:
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}";
}
Questo significa che se ho scritto un post nel 2008 collegandomi ad alcune risorse, otterrò l'istantanea di archive.org da circa 2008, non una versione moderna che potrebbe essere completamente diversa.
Rintracciamo i collegamenti con un'entità propria:
[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
}
La chiave per catturare le rotture future è ri-controllare. Le query di servizio per i collegamenti che non sono stati convalidati recentemente:
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);
}
Questo significa che ogni link viene ri-convalidato almeno una volta al giorno. Se un link precedentemente funzionante inizia a restituire 404s, lo prenderemo e inizieremo a cercare una sostituzione di archive.org. ConsecutiveFailures campo ci permette di essere un po 'perdonante - non segniamo un collegamento come rotto dopo un errore transitorio.
Ora per l'altro lato della moneta: persone (e motori di ricerca) che richiedono URL che non esistono. Questo include:
/archive/2006/05/15/123.aspx. Alcuni di questi sono ancora indicizzati, segnalibri, o collegati da altri siti.Il sistema di ricerca semantica è particolarmente utile qui. Anche senza un match diretto di slug, possiamo estrarre termini significativi dall'URL richiesto e trovare contenuti che combaciano semanticamente. Il sistema conosce sia la slug che ha i dati di inserimento per ogni post, in modo da poter trovare "abbastanza" corrispondenze anche per strutture URL completamente diverse.
Il mio primo istinto è stato quello di gestire reindirizzamenti in middleware - intercettare le richieste in anticipo, controllare i reindirizzamenti noti, e reindirizzare prima del routing anche calci in. potrebbe lavoro, ed ecco come sarebbe:
// 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);
}
}
Il problema? ogni singola richiesta a /blog/*. Questa è una query di database su ogni pagina vista, anche per gli URL perfettamente validi. Per un blog con traffico decente, si sta martellando il database per nessun buon motivo 99% del tempo.
L'approccio molto migliore: gancio nel gestore 404. Se ASP.NET ha già determinato la pagina non esiste, poi controlliamo i reindirizzamenti. Nessuna richiesta sprecata su pagine valide.
Fatto divertente: Nei primi giorni di ASP.NET MVC questo è stato il modo in cui abbiamo fatto funzionare gli URL amichevoli. La demo di piano di Scott Guthrie che ha iniziato MVC ha usato questo approccio; gancio nel sistema di movimentazione IIS 404 , leggere l'URL e servire il contenuto giusto. Era anche quello che ho usato in un sistema che ho costruito in WebForms 3 anni pervious...e come ho ottenuto un concerto PM sul team ASP.NET!
Quando qualcuno colpisce un 404, mostriamo loro suggerimenti usando stringhe fuzzy matching e (opzionalmente) ricerca semantica. Se fanno clic su un suggerimento, lo registriamo. Dopo abbastanza click con sufficiente fiducia, iniziamo auto-redirezione.
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 --> [*]
Tutta la logica redirect vive nel ErrorController. ASP.NET's UseStatusCodePagesWithReExecute middleware re-esegui la richiesta tramite il nostro gestore di errore quando si verifica un 404:
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");
}
}
}
La TryAutoRedirectAsync metodo gestisce sia reindirizzamenti appresi e ad alta fiducia partite per la prima volta:
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
}
Il servizio di suggerimento utilizza distanza Levenshtein (distanza modifica) più alcune euristiche:
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);
}
Quando un utente fa clic su un suggerimento, lo registriamo e aggiorniamo i punteggi di confidenza:
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);
}
Il calcolo del punteggio di fiducia considera sia i click che le impressioni:
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;
}
}
Abbiamo due livelli di reindirizzamenti automatici:
Prima fiducia (302): Se il punteggio di somiglianza è < 0,85 E c'è un divario significativo nella seconda partita migliore, reindirizziamo immediatamente con un 302. Questo cattura errori di battitura evidenti.
Reindirizzamenti imparati (301): Dopo 5+ click con il 70%+ di fiducia, usiamo un reindirizzamento permanente 301. Questo è il caso "gli esseri umani hanno parlato."
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
}
Per Collegamenti in uscita, middleware è la scelta giusta. BrokenLinkArchiveMiddleware deve intercettare la risposta HTML dopo è stato reso ma prima Non c'e' altro posto sensato per farlo - dobbiamo modificare il flusso di risposta, e il middleware e' progettato proprio per questo.
Per reindirizzamenti in entrata, middleware è la scelta sbagliata. /blog/* richiesta solo per verificare se forse, forse, questo URL potrebbe avere bisogno di un reindirizzamento. L'approccio 404 gestore esegue questa logica solo quando abbiamo già determinato la pagina non esiste - molto più efficiente.
// 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)
Il flusso assomiglia a questo:
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
Questo sistema è stato in esecuzione per un po 'di tempo ed è piuttosto soddisfacente vederlo imparare. Il database si riempie gradualmente con redirect mapping, link rotti ottenere sostituzioni archive.org, e gli utenti raramente vedere un nudo 404 più.
I principi fondamentali:
Non è perfetto - alcuni collegamenti sono veramente andati per sempre, e la ricerca semantica non sempre trovare la giusta sostituzione. Ma è una dannata vista meglio che lasciare migliaia di collegamenti rotti in giro.
Questa settimana (p/c 24 novembre 2025) pubblicherò la mia serie di ricerca semantica che va in maggior dettaglio su come funziona la ricerca basata sui vettori sotto il cofano. Quella serie coprirà la generazione di embedding, lo storage vettoriale Qdrant, e come tutto si collega a questo sistema di gestione 404. Se siete curiosi di sapere come ISemanticSearchService.SearchAsync In realtà trova contenuto "simile," è lì che si desidera guardare.
Il sorgente completo è nel repository se si desidera rubare uno qualsiasi di esso per i propri progetti.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.