和"M404:"的战争 让老旧的东西再次发挥作用 (中文 (Chinese Simplified))

和"M404:"的战争 让老旧的东西再次发挥作用

Sunday, 23 November 2025

//

12 minute read

一. 导言导言 导言 导言 导言

我最近从<2004-2009 @( https{://}<www_. mostlylucid.Qnet}/blog@/}/}入口)>进口了我的旧文章,并发现了这一点。 一切一切 外部链接指向十年前消失的站点 , 旧的URL计划不再符合当前结构

与Semantic Search系列和功能一同即将到来,

问题分为三部分::

  1. 内部链接 @-_ 固定在进口过程中使用我的 归档导入工具@.我的旧文章使用旧的 URL 计划互相引用@,}所以我重写了那些作为移民的一部分的.

  2. 外部链接 @(}% autreating\ ) @M- 这是与外部资源的链接, 自此消失后#, @% 移动@,}或成为完全不同的站点 @.}一些来自<2006?> Gone.}的文件的链接

  3. 收到的请求 @- @People @(#和搜索引擎@)}仍然尝试访问旧的 URL,比如 /archive/2006/05/15/123.aspx语义搜索系统往往能找出他们到底在追逐什么 即使没有精确的鼻涕虫匹配

现在,#I 能够 @.}今天工作的这个网站下个月可能会消失@.。 通过定期在运行时使用“- checking_, ”来处理此系统自动抓取 未来 不仅在进口时间存在的内容@. @%

本篇文章涵盖我的方法 :

  • 流出链接: BrokenLinkArchiveMiddleware @-_@ org 快照取代已死外部链接
  • 接入链接: 带有语义搜索的 @-处理器 即使从旧的 URL 方案中也能找到正确的内容
  • 学习系统@: 网站如何随着时间推移从用户点击中变得更聪明?
  • A. 背景处理: 检查链接而不阻截请求

旧内容问题

@' service_situation at a professional committee\ :QQI it'}您在#2006?} Gone中链接到的绝佳博客文章: 文档站点=?}重新构建了三次

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

幼稚的方法是修复手动连接@.}但是当你得到了上百个有数千个链接的邮箱时... ...,

建筑结构概览

该系统有两个主要组成部分,以协同配合的方式工作。

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

处理流出链接

破断的连环工程中继器

这个中件拦截的 HTML 响应并做了三件事:

  1. 为背景检查提取所有链接
  2. 将已知断开的外部链接替换为归档@. @org 版本
  3. 使用语义搜索以寻找内部断开链接的替换符

关键的观点是 我们想要找到档案... .@org 在文章写作前后@._Q@2024}来自 @MS K1 文章的快照或许会引用完全不同的内容 @.}所以我们查一查博客上的文章:#'{s公布日期,

这里的' 核心结构@: @%

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

提取链接

我们用生成的正gex 来拉出全部 href 属性@. @%I [GeneratedRegex] Q.NET 中的属性向我们提供编译@-time regex generation @,}这既要快,又要分拨 #-free:}

[GeneratedRegex(@"<a[^>]*\shref\s*=\s*[""']([^""']+)[""'][^>]*>",
    RegexOptions.IgnoreCase | RegexOptions.Compiled)]
private static partial Regex HrefRegex();

