La guerre contre 404: Faire fonctionner de nouveau les vieilles choses (Français (French))

La guerre contre 404: Faire fonctionner de nouveau les vieilles choses

Sunday, 23 November 2025

//

20 minute read

Introduction

Lorsque vous ' avez blogé depuis 2004 (oui,en fait), vous accumulez beaucoup de débris numériquesM SK5 J’ai récemment importé mes anciens articles de SSK6 ( httpsMSC8wwwMRK9mostlylucidMNK10netMGK11blogMKK12categoryMZK13ImportedMMK14 et j’ai découvert qu’environ tout a été rompu. Liens externes pointant vers des sites disparus il y a une décennie

REMARQUE: Il arrive bientôt, ainsi que la série de recherche sémantique et les fonctionnalités sur le blog . Just a BIG change and I had a few smaller articles to get out firstMSC3 Enjoy M SK4but stuff is still broken right now 🤓)

Le problème se divise en trois parties:

  1. Liens internes - Fixé durant le processus d'importation lui-même en utilisant mon Outil d'importateur ArchiveOrg. mes anciens articles se sont référés l’un à l’autre en utilisant le vieux schéma de URL, donc je les ai réécrits dans le cadre de la migration

  2. Liens externes (outgoing) - Il s'agit de la plus grande . Liens à des ressources externes qui ont disparu depuis lors, sont déplacésM SK3 ou se sont transformées en sites complètement différentsMSC4 Un lien vers une documentation provenant de 2006? DépasséMST6 Une référence à un post sur le blog par quelqu'un qui MST7 a pris leur site depuis longtemps downMst8 MortMSt9 Ces éléments nécessitent une gestion du temps d'exécutionM st10

  3. Demandes incombant - Les gens (et les moteurs de recherche ) tentent toujours d’accéder aux URL anciens comme /archive/2006/05/15/123.aspx. Le système de recherche sémantique peut souvent déterminer ce qu’ils étaient après.

Maintenant, I de pouvoir ont fait cuire l’archive.org lookups dans le processus d’importationM SK1 Mais ici'c’est ce qui se passe : les liens brochent au fil du temps avenir les interruptions, non seulement celles qui existaient au moment de l’importation.

Cet article traite de mon approche:

  • Liens sortants: BrokenLinkArchiveMiddleware - remplace les liens externes morts par des archives
  • Liens incombant: L'interprète 404 avec la recherche sémantique - trouve le contenu approprié même des anciennes schémas URL
  • Le système d'apprentissage: Comment le site devient plus intelligent au fil du temps grâce aux clics des utilisateurs
  • Traitement du contexte: Vérifier les liens sans bloquer des demandes

Le problème des contenus anciens

Voici' c'est la chose à propos de l'InternetM SK1 il ' n'est pas permanent. Ce excellent blog post que vous avez lié dans MST4 disparuMSP5 Ce site de documentationMSV6 Restructuré trois foisMSS7 Votre propre schéma d'URL avant que vous ne vous établisez sur une convention appropriée en matière d'affaissementMSR8 gênantMSSS9

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’approche naïve consiste à fixer les liens manuellement.

Aperçu de l'architecture

Le système comporte deux composants principaux qui fonctionnent en 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

Partie 1: Traitement des liens sortants

BrokenLinkArchiveMiddleware

Ce middleware intercepte les réponses HTML et fait trois choses:

  1. Extrait tous les liens pour vérifier le contexte
  2. Remplace les liens externes défectueux connus par des versions archive.org
  3. Utilise la recherche sémantique pour trouver des remplacements de liens internes défectueux

Nous voulons trouver ici des snapshots d’archive.org de à peu près au moment où le poste a été écrit. Un aperçu d’un article de 2024 peut se référer à un contenu complètement différent.

Voici la structure de base :

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

Extraire des liens

Nous utilisons un regex généré pour retirer tous les href attributs. Le [GeneratedRegex] l'attribut dans .NET nous permet de compiler-génération du regex tempsM SK2 qui est à la fois plus rapide et plus répartie -freeMSC4

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

