Війна на 404 р.: Зміна старих речей (Українська (Ukrainian))

Війна на 404 р.: Зміна старих речей

Sunday, 23 November 2025

//

18 minute read

Вступ

Після ведення блогу з 2004 року (так, насправді), ви накопичили багато цифрового detritus. Нещодавно я імпортував свої старі дописи з 2004- 2009 ( https: // www. method. net/ blog/ category/ Imported) і виявив, що приблизно це все Зовнішні посилання, що вказують на місця, які зникли десять років тому, старі схеми URL, які більше не відповідають поточній структурі, всьому місту.

Проблема розпадається на три частини:

  1. Внутрішні посилання - Фіксовано під час процесу імпортування за допомогою my Інструмент імпортування ArchiveOrgМої старі дописи посилаються один на одного, використовуючи стару схему URL, тому я переписую її як частину міграцій.

  2. Зовнішні посилання (відсутній) - Це велике. Зв' язок з зовнішніми ресурсами, які вже зникли, пересунуто або стають зовсім іншими сайтами. Посилання на документацію з 2006 року завершено. Посилання на допис блогу від когось, хто вже давно знешкодив свій сайт? Мертвий. Для роботи з цим сайтом потрібна робота у режимі запуску.

  3. Вхідні запити - Люди (і пошукові рушії) все ще намагаються отримати доступ до старих адрес URL на зразок /archive/2006/05/15/123.aspxСистема семантичного пошуку часто може з'ясувати, що їм потрібно, навіть без точного збігу зі слимаком.

Тепер, я може було випалено для процесу імпортування архів. org. Але ось ще одна річ: зв' язки розриваються з часом. Можливо, наступного місяця не буде сайта, який працює сьогодні. Керуючи цим під час періодичної перевірки, система автоматично ловить майбутні Розриви, не тільки ті, які існували в часі імпортування.

У цій статті описано мій підхід:

  • Вихідні посилання: BrokenLinkArchiveMiddleware - заміняє мертві зовнішні посилання на знімки archive.org
  • Вхідні посилання: Обробник 404 з семантичним пошуком знайде правильний вміст навіть зі старих схем URL
  • Система навчання: спосіб, у який сайт стає розумнішим з часом від натискання користувачем
  • Обробка тла: Перевірка посилань без блокування запитів

Проблема зі старим вмістом

Ось дещо про інтернет: він не є постійною. Цей чудовий допис блогу, з яким ви пов' язалися у 2006 році? Його було пропущено. Цей сайт документації? Повторно створено три рази. Ваша власна схема URL, перш ніж ви встановитеся на відповідній з' єднаній програмі? Виявляння інформації.

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

Частина 1. Робота з вихідними посиланнями

Пошкоджене програмне забезпечення LinkArchive

Ця центральна програма перехоплює HTML- відповіді і виконує три дії:

  1. Видобути всі посилання для фонової перевірки
  2. Заміняє відомі пошкоджені зовнішні посилання версіями archive.org
  3. Застосовує семантичний пошук для пошуку замін для поламаних внутрішніх посилань

Ключовим є те, що ми хочемо знайти знімки архіву.org з десь під час написання дописуСценарій з 2024 статті 2006 року може посилатися на зовсім інший вміст. Отже, ми відшукаємо дату оприлюднення блогу і попросимо на найближчий знімок archive.org.

Ось основна структура:

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

Видирання посилань

Ми використовуємо створений regex для витягання всіх href Атрибути. The [GeneratedRegex] attribute у . NET надає нам створення компіляції- часу формального виразу, який є швидшим і вільним від пов' язування:

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

Тепер ви можете досить помірковано використовувати HtmlAglibibilityPack / https: // gitub.com/AngleSap/ AngleSap або подібні HTML (та інші) аналізери у цьому вікні для складніших сценаріїв, але з цією метою вона була перевершена - нам просто потрібно швидко видобути hrafs.

Зареєстрування посилань для перевірки тла

Ми не хочемо блокувати відповідь під час перевірки посилань. Замість цього, ми випускаємо фонове завдання:

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 - нам потрібна нова сфера, тому що обчислювальні послуги нашого запиту будуть відмінені до завершення нашого фонового завдання.

Важливе: Це Остаточна послідовність Зразок. any link спочатку виявлено, його буде поставлено до черги для перевірки - ми ще не знаємо, чи не пошкоджено. Служба тла перевіряє його (попит HEAD), і, якщо її пошкоджено, отримує заміну архіву. org. next Відвідувач цієї сторінки побачить фіксоване посилання. Для блогу зі звичайним трафіком це зазвичай означає, що поламані посилання будуть фіксовані протягом декількох годин після першого відкриття. Краса полягає в тому, що нам не потрібно вгадувати, які посилання можуть бути зламані, - ми просто підтверджуємо їх автоматично.

Заміна пошкоджених посилань

Після того, як ми знаємо, що посилання було пошкоджено і встановлено заміну archive.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);
    }
}

