Cuando has estado blogueando desde 2004 (sí, realmente), acumulas un montón de detritus digital. Recientemente importé mis antiguos posts de 2004-2009 (https://www.mostlylucid.net/blog/category/Imported) y descubrí que aproximadamente todo enlaces externos que apuntan a sitios que desaparecieron hace una década, viejos esquemas de URL que ya no coinciden con la estructura actual, todo el lote.
El problema se divide en tres partes:
Enlaces internos - Fijado durante el propio proceso de importación usando mi Herramienta de importación de ArchiveOrg. Mis antiguos posts se referían entre sí usando el antiguo esquema URL, así que reescribí aquellos como parte de la migración.
Enlaces externos (en curso) - Este es el grande. Enlaces a los recursos externos que desde entonces han desaparecido, se han movido, o se han convertido en sitios completamente diferentes. Un enlace a alguna documentación de 2006? Se ha ido. Una referencia a una publicación de blog por alguien que hace mucho tiempo ha tomado su sitio? Muerto. Estos necesitan manejo de tiempo de ejecución.
Solicitudes entrantes - La gente (y los motores de búsqueda) todavía tratan de acceder a URLs antiguas como /archive/2006/05/15/123.aspx. El sistema de búsqueda semántica a menudo puede averiguar lo que fueron después, incluso sin una coincidencia exacta de bala.
Ahora, yo podría han horneado las búsquedas de archive.org en el proceso de importación. Pero esta es la cosa: los enlaces rompen con el tiempo. Un sitio que está funcionando hoy podría desaparecer el próximo mes. Al manejar esto en tiempo de ejecución con re-chequeo periódico, el sistema captura automáticamente futuro roturas, no sólo las que existían en el momento de la importación.
Este artículo cubre mi enfoque:
BrokenLinkArchiveMiddleware - reemplaza los enlaces externos muertos con snapshots archive.orgEsto es lo que pasa con internet: no es permanente. ¿Esa excelente publicación de blog a la que enlazaste en 2006? ¿Se ha ido. Ese sitio de documentación? Reestructurado tres veces. ¿Tu propio esquema de URL de antes de instalarte en una convención adecuada?
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
El enfoque ingenuo es arreglar enlaces manualmente, pero cuando tienes cientos de mensajes con miles de enlaces, eso no está encendido, necesitamos automatización.
El sistema tiene dos componentes principales trabajando en tándem:
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
Este middleware intercepta las respuestas HTML y hace tres cosas:
La idea clave aquí es que queremos encontrar snapshots archive.org de alrededor del momento en que el post fue escrito. Una instantánea de 2024 de un artículo de 2006 podría hacer referencia a contenido completamente diferente. Así que buscamos la fecha de publicación del blog y pedimos archive.org para la instantánea más cercana.
Aquí está la estructura central:
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);
}
}
}
Utilizamos un regex generado para sacar todo href los atributos. [GeneratedRegex] atributo en .NET nos da generación de regex de tiempo de compilación, que es tanto más rápido como libre de asignación:
[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();
}
Ahora usted podría utilizar bastante razonablemente HtmlAgilityPack / https://github.com/AngleSharp/AngleSharp o analizadores HTML similares (y otros) aquí para escenarios más complejos, pero fue exagerado para este propósito - sólo tenemos que extraer hrefs rápidamente.
No queremos bloquear la respuesta mientras comprobamos los enlaces. En su lugar, disparamos una tarea de fondo:
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);
});
}
Nótese que: IServiceScopeFactory - Necesitamos un nuevo alcance porque los servicios de alcance original de la solicitud serán eliminados antes de que nuestra tarea de fondo se complete.
Importante: Esto es un consistencia eventual modelo. Cuando cualquier enlace se descubre por primera vez, se hace cola para la validación - no sabemos si está roto todavía. El servicio de fondo lo comprueba (solicitud HEAD), y si está roto, obtiene un reemplazo archivo.org. siguiente Para un blog con tráfico regular, esto típicamente significa que los enlaces rotos se arreglan dentro de horas del primer descubrimiento. La belleza es que no tenemos que adivinar qué enlaces podrían estar rotos - simplemente los validamos automáticamente.
Una vez que sabemos que un enlace está roto y tenemos un reemplazo de archivo.org (de una verificación de antecedentes anterior), los intercambiamos con un consejo útil:
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);
}
}
Para enlaces internos rotos, primero probamos la búsqueda semántica:
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);
El middleware no comprueba los enlaces durante la solicitud - que sería demasiado lento. En su lugar, las colas descubrió enlaces para el procesamiento de fondo a través de una tabla de base de datos, y un BrokenLinkCheckerBackgroundService se encarga de la comprobación real.
El servicio funciona cada hora y hace dos cosas:
Somos buenos ciudadanos sobre esto - el verificador utiliza un agente de usuario personalizado que se identifica a sí mismo y enlaza de nuevo al sitio:
request.Headers.UserAgent.ParseAdd(
"Mozilla/5.0 (compatible; MostlylucidBot/1.0; +https://www.mostlylucid.net)");
Esto permite a los administradores del sitio ver lo que está golpeando su servidor, y pueden buscarnos si tienen curiosidad. También aceleramos peticiones (2 segundos entre cheques) para evitar martillar el servidor de cualquier persona.
Principalmente, los enlaces son revisados de nuevo periódicamenteUn enlace que funcionaba la semana pasada podría estar muerto hoy. El servicio recoge enlaces que no han sido revisados en las últimas 24 horas y los verifica de nuevo. Sin embargo, una vez que hemos encontrado un reemplazo de archivo.org para un enlace roto, no revisamos de nuevo el original - ya está muerto y tenemos un reemplazo de trabajo.
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 proporciona una API de CDX (Capture Index) que nos permite consultar las instantáneas. El bit inteligente se filtra por fecha:
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}";
}
Esto significa que si escribía un post en 2008 enlazando con algún recurso, obtendré la instantánea de archive.org de alrededor de 2008, no una versión moderna que podría ser completamente diferente.
Rastreamos los vínculos con una entidad adecuada:
[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
}
La clave para capturar futuras rupturas es volver a comprobar. Las consultas de servicio para enlaces que no han sido validados recientemente:
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);
}
Esto significa que cada enlace se vuelve a validar al menos una vez al día. Si un enlace de trabajo anterior comienza a devolver 404s, lo cogeremos y empezaremos a buscar un reemplazo de archive.org. ConsecutiveFailures campo nos permite ser un poco indulgente - no marcamos un enlace como roto después de un error transitorio.
Ahora para el otro lado de la moneda: personas (y motores de búsqueda) solicitando URLs que no existen. Esto incluye:
/archive/2006/05/15/123.aspx. Algunos de ellos todavía están indexados, marcados con marcadores o enlazados desde otros sitios.El sistema de búsqueda semántica es particularmente útil aquí. Incluso sin una coincidencia directa de babosas, podemos extraer términos significativos de la URL solicitada y encontrar contenido que coincida semánticamente. El sistema conoce tanto la babosa y tiene datos incrustados para cada publicación, por lo que puede encontrar coincidencias "lo suficientemente cercanas" incluso para estructuras URL completamente diferentes.
Mi primer instinto fue manejar redireccionamientos en middleware - interceptar peticiones temprano, comprobar si las redirecciones conocidas, y redirigir antes de que el enrutamiento incluso se inicia. podría trabajo, y así es como se vería:
// 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);
}
}
¿El problema? todas y cada una de las solicitudes a /blog/*. Esa es una consulta de base de datos en cada vista de página, incluso para URLs perfectamente válidas. Para un blog con tráfico decente, usted está martillando la base de datos sin ninguna buena razón 99% de las veces.
El enfoque mucho mejor: enganchar en el controlador 404. Si ASP.NET ya ha determinado que la página no existe, entonces Comprobamos si hay redireccionamientos. No hay consultas perdidas en páginas válidas.
Hecho curioso: En los primeros días de ASP.NET MVC así fue como hicimos funcionar URLs amigables. Demo de avión de Scott Guthrie que comenzó MVC utilizó este enfoque; enganchar en el sistema de manejo IIS 404 , leer la URL y servir el contenido correcto. Fue TAMBIÉN lo que utilicé en un sistema que construí en WebForms 3 años atrás... y cómo conseguí un concierto PM en el equipo de ASP.NET!
Cuando alguien golpea un 404, le mostramos sugerencias usando coincidencias de cadenas borrosas y (opcionalmente) búsqueda semántica. Si hacen clic en una sugerencia, registramos eso. Después de suficientes clics con suficiente confianza, comenzamos la auto-redirección.
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 --> [*]
Toda la lógica de redireccionamiento vive en el ErrorController. ASP.NET's UseStatusCodePagesWithReExecute middleware re-ejecuta la solicitud a través de nuestro controlador de errores cuando ocurre 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");
}
}
}
Los TryAutoRedirectAsync método maneja tanto redireccionamientos aprendidos y partidos de alta confianza por primera vez:
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
}
El servicio de sugerencias utiliza la distancia de Levenshtein (distancia de edición) más algunas heurísticas:
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);
}
Cuando un usuario hace clic en una sugerencia, la registramos y actualizamos las puntuaciones de confianza:
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);
}
El cálculo de la puntuación de confianza considera tanto clics como impresiones:
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;
}
}
Tenemos dos niveles de redireccionamiento automático:
Primera vez alta confianza (302): Si la puntuación de similitud es > 0.85 Y hay una brecha significativa al segundo mejor partido, redirigimos inmediatamente con un 302. Esto atrapa errores tipográficos obvios.
Redireccionamientos aprendidos (301): Después de 5+ clics con un 70%+ de confianza, utilizamos una redireccionamiento permanente 301. Este es el caso "los humanos han hablado".
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
}
Por enlaces salientes, middleware es la elección correcta. BrokenLinkArchiveMiddleware necesita interceptar la respuesta HTML después ha sido renderizado, pero antes No hay otro lugar sensato para hacer esto - necesitamos modificar el flujo de respuesta, y middleware está diseñado exactamente para eso.
Por redireccionamientos entrantes, middleware es la elección equivocada. Estaríamos ejecutando consultas de base de datos en cada /blog/* petición sólo para comprobar si tal vez, posiblemente, esta URL podría necesitar una redireccionamiento. El enfoque de controlador 404 sólo se ejecuta esa lógica cuando ya hemos determinado que la página no existe - mucho más eficiente.
// 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)
El flujo se ve así:
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
Este sistema ha estado funcionando durante un tiempo y es bastante satisfactorio ver cómo aprende. La base de datos se llena gradualmente con asignaciones de redireccionamiento, enlaces rotos obtener reemplazos archive.org, y los usuarios rara vez ven un 404 desnudo ya.
Los principios fundamentales:
No es perfecto - algunos enlaces realmente se han ido para siempre, y la búsqueda semántica no siempre encuentra el reemplazo correcto. Pero es una maldita vista mejor que dejar miles de enlaces rotos mintiendo.
Esta semana (p/c 24 de noviembre de 2025) voy a publicar mi serie de búsquedas semánticas que entra en mucho más detalle sobre cómo funciona la búsqueda basada en vectores bajo el capó. Esa serie cubrirá la generación de incrustación, almacenamiento de vectores Qdrant, y cómo todo se vincula en este sistema de manejo 404. Si tienes curiosidad sobre cómo ISemanticSearchService.SearchAsync En realidad encuentra contenido "similar", ahí es donde querrás buscar.
La fuente completa se encuentra en el repositorio si desea robar algo de él para sus propios proyectos.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.