Back to "Der Krieg gegen die Herstellung von alten Sachen wieder Arbeit"

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

Der Krieg gegen die Herstellung von alten Sachen wieder Arbeit

Sunday, 23 November 2025

Einleitung

Wenn du dich seit dem Bloggen von 2004 befindest, habe ich vor kurzem meine alten Beiträge aus 2004-2009 importiert. alles wurde gebrochen .. . Externe Links zeigen auf Websites, die vor einem Jahrzehnt verschwanden ?, . alte URL-Schema, die nicht mehr mit der aktuellen Struktur entsprechen .

HINWEIS:-Dies kommt bald - ,- zusammen mit der Semantic Search Serie UND Funktionalität auf dem Blog - .-Nur eine große Änderung und ich hatte ein paar kleinere Artikel, um zuerst herauszukommen , .-Genießen Sie , (-aber Zeug ist noch gebrochen im Moment

Das Problem bricht in drei Teile auf

  1. Interne Verbindungen Behoben während des Importprozesses selbst mit meinem ArchivOrg Importeur WerkzeugMeine alten Beiträge referenzierten sich gegenseitig mit dem alten URL-Schema, also habe ich sie als Teil der Migration neu geschrieben.

  2. Externe Links Dies ist die große One-S.. Links zu externen Ressourcen, die seitdem verschwunden sind ,. moved.,. or become völlig verschiedene Sites... Ein Link zu einer Dokumentation von 2006?. Gone... Ein Verweis auf einen Blogbeitrag von jemandem, der seine Website längst nach unten genommen hat.MSC8. Dead.MSSK9. Diese müssen Runtime-Handling

  3. Eingehende Anträge Menschen und Suchmaschinen versuchen immer noch, auf alte URLs wie /archive/2006/05/15/123.aspxDas semantische Suchsystem kann oft herausfinden, was sie auch ohne eine exakte Schnecke waren.

Jetzt ,,, I könnte haben das Archiv gebacken ,. ,org lookups in den Importprozess ,. Aber hier könnte die Sache ' , : , Links brechen im Laufe der Zeit , ein Aufstellungsort, dass ,' , arbeiten heute könnte im nächsten Monat gegangen sein .. , Durch die Handhabung dieses zur Laufzeit mit periodischen re ,mesK7 checking , das System automatisch fängt Zukunft breakages-,-nicht nur diejenigen, die zum Importzeitpunkt existierten

Dieser Artikel deckt meinen Ansatz ab.

  • Auslaufende Verbindungen: BrokenLinkArchiveMiddleware Ersetzt tote externe Links durch Archiv-Snapshots
  • Eingehende VerknüpfungenDer Handler mit semantischer Suche findet den richtigen Inhalt auch aus alten URL-Schema
  • Das LernsystemWie die Website im Laufe der Zeit von Benutzerklicks intelligenter wird
  • HintergrundbearbeitungÜberprüfe Links ohne Sperranfragen

Das Problem mit alten Inhalten

Hier ist die Sache über das Internet, :.'.s nicht permanent, .. Das ausgezeichnete Blog-Post, mit dem Sie in Link 2006?.sgone..... that documentation site??.s restructured drei Mal Â..s Ihre eigene URL-Schema von bevor Sie sich auf einer korrekten schlampigen Konvention siedelten...

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

Der naive Ansatz ist es, Verbindungen manuell zu beheben...Aber wenn du Hunderte von Pfosten mit Tausenden von Links erhalten hast, ,. Dass...'. nicht auf .. Wir brauchen Automatisierung.

Architektur im Überblick

Das System verfügt über zwei Hauptkomponenten, die in Tandem arbeiten.

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

Das gebrocheneLinkArchivMittleres

Diese Middleware fängt HTML-Antworten ab und macht drei Dinge

  1. Extrahiert alle Links zur Hintergrundprüfung
  2. Ersetzt bekannte defekte externe Links mit Archiv-Versionen
  3. Verwendet semantische Suche, um Ersatz für defekte interne Links zu finden

Die wichtigste Einsicht hier ist, dass wir Archiv finden möchten. um die Zeit, als der Beitrag geschrieben wurdeEin Snapshot aus einem Artikel, der sich auf ganz andere Inhalte beziehen könnte, also suchen wir den Blog-Post nach dem Datum der Veröffentlichung und fragen das Archiv nach.

Hier ist die Kernstruktur.

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);
        }
    }
}

