عندما كنت تدوّن منذ عام 2004 (نعم، حقاً، أنت تتراكم الكثير من ديتريتوس الرقمي، قمت مؤخراً باستيراد مواقعي القديمة من 2004-2009 (https://www.mostalyluccid.net/blog/ stem/ Imported) واكتشفت ذلك تقريباً " كل شيء " الروابط الخارجية تشير إلى المواقع التي اختفت منذ عقد مضى، مخططات المسار القديمة التي لم تعد مطابقة للبنية الحالية، كل شيء.
المشكلة تنقسم إلى ثلاثة أجزاء:
عوامل داخلية - تثبيتها أثناء عملية الاستيراد نفسها باستخدام أمر مستورد الأداةقام مكتبي القديم بمرجعية بعضهم البعض باستخدام مخطط المسار القديم لذا أعدت كتابتها كجزء من الهجرة
الوصلات الخارجية (الصادرة) - هذا هو الكبير. وصلات إلى الموارد الخارجية التي اختفت منذ ذلك الحين أو انتقلت أو أصبحت مواقع مختلفة تماما. وصلة إلى بعض الوثائق من عام 2006؟ ذهب. إشارة إلى تدوينة من قبل شخص ما الذي استغرق وقتا طويلا منذ أخذ موقعه إلى أسفل؟ ميت. هذا يحتاج إلى مناولة وقت التشغيل.
الطلبات والطلبات الواردة الواردة في الطلبات الواردة - الناس (ومحركات البحث) لا تزال تحاول الوصول إلى العناوين القديمة مثل /archive/2006/05/15/123.aspxنظام البحث الدلالي غالباً ما يستطيع معرفة ما كانوا يسعون خلفه حتى بدون تطابق دقيق
الآن، أنا يمكن الـ أرشيف. org ابحث في عملية الاستيراد. لكن هنا الشيء: الروابط كسر مع الوقت. الموقع الذي يعمل اليوم قد يكون قد ذهب في الشهر القادم. من خلال التعامل مع هذا في وقت التشغيل مع إعادة فحص دورية ، النظام تلقائياً يلتقط. في المستقبل في المستقبل الكسور، وليس فقط تلك التي كانت موجودة في وقت الاستيراد.
هذه المادة تغطي نهجي:
BrokenLinkArchiveMiddleware - استبدال الوصلات الخارجية الميتة مع أرشيف. orgهذا هو الشيء عن الإنترنت: هو ليس دائم. تلك التدوينة الممتازة التي ارتبطت بها في عام 2006؟ ذهب. ذلك الموقع الوثائقي؟ أعد تنظيمه ثلاث مرات. مخطط المسار الخاص بك من قبل أن تستقر في مؤتمر مناسب؟ التساهل.
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 قد تشير إلى محتوى مختلف تماماً، لذا نراجع تاريخ نشر التدوينة ونسأل الأرشيف. 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);
}
}
}
نستعمل ناتجاً متولداً لسحب كل href الصفات المميزة [GeneratedRegex] تتيح لنا الشبكة جيلاً تجمّعياً في الوقت المناسب، يكون أسرع وخالياً من التخصيص على حد سواء:
[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();
}
الآن يمكنك أن تستخدم استخداماً معقولاً HTmlL FPDPack / https://github.com/AngleSharp/AngleSharp أو ما شابه HTML (وغير ذلك) parsers هنا لسيناريوهات أكثر تعقيداً ولكنها كانت مبالغة لهذا الغرض - نحن فقط بحاجة لاستخراج 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 - نحن بحاجة إلى نطاق جديد لأن خدمات الطلب الأصلي المخططة سيتم التخلص منها قبل الانتهاء من مهمتنا الأساسية.
**1 ف-4، 1 ف-3، 1 خ م، 1 خ ع (رأ)، 1 خ ع (رأ)، 1 خ ع (رأ)، 1 خ ع (رأ)، 1 خ ع (رأ)، 1 خ ع (رأ)**هذا هو الاتساق في نهاية الأمر النموذج النموذجي للنموذج، متى - - - - - - - - - - تم اكتشافه أولاً، يتم تصنيفه للمصادقة - لا نعلم إن كان مكسوراً بعد. الخدمة الخلفية تتحقق منه (طلب HEAD)، وإذا كان معطوباً، فإنه يجلب أرشيفاً. org بديل. * * زائر تلك الصفحة سيرى الرابط الثابت. بالنسبة لمدوّنة مع حركة مرور منتظمة، هذا يعني عادةً أنّ الروابط المكسورة ستُثبّت في غضون ساعات من الاكتشاف الأول. الجمال هو أننا لا نحتاج إلى تخمين أي الروابط قد تُكسر - نحن فقط نتحقق منها جميعاً تلقائياً.
بمجرد أن نعرف أن الرابط قد تم كسره و لدينا أرشيف. 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);
البرمجيات الوسيطة لا تتحقق من الروابط خلال الطلب - ذلك سيكون بطيئاً جداً جداً. بدلاً من ذلك، فإنها تقوم بفرز الروابط المكتشفة لمعالجة الخلفية عن طريق جدول قاعدة بيانات، وa BrokenLinkCheckerBackgroundService يُعالجُ الفحصَ الفعليَ.
الخدمة تعمل بالساعة وتفعل شيئان:
نحن مواطنون جيدون حول هذا - المدقق يستخدم عامل مخصص المستخدم الذي يحدد نفسه ويربطه بالموقع:
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
Arche.org يوفر ADX (مؤشر) 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 مرتبطاً بمورد ما، سأحصل على لقطة أرشيف. 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
}
المفتاح إلى اصطياد المستقبل هو إعادة التدقيق. الخدمة إستفسارات لـ روابط ليس مؤكّد مؤخرًا:
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);
}
هذا يعني أن كل وصلة يتم إعادة صلاحيتها مرة واحدة في اليوم على الأقل. إذا كان هناك وصلة عمل سابقة تبدأ بإعادة 404s، سنمسكها ونبدأ بالبحث عن أرشيف. org بديل. ConsecutiveFailures دعونا نكون متسامحين قليلاً - نحن لا نضع علامة على رابط كما كسر بعد خطأ عابر واحد.
الآن بالنسبة للجانب الآخر من العملة: الناس (ومحركات البحث) الذين يطلبون URLات غير موجودة. وهذا يشمل:
/archive/2006/05/15/123.aspx- لا يزال بعضها مفهرساً أو معلماً أو مرتبطاً بمواقع أخرى.نظام البحث الدلالي مفيد بشكل خاص هنا. حتى بدون وجود تطابق كشطي مباشر، يمكننا استخراج مصطلحات ذات معنى من العنوان المطلوب والعثور على المحتوى الذي يتطابق مع الدلالية. يعرف النظام كلاً من الركيزة ويدمج البيانات لكل عمود، حتى يتمكن من العثور على مباريات "اغلق بما فيه الكفاية" حتى لبنيات العنوان المختلفة تماماً.
كانت غريزتي الاولى ان اتعامل مع اعادة التوجيه في البرامج المتوسطة - اعتراض الطلبات في وقت مبكر ، التحقق من عمليات اعادة التوجيه المعروفة ، وإعادة التوجيه قبل ان يبدأ التوجيه حتى . يمكن العمل، وهنا كيف سيبدو شكله:
// 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/*هذه قاعدة بيانات استعلامية في كل صفحة، حتى بالنسبة للعنوانات الصحيحة تماماً. بالنسبة لمدوّنة مع حركة مرور لائقة، أنت تدق قاعدة البيانات بدون سبب وجيه 99% من الوقت.
النهج الأفضل بكثير: إربط في الـ 404 control. إذا كان ASP.NET قد قرر بالفعل أن الصفحة غير موجودة، ثم لا تهدر الإستفسارات على صفحات صالحة.
الحقيقة: في الأيام الأولى من برنامج ASP.net MVC هذه هي الطريقة التي جعلنا بها الوصلات الودية تعمل. عرض طائرة سكوت غوثري الذي بدأ MVC استخدم هذا النهج; الولوج إلى نظام مناولة IIS 404, قراءة URL و خدمة المحتوى الصحيح. هو أيضاً ما استخدمته في نظام بنيته في WebForms 3 سنوات سابقة... وكيف حصلت على وظيفة رئيس الوزراء في فريق 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 --> [*]
كلّ إعادة توجيه كلّ المُجال الحيّات في ErrorControllerSP. SP. net UseStatusCodePagesWithReExecute واعادة توجيه الطلب من خلال مُعالج الخطأ عندما يحدث 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");
}
}
}
الـ 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
}
وتستخدم الخدمة المقترحة المسافة Levenshtein (المسافة المدية) بالإضافة إلى بعض أدوات التهوية:
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;
}
}
لدينا مستويين من إعادة التوجيه التلقائي:
**درجة عالية من الثقة في الدرجة الأولى لأول درجة )٢٠٣(**إذا كانت درجة التشابه > = 0.85، وهناك فجوة كبيرة إلى ثاني أفضل مباراة، فإننا نعيد توجيهها فورا مع 302. هذا يلتقط خطأ واضح.
**)٣٠١(**بعد 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
}
(أ) الوصلات المنتهية،البرمجيات الوسطى هي الخيار الصحيح الصحيح. BrokenLinkArchiveMiddleware إلى اعتراض استجابة HTML ما بعد تم تحويله ولكن ثالثاً لا يوجد مكان معقول آخر للقيام بذلك - نحن بحاجة إلى تعديل تيار الاستجابة، و برامج متوسطة مصممة لهذا بالضبط.
(أ) عدد الذين يردونالبرامج الوسطى هي الخيار الخطأ. سوف ندير قاعدة بيانات للاستفسارات على كل /blog/* طلب فقط للتأكد إذا كان ربما، ربما، هذا العنوان قد يحتاج إلى إعادة توجيه. نهج الـ 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
هذا النظام يعمل منذ فترة الآن، وهو بالأحرى مرضٍ لمشاهدته وهو يتعلم. قاعدة البيانات تمتلئ تدريجياً بخرائط إعادة توجيه،
وفيما يلي المبادئ الأساسية:
ليس مثالياً، بعض الروابط ذهبت إلى الأبد، والبحث الدلالي لا يجد البديل الصحيح دائماً، لكنّه منظر أفضل من ترك آلاف الروابط المكسورة
هذا الأسبوع (w/c 24th تشرين الثاني/نوفمبر 2025) سوف أقوم بنشر سلسلة البحث الدلالي التي تدخل في تفاصيل أكثر بكثير حول كيفية عمل البحث القائم على المتجه تحت الغطاء. هذه السلسلة سوف تغطي توليد التضمين، تخزين متجه Qdrant، وكيف يرتبط كل ذلك في نظام المناولة هذا 404. إذا كنت فضولياً عن كيفية ISemanticSearchService.SearchAsync في الواقع تجد محتوى "مشابه"، حيث سترغب في النظر.
المصدر الكامل موجود في المستودع إذا كنت تريد أن تسرق أياً منه لمشاريعك الخاصة.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.