private List<string> ExtractAllLinks(string html, HttpRequest request)
{
    var links = new List<string>();
    var matches = HrefRegex().Matches(html);

    foreach (Match match in matches)
    {
        var href = match.Groups[1].Value;

        // Skip special links (anchors, mailto, etc.)
        if (SkipPatterns.Any(p => href.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
            continue;

        if (Uri.TryCreate(href, UriKind.Absolute, out var uri))
        {
            if (uri.Scheme == "http" || uri.Scheme == "https")
                links.Add(href);
        }
        else if (href.StartsWith("/"))
        {
            // Convert relative URLs to absolute for tracking
            var baseUri = new UriBuilder(request.Scheme, request.Host.Host,
                request.Host.Port ?? (request.Scheme == "https" ? 443 : 80));
            links.Add(new Uri(baseUri.Uri, href).ToString());
        }
    }

    return links.Distinct().ToList();
}

现在你可以相当合理地使用 Html 灵活性包 / @https@://github.com}/AngleSharp/}安格勒沙爾普或类似的 HTML @(和這裡的其他{)采訪者 對於更複雜的假設,但為此目的超過於殺害@-我們只需要快速提取 hrefs_.

背景检查的注册链接

我们不想在检查链接时阻断响应@._% 取代#,}我们解除了背景任务 @:

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

注注: IServiceScopeFactory -我们需要一个新的范围, 因为原始请求@'s specasted services 在我们完成背景任务前将会被处理 @.}

重要: 这是 最终最终一致性 型号@. 任何 首次发现链接@,}它被排队作为验证 {-我们不知道它是否坏了吗? '*%s broken yet.}背景服务检查 它的<(>HEAD请求),} 如果它打破了='}#,}取回一个归档 *.}org replace.} 下一个 访问该页面的人会看到固定链接@. @% 对于一个定期流量的博客来说, 这通常意味着断开的连接在第一次发现后数小时内就被固定了 @.# 美丽的是我们不得不猜测哪些链接会被打破 #-}我们只是自动验证它们是否所有#.}

替换断开的链接

一旦我们知道一个链接被打破, 并有一个归档@.org 替换 @(。 从先前的背景检查),}我们用有用的工具提示来交换它们

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

对于内部断开的链接@ ,, 我们首先尝试语义搜索@ I:

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

Part {2:} 背景链接检查

中队列通过数据库表格@,}和 a 发现背景处理链接 BrokenLinkCheckerBackgroundService 处理实际检查@. @%

服务按小时运行,做两件事情:

  1. 检查链接有效性 -HEAD 请求查看链接是否还活着
  2. 获取归档@. @org URL 找到最接近历史的快照

校对者使用自定义用户“-”表示身份并链接到网站@:}

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

这让站点管理者看到什么 ' @%s击中他们的服务器@,}他们可以搜索我们,如果他们是'.}我们还请求在检查之间拨打+2秒 以避免敲击任何人_'}*s services *#.}

关键是 定期重新@- @ checked上周工作过的链接在今天可能已绝迹@. @The service picks up link that hasned '}在最后的24时间里被检查过,并再次验证了这些链接。 但是一旦我们找到了一个归档文件.},

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

CDX API 存档@. @org

Achild.org 提供 CDX {(} Capture Inview_)} API, 它让我们查询快照@.} 智能位元在日期前过滤:}

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

这表示如果我在“2008”中写下一篇文章, 连结到一些资源@,}(I') 会收到来自「.」的存档快照。

情况追踪链接

我们追踪与一个合适的实体的链接 :

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

周期性 Re- check

抓取未来断裂点的关键是重新@ - @ checking.}( 此服务查询) 。 最近已验证该链接的连接@ MPK2 @ t

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

这意味着每个链接每天至少一次被重新% - @ valiated 至少一次@ MS K1} 如果一个先前的工作连接开始返回 {404} @s,} we'} ConsecutiveFailures 字段让我们略微原谅\ - {我们不做\ I' t标记链接在短暂错误后断裂@.}\

处理收到的请求

现在对硬币的另一面来说, “: ” 人们“(”和搜索引擎“MS K2 ”要求有URL的 '“t 存在” 。 这包括‘:’

  • 旧的URL办法 @-我的古老博客使用路径, /archive/2006/05/15/123.aspx@._% 其中一些仍然被索引化@,}书签{,}或从其他网站连接到#.}
  • Typos 泰波 - @ someone fat@-#finger the URL 或者复制错误的.}
  • 部分子弹 搜索引擎有时会索引怪异的碎片

语义搜索系统在此特别有用 @ . @ 即使没有直接的子弹匹配@ MS K1} 我们也可以从所请求的 URL 中提取有意义的术语, 并找到与 . 字词相匹配的内容。 这个系统既了解弹体, 也嵌入了每个 post@,}的数据, 这样它就可以找到“ "Qclose ” 足够接近@ MPK5 匹配甚至完全不同的 網址结构@ I. {

为何不叫米德尔 ??

我的第一个本能是处理中间软件的重置 - 拦截请求\ , 检查已知的重定向\ , 和重新定位之前甚至踢前 能够 和这里的',它看起来像什么?

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

