This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
Sunday, 23 November 2025
Після ведення блогу з 2004 року (так, насправді), ви накопичили багато цифрового detritus. Нещодавно я імпортував свої старі дописи з 2004- 2009 ( https: // www. method. net/ blog/ category/ Imported) і виявив, що приблизно це все Зовнішні посилання, що вказують на місця, які зникли десять років тому, старі схеми URL, які більше не відповідають поточній структурі, всьому місту.
Проблема розпадається на три частини:
Внутрішні посилання - Фіксовано під час процесу імпортування за допомогою my Інструмент імпортування ArchiveOrgМої старі дописи посилаються один на одного, використовуючи стару схему URL, тому я переписую її як частину міграцій.
Зовнішні посилання (відсутній) - Це велике. Зв' язок з зовнішніми ресурсами, які вже зникли, пересунуто або стають зовсім іншими сайтами. Посилання на документацію з 2006 року завершено. Посилання на допис блогу від когось, хто вже давно знешкодив свій сайт? Мертвий. Для роботи з цим сайтом потрібна робота у режимі запуску.
Вхідні запити - Люди (і пошукові рушії) все ще намагаються отримати доступ до старих адрес URL на зразок /archive/2006/05/15/123.aspxСистема семантичного пошуку часто може з'ясувати, що їм потрібно, навіть без точного збігу зі слимаком.
Тепер, я може було випалено для процесу імпортування архів. org. Але ось ще одна річ: зв' язки розриваються з часом. Можливо, наступного місяця не буде сайта, який працює сьогодні. Керуючи цим під час періодичної перевірки, система автоматично ловить майбутні Розриви, не тільки ті, які існували в часі імпортування.
У цій статті описано мій підхід:
BrokenLinkArchiveMiddleware - заміняє мертві зовнішні посилання на знімки archive.orgОсь дещо про інтернет: він не є постійною. Цей чудовий допис блогу, з яким ви пов' язалися у 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
Ця центральна програма перехоплює HTML- відповіді і виконує три дії:
Ключовим є те, що ми хочемо знайти знімки архіву.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);
Середня програма не перевіряє посилання під час запиту - це надто повільно. Замість цього у черзі буде знайдено посилання для обробки тла за допомогою таблиці бази даних, а також таблиці бази даних. BrokenLinkCheckerBackgroundService працює з поточною перевіркою.
Служба працює щогодини і виконує дві функції:
Ми є добрими громадянами щодо цього - засіб перевірки використовує нетиповий 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
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 Поле, яке дає нам можливість трохи пробачити - ми не позначаємо посилання як зламаний після однієї тимчасової помилки.
Тепер для іншої сторони монети: people (і пошукові рушії) з запитом на адреси URL, яких не існує. Серед них:
/archive/2006/05/15/123.aspxДеякі з них все ще індексовані, закладені закладками або пов'язані з іншими сайтами.Система семантичного пошуку є особливо корисною у цьому випадку. Навіть без прямого збігу з шаром, ми можемо видобути терміни з вказаної адреси 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 --> [*]
Вся логіка переспрямування живе в 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;
}
}
Ми маємо два рівні автоматичного перенаправлення:
Перший високий рівень самовпевненості (302): Якщо рахунок подібності > = 0. 85 І існує суттєвий проміжок між другими значеннями, ми негайно переспрямовуємо дані на 302. Таким чином можна помітити очевидні помилки.
Навчені переспрямування (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 більше.
Ключові принципи:
Це не ідеально - деякі посилання справді зникають, а семантичний пошук не завжди знаходить правильну заміну, але краще видно, ніж лишати тисячі зламаних посилань.
Цього тижня (w/ c 24 листопада 2025 року) Я оприлюднюю свою семантичну серію пошуку, яка буде докладніше розглянути як векторний пошук працює під капотом. Ця серія охопить створення, векторне зберігання QDant, і те, як все це пов' язане з цією системою обробки 404. Якщо вам цікаво, як саме ISemanticSearchService.SearchAsync насправді знаходить "симулярний" зміст, ось де ви захочете знайти.
Повне джерело знаходиться у сховищі, якщо ви бажаєте з' єднати його з вашим власним проектом.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.