Verknüpfungen extrahieren

Wir benutzen einen generierten Regex, um alle href Attribute: [GeneratedRegex] Attribut in .-NET gibt uns kompilieren -?time regex generation?, , die sowohl schneller als auch Allokation ist

[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();
}

Jetzt können Sie ziemlich vernünftig verwenden HtmlAgilityPack / , https.://,github ,. ,com.../ ,AngleSharp , / , AngleSharpe oder ähnliche HTML , und andere ,) , Parser hier für komplexere Szenarien , aber es war Overkill für diesen Zweck , - , wir müssen nur hrefs schnell extrahieren

Wir wollen nicht die Antwort blockieren, während wir Links prüfen. Stattdessen feuern wir eine Hintergrundaufgabe ab

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);
    });
}

Beachten Sie die IServiceScopeFactory Wir brauchen einen neuen Bereich, da die ursprüngliche Anfrage – '-Scoped Services vor Abschluss unserer Hintergrundaufgabe entsorgt werden.

WichtigDies ist ein Eventuelle Kohärenz Modell, wenn keine Link wird zuerst entdeckt ,, , es wird für die Validierung angekreuzt , - , wir wissen nicht , wenn es , ' , gebrochen noch ,. , Der Hintergrund-Service überprüft es .( ,HEAD Anfrage , und wenn es nicht ' , holt ein Archiv , . ,org ersatz , das Nächster Besucher zu dieser Seite wird den festen Link sehen.. Für einen Blog mit normalem Verkehr.MSC1. Dies bedeutet typischerweise gebrochene Links werden innerhalb von Stunden nach der ersten Entdeckung behoben. .. Die Schönheit ist, dass wir nicht wissen müssen, welche Links gebrochen werden könnten.-. wir validieren sie einfach alle automatisch.

Sobald wir wissen, dass ein Link kaputt ist und haben Sie ein Archiv..-Org Ersatz von einem früheren Hintergrund check. ),-Wir tauschen sie mit einem hilfreichen Tooltip aus.

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 interne defekte Links versuchen wir semantische Suche zuerst

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);

Die Middleware doesns.' checkt Links während der Anfrage, die viel zu langsam sein würde, . , statt ,, , es wartet entdeckt Links für Hintergrund-Verarbeitung über eine Datenbanktabelle und ein BrokenLinkCheckerBackgroundService behandelt die tatsächliche Prüfung .. .

Der Service läuft stündlich und macht zwei Dinge

  1. Überprüfung der Gültigkeit des Links HAUPTANFORDERUNGEN, ob Links noch am Leben sind
  2. Archiv einholen – URLs von .-org Für gebrochene Links finden Sie den nächsten historischen Snapshot

Wir sind gute Bürgerinnen und Bürger über dies, dass der Checker verwendet einen benutzerdefinierten Benutzer, der sich identifiziert und Links zurück zur Website, :

request.Headers.UserAgent.ParseAdd(
    "Mozilla/5.0 (compatible; MostlylucidBot/1.0; +https://www.mostlylucid.net)");

Dies lässt Website-Administratoren sehen, was die ''s schlagen ihren Server und sie können uns nachschlagen, wenn sie neugierig sind ''re Wir drosseln auch Anfragen.

Entscheidend sind: in regelmäßigen Abständen überprüftEin Link, der letzte Woche arbeitete, könnte heute tot sein.Der Service greift Links auf, die in den letzten Stunden überprüft wurden und prüft sie erneut... Jedoch, sobald wir ein Archiv gefunden haben, das '.org Ersatz für einen kaputten Link gefunden hat.

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

Die Archiv-CDX API

