# Der Krieg gegen die Herstellung von alten Sachen wieder Arbeit

<!--category-- ASP.NET, C# , Middleware, 404s-->
<datetime class="hidden">2025-11-23T10:35</datetime>

## 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 Werkzeug](https://github.com/scottgal/mostlylucid.nugetpackages/tree/main/Mostlylucid.ArchiveOrg)Meine 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.aspx`Das 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üpfungen**Der Handler mit semantischer Suche findet den richtigen Inhalt auch aus alten URL-Schema
- **Das Lernsystem**Wie die Website im Laufe der Zeit von Benutzerklicks intelligenter wird
- **Hintergrundbearbeitung**Überprüfe Links ohne Sperranfragen

[TOC]

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

```mermaid
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.

```mermaid
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
```

## Teil - Bearbeitung von Outgoing-Links

### 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 wurde*Ein 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.

```csharp
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

```csharp
[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://html-agility-pack.net/) / , 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

### Registrierung von Links für die Hintergrundprüfung

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

```csharp
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.

**Wichtig**Dies 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.

### Ersetzen gebrochener Links

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.

```csharp
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

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

## Teil Überprüfter Hintergrund-Link

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, :

```csharp
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üft**Ein 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.

```mermaid
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.

```csharp
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

### Link Status Tracking

Wir verfolgen Verbindungen mit einer korrekten Entity

```csharp
[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.

```csharp
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.aspx`Einige 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

```csharp
// 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 .

```mermaid
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 `ErrorController`ASP-.-NET-'-s `UseStatusCodePagesWithReExecute` Middleware führt die Anfrage durch unseren Fehler-Handler wieder aus, wenn ein 404 auftritt.

```csharp
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

```csharp
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 .

```csharp
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.

```csharp
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.

```csharp
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--Vertrauen**Wenn 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 Weiterleitungen**Wir verwenden eine permanente Umleitung von . Dies ist die ", die Menschen gesprochen haben.

```csharp
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 Verbindungen**Die 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 Weiterleitungen**Mittelware 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..

```csharp
// 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

```mermaid
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.