Maintenant, vous pouvez raisonnablement utiliser HtmlAgilityPack / httpsM SK1github.comMSC3AngleSharpMST4AnglesSharp ou HTML similaire (et d’autres parseurs MST6 ici pour des scénarios plus complexes mais il s’agissait de surkill à cette fin.

Liens d'inscription pour la vérification du contexte

Nous ne voulons pas bloquer la réponse en vérifiant les liens.

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

Notez le IServiceScopeFactory - nous avons besoin d’un nouveau champ de compétence parce que la demande initiale ' les services couverts seront éliminés avant que notre tâche de fond ne soit achevée.

Importance: C'est un la cohérence éventuelle modèle. Quand Toute le lien est découvert pour la première fois, il fait l'objet d'une queue de validation M SK1 nous ne savons pas'il n'est pas encore brouillé ' Le service de base le vérifie M SK5Requête du chefMSC6 et s'il est brouéMNK7le ,le reçoit un archiveMRK9remplacement par une source de données Suivant Le visiteur de cette page verra le lien fixe. Pour un blog avec un trafic régulier, cela signifie habituellement que les liens défectueux sont réglés dans des heures après la première découverteM SK2 La beauté est que nous n’avons pas à croire quels liens risquent d’être défectus.

Remplacement des liens bloqués

Une fois que nous savons qu’un lien est rompu et qu’il y a un archive.org de remplacement (de la vérification précédente de l’arrière-planM SK2, nous les échangons avec une mise à jour utile

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

Pour les liens internes défectueux, nous essayons la recherche sémantique d’abord:

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

Partie 2: Examen des liens de fond

Le middleware ne vérifie pas les liens pendant la demande. BrokenLinkCheckerBackgroundService gère le contrôle effectif.

Le service fonctionne en heures et fait deux choses:

  1. Vérifier la validité du lien - HEAD demande de voir si les liens sont encore vivants
  2. Trouver les adresses URL de l'archive.org - Pour les liens brisés, trouvez la photo historique la plus proche

Nous sommes des bons citoyens à cet égard.

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

Cela permet aux administrateurs du site de voir ce qui touche leur serveur, 's et ils peuvent nous consulter s’ils sont curieux.

Les liens suivants sont essentiels : Renouveler périodiquement -appuyé. Un lien qui fonctionnait la semaine dernière pourrait être mort aujourd’hui. Le service prend en charge des liens qui n’ont pas été vérifiés dans les dernières heures ' et les vérifie de nouveauM SK4 CependantMSC5 une fois que nousMNK6 avons trouvé un archiveMMK7 un remplacement d’org pour un lien défectueuxMRK8 nous ne l’avons pas encore fait,' nous n’en sommes pas encore arrivés à la version originale MNK11 c’est-à-dire ' il est déjà mort et nous disposons d’un remplaçant en cours de travailMBK13

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

The Archive.org CDX API

Archive.org fournit une API CDX M SK1Capture Index) qui nous permet de faire des recherches sur les photos prises à l'aide d'un fichier ci-dessous.

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

Cela signifie que si j’ai écrit un post dans 2008 liant à une ressource , je' obtiendrai l’archive .org snapshot de la région \2008, pas une version moderne qui pourrait être complètement différente

Surveillance de l'état des liens

Nous suivons les liens avec une entité appropriée:

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

Examen périodique Re-checking

La clé pour attirer les ruptures futures est de re-checking. Les demandes de service concernant des liens qui n’ont pas été validés récemmentM SK2

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

Cela signifie que chaque lien est validé au moins une fois par jour. ConsecutiveFailures le champ nous permet d’être un peu pardonnants - nous ne marquons pas une liaison comme défectueuse après une erreur transitoire

Partie 3: Traitement des demandes reçues

Maintenant, pour l’autre côté de la monnaie, les personnes qui demandent des URLs qui ne sont pas existantes dans le ' et les moteurs de recherche du . incluent ce qui suit :

  • Schemes URL anciens - Mon antique blog a utilisé des chemins comme /archive/2006/05/15/123.aspx. Certains d’entre eux sont encore indexés , affichés à l’index de la pageM SK2 ou liés d’autres sites.
  • Types - Someone fat-fingers the URL or copie it incorrectly .
  • Enlèvements partiels - Les moteurs de recherche indexent parfois des fragments étranges

Le système de recherche sémantique est particulièrement utile à cet égard. Même sans une correspondance directe entre les libellées d’un libellés de l’URL requise, nous pouvons extraire des termes significatifs de la URL demandée et trouver un contenu qui corresponde sémantiellement aux libelles d’une URL requise. Le système connaît aussi bien le libellement que les données embedding pour chaque postM SK3 de sorte qu’il peut trouver " clore suffisammentMSC5 des mots qui correspondent même à des structures URL complètement différentesMNK6

Pourquoi pas Middleware?

Mon premier instinct était de traiter les redirections dans le middleware - demandes d'intercepte précoces , vérifier pour les redirections connues, et rediriger avant que la routage ne soit même lancée en de pouvoir Travailler , et ici ' c'est ce qu'il ressemblerait

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

Le problème? Ce système fonctionne chaque demande unique à /blog/*. C'est ce que ' est une demande de base de données sur chaque page d'affichage

L'approche beaucoup meilleure: hook dans l'interface 404 handler . Si ASPM SK3NET a déjà déterminé que la page n'existe pas puis Nous vérifions les redirections. Aucune demande inutile sur des pages valides .

Fait intéressant: Au début de l’ASP.NET MVC c’est ainsi que nous avons fait fonctionner des URLs convivialesMSC2 Scott GuthrieM SK3 la démonstration aérienne qui a démarré MVC a utilisé cette approche ; couper dans le système IIS MST5 le système de manutention MST6 lire les URL et servir le contenu approprié Mst7 C’était ALSO ce que j’ai utilisé dans un système que je me suis construit dans WebForms

Le système d'apprentissage

Lorsque quelqu’un touche un 404,, nous leur montrons des suggestions en utilisant le parallèle de chaînes floues et une recherche sémantique.

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

Le manutentionnaire 404

Toute la logique de redirection vit dans le ErrorController. ASPM SK1NET's UseStatusCodePagesWithReExecute middleware re-execute la demande par l'entremise de notre gestionnaire d'erreurs lorsque se produit 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");
        }
    }
}

L'article 5 de la Loi sur l'immigration et le statut des réfugiés TryAutoRedirectAsync la méthode gère à la fois les redirections apprises et les matches de temps élevés-confidence premièreM SK1:

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
}

Évaluation de la similitude

Le service de suggestion utilise la distance de Levenshtein (distance d'éditionM SK1 plus quelques héuristiques:

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

Apprentissage à partir du comportement de l'utilisateur

Lorsque l’utilisateur clique sur une suggestion, nous la consignons et actualisons les cotes de confiance.

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

Le calcul du score de confiance tient compte à la fois des clics et des impressions:

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

Deux-Tier Auto-Redirection

Nous avons deux niveaux de redirection automatique:

  1. Premier-temps élevéM SK1confidence (302): Si la note de similitude est >= 0.85 ET qu’il y a une différence significative entre les deux matchs, nous redirectionnons immédiatement avec un

  2. Redirections apprises (301): Après les clics de 5+ avec la confiance de |70%+, nous utilisons un redirectionnement permanent de \301.

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
}

Mettre tout ensemble

Quand Middleware fait son sens (et quand il ne le fait pasM SK1t)

Pour Liens sortants, le logiciel de middleware est la bonne option BrokenLinkArchiveMiddleware doit intercepter la réponse HTML après il' a été rendu mais précédemment Il n’y a pas d’autre endroit raisonnable pour le faire - nous devons modifier le flux de réponse , et la middleware est conçue exactement pour ce qui suit :

Pour redirections incombant, le middleware est la mauvaise option . Nous'd’effectuerons des recherches sur chaque base de données /blog/* demande simplement de vérifier si peut-être , possible, cette URL pourrait avoir besoin d'un redirectionnement . L'approche de l'interprète 404 ne fonctionne que lorsque nous avons déjà déterminé que la page n'existe pasM SK5 non plus.

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

Le flux ressemble à ce qui suit:

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

Conclusion

Ce système fonctionne depuis un certain temps et il est plutôt satisfaisant de le voir apprendre.

Principes clés:

  • Don't demandes de blocage - utiliser le traitement de fond pour des opérations coûteuses
  • Étudier auprès des utilisateurs - leurs clics sont des signaux valables
  • Soyez prudent avec les redirections auto- - ne redirige que lorsque vous êtes confiant
  • Le temps-aware archiving - recherche d'archives. snapshots de l'org du moment où le contenu a été écrit
  • Dégradation gracieuse - si les services ne sont pas disponibles ' non disponible M SK2 se poursuivent normalement

Il n’est pas parfait. Certains liens sont vraiment perdus pour toujours et la recherche sémantique ne permet pas de trouver le remplacement approprié.

Coming Soon: Recherche sémantique Deep Dive

Cette semaine, je publierai une série de recherches sémantiques sur la façon dont fonctionne le vecteur basé sur - sous le capuchon. ISemanticSearchService.SearchAsync en fait trouve " similaire" contenuM SK2 queMSC3 est là où vous voudrez regarder

La source complète se trouve dans le répertoire si vous voulez en faire un extrait pour vos propres projets.

Finding related posts...
logo

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