Back to "Kriget mot 404: Att få gamla saker att fungera igen"

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

404s ASP.NET C# Middleware

Kriget mot 404: Att få gamla saker att fungera igen

Sunday, 23 November 2025

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

  1. 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.

  2. 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.

  3. 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

  1. Extraherar alla länkar för bakgrundskontroll
  2. Ersätter kända brutna externa länkar med archive.org versioner
  3. 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.

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

  1. Se länkens Gültighet - HEAD frågar om länkarna är fortfarande levande
  2. 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 ♫

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:

  1. 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

  2. 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.

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.