问题?}这持续着 每项要求 至 /blog/*@.#' 在每个页面视图上都有一个数据库查询@,(即使对于完全有效的 URLs%.) 对于一个有体面交通的博客来说,你{'(无缘无故地在数据库中敲打_99%(时间为<.>)*}

如果 ASP=.NET 已决定页面是否已存在 '}{,}, 时当时 我们检查重定向@.}在有效页面上没有浪费的查询 @.

在ASP=.NET MVC的最初几天, 我们如何制作友好的 URLs 工作?.Scott Guthrie'S平面演示开始MVC使用这个方法。 ;勾钩进入IMS @404处理系统。

学习系统

当有人点击“ 404,” 时, 我们用模糊字符串匹配和“ 可选” @ MS K2 语义搜索@ . 如果单击一个建议\ , 我们记录在足够多的点击后 “ MSKO5 ” 充分自信地开始自动“ I- 重新定向” @.

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”处理器

所有转方向逻辑 生命在 ErrorController@. @ASP#.NET@'%s UseStatusCodePagesWithReExecute 当发生“% 404” 事件时, 请通过我们的错误处理器执行请求@ MS K0 @ option: check

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

缩略 TryAutoRedirectAsync 方法既处理学习的重定向,又处理高端@- @sustrie first @-}时间匹配:

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
}

相似位置分布

推荐服务使用 Levenshtein 距离@(edit 距离 @)}加上一些超自然现象 @MS K2}

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

从用户行为中学习

当用户点击一个建议时 @ ,我们记录并更新信任评分@ MS K1#

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

信心评分计数既考虑点击又考虑印象 @:

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

two - @ Tier 自动@ MS K1 @ redirect

我们有两层自动重定向 :

  1. 第一次- 高一 一次-信任如果相似的分数是 @>= @ @0.85, 并且有#' 与第二个“-}最佳匹配”的差幅那么大的话 我们马上用“NSK6”重新定位

  2. **学习重定向@(301) @%**在“5+”点击「70%+」后,

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
}

团结在一起

当Middroup 使感官(和当它 '"T"MSSK2"

用于 退出链接@,中间器械是正确选择#. BrokenLinkArchiveMiddleware 需要来拦截 HTML 响应 之后 已经完成,但 之前 发送到客户端@. @ there @'}没有其它明智的地方可以做这个-}我们需要修改响应流{,}和中间软件是专门设计为“.”的

用于 输入的重定向, 中间工具是错误的选择 @.We'd 正在运行每个数据库查询 /blog/* 此 URL 可能需要重定向 < . {404\ } <% MS K4\ > > 处理器方法只有在我们已经确定该页面是否存在{'}\ t } @-} 有效度更高时才运行此逻辑@.}

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

流体看起来像这个:

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

结论结论

这个系统已经运行了一段时间, 它相当满意地看着它学习 .。 数据库逐渐填满了重新定位的映射@,断开的链接 得到归档.org 替换{, 用户很少看到裸体的#404 #.#

关键原则#:

  • Don't 区块请求 -在昂贵的操作中使用背景处理
  • 向用户学习 他们的点击是有价值的信号
  • 使用自动@- redirects 保持稳妥 @- 只有当您有信心时才会重新定向@ '
  • Time - @avore 归档 @- 找到内容撰写时的归档@.org快照
  • 优减降解 如果服务是'#t 可用 @,}请保持正常状态

语义搜索总是能找到合适的替代词 @.}但是它比留下成千上万个关于#.的断开链接更好看

即将到来#:语义搜索深海底

本周 @( @w /}{MSK 1}c MS K2}11月_2025)#'}将公布我的语义搜索序列。 该系列将会更详细地说明矢量@-}以矢量为基地的搜索如何在hun.}下工作。 这个系列将包括嵌入下一代=,QDrant矢量存储}♪,},以及它是如何与这个“404处理系统”连接的? ISemanticSearchService.SearchAsync @'}会真正找到您想要查看的“.” 位置上的 “'Qs ”

完整的源在存储器中, 如果您想为您自己的工程nick any of it@ . @

Finding related posts...
logo

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