Archiv - .-org bietet eine CDX-Datei-Index-)-API, mit der wir nach Snapshots suchen können. Das clevere Bit filtert nach 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}";
}

Dies bedeutet, wenn ich schrieb einen Beitrag in -2008 linking to some ressource -, il IM' il erhalten Sie das Archiv -. org Snapshot von rund -mesK4 nicht eine moderne Version, die völlig anders sein könnte

Wir verfolgen Verbindungen mit einer korrekten Entity

[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
}

Periodische Überprüfung

Der Schlüssel zum Fangen von zukünftigen Brüchen ist re---Checking-.-Die Serviceanfragen für Links, die havenakes'-T vor kurzem validiert wurde.

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);
}

Das bedeutet, dass jeder Link mindestens einmal pro Tag re---validiert wird..-Wenn ein zuvor funktionierender Link beginnt zurückzukommen, fangen wir '-ll ihn und suchen nach einem Archiv.MSC5-org Ersatz ConsecutiveFailures Feld lässt uns ein wenig verzeihen -- , wir don .' , markieren Sie einen Link als gebrochen nach einem vorübergehenden Fehler .

Teil - Bearbeitung von eingehenden Anfragen

Jetzt für die andere Seite der Münze... Menschen und Suchmaschinen, die URLs anfordern, die nicht existieren....

  • Alte URL-Systeme Mein alter Blog benutzte Pfade wie /archive/2006/05/15/123.aspxEinige von ihnen sind immer noch indexiert, , bookmarked, oder von anderen Websites verlinkt
  • Typos Einem fetten - Finger die URL oder kopiert sie falsch
  • Teile von Schnecken Suchmaschinen manchmal indizieren seltsame Fragmente

Das semantische Suchsystem ist hier besonders hilfreich – auch ohne direktes Schneckenspiel – können wir sinnvolle Begriffe aus der angeforderten URL extrahieren und Inhalte finden, die semantisch zu . passen.Das System kennt sowohl die Schnecke als auch hat für jeden Post Daten einbettet., , so dass es genügend " schliessen kann.

Warum nicht Middleware?

Mein erster Instinkt war es, Redirects in Middleware zu handhaben.--Abfanganfragen frühzeitig zu überprüfen, ob bekannte Umleitungen vorliegen und umleiten, bevor das Routing überhaupt eintritt. könnte und hier, wie es aussehen würde

// 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);
    }
}