Для внутрішніх поламаних посилань спочатку спробуйте семантичний пошук:

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

Частина 2: перевірка тла посилання

Середня програма не перевіряє посилання під час запиту - це надто повільно. Замість цього у черзі буде знайдено посилання для обробки тла за допомогою таблиці бази даних, а також таблиці бази даних. BrokenLinkCheckerBackgroundService працює з поточною перевіркою.

Служба працює щогодини і виконує дві функції:

  1. Перевірити посилання коректно - Надійшов запит на HEAD, щоб перевірити, чи ще живі посилання.
  2. Отримати адреси archive.org - Для розбитих посилань знайти найближчий історичний знімок

Ми є добрими громадянами щодо цього - засіб перевірки використовує нетиповий User- Agent, який ідентифікує себе і посилається на сайт:

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

За допомогою цього пункту можна дізнатися про те, що відбувається з сервером, і про те, що вони можуть знайти нас, якщо їм цікаво. Крім того, ми надсилаємо запити (2 секунди між перевірками), щоб не забивати чийсь сервер.

По суті, посилання - це періодично перевірено повторно... посилання, що працював минулого тижня, може бути мертвим сьогодні. Служба знаходить посилання, які не було перевірено протягом останніх 24 годин, і знову їх перевіряє. Але після того, як ми знайшли заміну архіву.org на пошкоджене посилання, ми не перевіримо знову, що оригінальне, - воно вже мертве, і у нас є робоча заміна.

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 - це API CDX (Capture Indexment), за допомогою якого ми можемо надсилати запит на знімки екрана. Розумним бітом є фільтрування за датою:

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 році, пов'язаний з певним ресурсом, я отримаю знімок архіву.org десь з 2008 року, не сучасну версію, яка може бути зовсім іншою.

Слідкування за станом зв' язку

Ми стежимо за посиланнями з відповідним об'єктом:

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

Періодична перевірка

Ключем для виявлення наступних breakages є повторна перевірка. Запити служб щодо посилань, які не були нещодавно перевірені:

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

Це означає, що кожне посилання буде звірено принаймні раз на день. Якщо раніше робоче посилання почне повертати 404-ті, ми його зловимо і почнемо шукати заміну archive.org. ConsecutiveFailures Поле, яке дає нам можливість трохи пробачити - ми не позначаємо посилання як зламаний після однієї тимчасової помилки.

Частина 3. Обробка вхідних запитів

Тепер для іншої сторони монети: people (і пошукові рушії) з запитом на адреси URL, яких не існує. Серед них:

  • Старі схеми адрес URL - Мій стародавній блог використовував шляхи на зразок /archive/2006/05/15/123.aspxДеякі з них все ще індексовані, закладені закладками або пов'язані з іншими сайтами.
  • Typos - Хтось неправильно копіює адресу URL.
  • Часті слимаки - Пошукові двигуни іноді індексують дивні фрагменти.

Система семантичного пошуку є особливо корисною у цьому випадку. Навіть без прямого збігу з шаром, ми можемо видобути терміни з вказаної адреси URL і знайти вміст, який відповідає семантичному пошуку. Система знає як слимак, так і дані для кожного з дописів, отже вона може знайти відповідники до " close " навіть для абсолютно різних структур URL.

Чому вони не є Середніми книжками?

Моїм першим інстинктом було виконання переспрямувань у середньому перехопленні - завчасно, перевірка на відомі переспрямування та перенаправлення перед тим, як з'явиться потреба у переналаштуванні. може робота, і ось як вона виглядатиме:

// 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/*. Це запит до бази даних на всіх переглядах сторінок, навіть для цілком коректних адрес URL. Для блогу з належним навантаженням ви не з доброї причини маєте 99% часу для роботи з базою даних.

Набагато кращий підхід: зв' язатися з інструментом обробки 404. Якщо ASP.NET вже визначено, що сторінки не існує, потім No lost questions on valid pages.

Цікавим фактом є те, що на ранніх днях роботи ASP.NET MVC ми створили дружні адреси URL. Демонстрація літака Скотта Гутрі, яка запустила MVC, використала цей підхід; зв' язалася з системою обробки IIS 404, прочитала адресу URL і подала правильний зміст. Це було те, що я використовував у системі, яку я створив у WebForms 3 роки безперервно... і як я отримав PM- імпульс на команді ASP. NET!

Система навчання

Після того, як хтось натрапить на 404, ми покажемо їм пропозиції з використанням плавного пошуку рядків і (необов' язково) семантичного пошуку. Якщо вони натискають на пропозицію, ми записуємо її. Після достатнього клацання з достатньою довірою, ми розпочнемо автоматичне переспрямування.

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 Команда middleware перевизначає запит за допомогою нашого інструменту обробки помилок під час виконання 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");
        }
    }
}

The TryAutoRedirectAsync метод керує як вивченими переспрямуваннями, так і перенапруженнями з високою самовпевненістю у перший раз:

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
}

Оцінка подібності

Служба пропозиції використовує Lvenshtein дистанцію (edit distance) плюс деякі геуристики:

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

Вчімося від поведінки користувача

Після наведення вказівника миші на пропозицію з наступним клацанням лівою кнопкою миші, запис буде виконано, а отже, - оновлення рахунку довіри:

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

Двотиметровий автоматичний пов' язаний з

Ми маємо два рівні автоматичного перенаправлення:

  1. Перший високий рівень самовпевненості (302): Якщо рахунок подібності > = 0. 85 І існує суттєвий проміжок між другими значеннями, ми негайно переспрямовуємо дані на 302. Таким чином можна помітити очевидні помилки.

  2. Навчені переспрямування (301): Після натискання 5+ з довірою до 70% +, ми використовуємо постійне переспрямування 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
}

Усе це разом

Коли програмне забезпечення стає свідомим (а коли ні)

for Вихідний зв' язокПосередня програма - це правильний вибір. BrokenLinkArchiveMiddleware має перехопити відповідь HTML після він був перекладений, але до Для цього не існує іншого розумного місця - нам потрібно змінити потік відповіді, а для цього розроблено програмне забезпечення.

for Вхідні переспрямуванняМи б проводили запити до баз даних з кожного /blog/* щоб перевірити чи ця адреса URL може потребувати переспрямування. підхід до обробки 404 запускає цю логіку, лише якщо ми вже визначили, що сторінки не існує - набагато ефективніше.

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

Висновки

Ця система вже давно виконується, і вона, скоріше, задовольняється спостереженням за її навчанням. База даних поступово заповнює карти переспрямування, пошкоджені посилання отримують заміни archive.org, і користувачі рідко коли бачать наготу 404 більше.

Ключові принципи:

  • Не блокуйте запити - використовувати фонову обробку для дорогих операцій
  • Вчити у користувачів - їхні клацання є цінними сигналами
  • Будьте консервативними з автоматичними каталогами - только перенаправить, когда ты уверен.
  • Розподіл часу для архівування - знайти знімки archive.org з часу написання вмісту
  • Милосердна деградація - якщо сервіси недоступні, просто продовжуйте нормально

Це не ідеально - деякі посилання справді зникають, а семантичний пошук не завжди знаходить правильну заміну, але краще видно, ніж лишати тисячі зламаних посилань.

Незабаром: Семантичний пошук глибоко поринув

Цього тижня (w/ c 24 листопада 2025 року) Я оприлюднюю свою семантичну серію пошуку, яка буде докладніше розглянути як векторний пошук працює під капотом. Ця серія охопить створення, векторне зберігання QDant, і те, як все це пов' язане з цією системою обробки 404. Якщо вам цікаво, як саме ISemanticSearchService.SearchAsync насправді знаходить "симулярний" зміст, ось де ви захочете знайти.

Повне джерело знаходиться у сховищі, якщо ви бажаєте з' єднати його з вашим власним проектом.

Finding related posts...
logo

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