Das Problem: jede einzelne Anfrage zu /blog/*Eine Datenbank-Abfrage auf jeder Seitenansicht......auch für perfekt gültige URLs....... für einen Blog mit anständigem Verkehr........ die Datenbank ohne guten Grund hämmern....

Der viel bessere Ansatz-Angriff in den Handler von 404, wenn ASP..,NET bereits festgestellt hat, dass die Seite nicht existiert dann Wir überprüfen auf Weiterleitungen ,. , Keine verschwendeten Anfragen auf gültigen Seiten , . ,

Spass Tatsache, dass wir in den frühen Tagen von ASP .. .NET MVC dies war, wie wir freundliche URLs arbeiteten ?. .Scott Guthrie .' .s Flugzeug-Demo, die begann MVK verwendet diesen Ansatz ?; ? Haken in das IIS . 404 ? Handhabungssystem lesen Sie die URL und dienen den richtigen Inhalt ?. ? Es war AUCH, was ich in einem System verwendet habe, das ich in WebForms gebaut ? 3 ? Jahre pervious .

Das Lernsystem

Wenn jemand trifft ein 404, , zeigen wir ihnen Vorschläge mit fuzzy String Matching und optional ) , semantische Suche , wenn sie klicken Sie auf einen Vorschlag ,, , wir aufnehmen, dass nach genug Klicks mit ausreichendem Vertrauen , , , starten wir Auto , - , redirecting .

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 --> [*]

Der Handler

Die ganze Umleitungslogik lebt in der ErrorControllerASP-.-NET-'-s UseStatusCodePagesWithReExecute Middleware führt die Anfrage durch unseren Fehler-Handler wieder aus, wenn ein 404 auftritt.

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");
        }
    }
}

Das TryAutoRedirectAsync Methode handhabt sowohl gelernte Umleitungen als auch hohes Vertrauen zuerst-.Zeit-Matches

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
}

Ähnlichkeit Bewertung

Der Vorschlag Dienst verwendet Levenshtein Entfernung -( edit distance -) , plus einige Heuristiken-MSC2 .

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);
}

Von Benutzerverhalten lernen

Wenn ein Benutzer einen Vorschlag anklickt, nehmen wir ihn auf und aktualisieren die Konfidenzergebnisse.

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);
}

Die Konfidenz-Score-Berechnung berücksichtigt sowohl Klicks als auch Impressionen.

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;
    }
}

Zwei---Tier Auto---Redirect

Wir haben zwei Ebenen von automatischen Umleitungen .

  1. Erstes --Zeithoch--VertrauenWenn die Ähnlichkeitspunktzahl ist, >=... und dort ein signifikanter Abstand zu dem zweiten - beste Match ist, leiten wir sofort mit einem 302.. Dieser fängt offensichtliche Typos.

  2. Gelernte WeiterleitungenWir verwenden eine permanente Umleitung von . Dies ist die ", die Menschen gesprochen haben.

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
}

Alles zusammenstellen

Wenn Middleware Sinn macht und wenn es wirkt

Für ausgehende VerbindungenDie Mittelware ist die richtige Wahl. BrokenLinkArchiveMiddleware muss die HTML-Antwort abfangen nach es wurde gerendert, aber Vorher es ist ', die an den Client gesendet werden, es ist kein anderer vernünftiger Ort, um dieses zu tun, müssen wir den Antwortstrom ändern. , und Middleware ist für genau das ausgelegt.

Für eingehende WeiterleitungenMittelware ist die falsche Wahl ,. , wir , ' , führen Datenbank-Abfragen auf jedem /blog/* Anfrage nur, um zu überprüfen, ob vielleicht.,...möglicherweise... diese URL eine Umleitung benötigen könnte...... Der ..404... Handler-Ansatz läuft nur dann, wenn wir...'... bereits festgestellt haben, dass die Seite nicht existiert..

// 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)

Der Fluss sieht so aus

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

Schlußfolgerung

Dieses System läuft seit einer Weile und es ist ziemlich befriedigend zu beobachten, wie es lernt... Die Datenbank füllt sich allmählich mit Umleitungs-Mappings. ,. defekte Links erhalten Archiv.....org Ersetzungen.

Die wichtigsten Grundsätze

  • Anträge auf Blockierung von Don-'.t Verwenden Sie Hintergrundverarbeitung für teure Operationen
  • Lernen von Anwendern Ihre Klicks sind wertvolle Signale
  • Konservativ sein mit Auto- ---Redirects Nur umleiten, wenn Sie sich sicher sind
  • Archivierung von Zeit-- Archiv finden.®org Snapshots aus dem Zeitpunkt, an dem der Inhalt geschrieben wurde
  • Anmutige Erniedrigung Wenn Dienstleistungen verfügbar sind, dann setzen Sie einfach normal fort

Es ist nicht perfekt -. Einige Links sind wirklich für immer verschwunden , und semantische Suche doesn't'. Finden Sie immer den richtigen Ersatz, aber es ist ein verdammter Anblick besser als tausende von kaputten Links liegen lassen über ..

Demnächst erscheinend:-Semantische Suche Deep Dive

Diese Woche Veröffentlichen Sie meine semantische Suchreihe, die in viel mehr Detail geht darüber, wie der Vektor arbeitet - based Search unter dem Haube.. Diese Serie deckt einbettende Generation ?, ? Qdrant vector storage ? , ? und wie es alles in diesem ? 404 ? Handling-System ?. ? Wenn Sie neugierig darauf, wie ISemanticSearchService.SearchAsync tatsächlich findet " similar, " Inhalt, dass ', wo Sie suchen möchten .

Die komplette Quelle befindet sich im Repository, wenn Sie eines davon für Ihre eigenen Projekte nicken möchten.

logo

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