# الحرب على ٤٠٤ : جعل الاشياء القديمة تعمل مجددا

<!--category-- ASP.NET, C# -->
<datetime class="hidden">2025-11-23T09:00</datetime>

## أولاً

عندما كنت تدوّن منذ عام 2004 (نعم، حقاً، أنت تتراكم الكثير من ديتريتوس الرقمي، قمت مؤخراً باستيراد مواقعي القديمة من 2004-2009 (https://www.mostalyluccid.net/blog/ stem/ Imported) واكتشفت ذلك تقريباً *" كل شيء "* الروابط الخارجية تشير إلى المواقع التي اختفت منذ عقد مضى، مخططات المسار القديمة التي لم تعد مطابقة للبنية الحالية، كل شيء.

المشكلة تنقسم إلى ثلاثة أجزاء:

1. **عوامل داخلية** - تثبيتها أثناء عملية الاستيراد نفسها باستخدام [أمر مستورد الأداة](https://github.com/scottgal/mostlylucid.nugetpackages/tree/main/Mostlylucid.ArchiveOrg)قام مكتبي القديم بمرجعية بعضهم البعض باستخدام مخطط المسار القديم لذا أعدت كتابتها كجزء من الهجرة

2. **الوصلات الخارجية (الصادرة)** - هذا هو الكبير. وصلات إلى الموارد الخارجية التي اختفت منذ ذلك الحين أو انتقلت أو أصبحت مواقع مختلفة تماما. وصلة إلى بعض الوثائق من عام 2006؟ ذهب. إشارة إلى تدوينة من قبل شخص ما الذي استغرق وقتا طويلا منذ أخذ موقعه إلى أسفل؟ ميت. هذا يحتاج إلى مناولة وقت التشغيل.

3. **الطلبات والطلبات الواردة الواردة في الطلبات الواردة** - الناس (ومحركات البحث) لا تزال تحاول الوصول إلى العناوين القديمة مثل `/archive/2006/05/15/123.aspx`نظام البحث الدلالي غالباً ما يستطيع معرفة ما كانوا يسعون خلفه حتى بدون تطابق دقيق

الآن، أنا *يمكن* الـ أرشيف. org ابحث في عملية الاستيراد. لكن هنا الشيء: الروابط كسر مع الوقت. الموقع الذي يعمل اليوم قد يكون قد ذهب في الشهر القادم. من خلال التعامل مع هذا في وقت التشغيل مع إعادة فحص دورية ، النظام تلقائياً يلتقط. *في المستقبل في المستقبل* الكسور، وليس فقط تلك التي كانت موجودة في وقت الاستيراد.

هذه المادة تغطي نهجي:

- **الوصلات الصادرة**: `BrokenLinkArchiveMiddleware` - استبدال الوصلات الخارجية الميتة مع أرشيف. org
- **الوصلات المدخلة**التعامل مع 404 مع البحث الدلالي - يجد المحتوى الصحيح حتى من المخططات القديمة
- **نظام التعلم**: كيف يصبح الموقع أكثر ذكاء مع مرور الوقت من نقرات المستخدم
- **الموعــد الأساسـي**: التحقق من الوصلات دون طلبات التثبيت

[TOC]

## المشكلة مع المحتوى القديم

هذا هو الشيء عن الإنترنت: هو ليس دائم. تلك التدوينة الممتازة التي ارتبطت بها في عام 2006؟ ذهب. ذلك الموقع الوثائقي؟ أعد تنظيمه ثلاث مرات. مخطط المسار الخاص بك من قبل أن تستقر في مؤتمر مناسب؟ التساهل.

```mermaid
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
```

النهج الساذج هو إصلاح الروابط يدوياً. ولكن عندما يكون لديك مئات الوظائف مع الآلاف من الوصلات، فإن ذلك لا يعمل. نحن بحاجة إلى التشغيل الآلي.

## أولاً - لمحة عامة

ويتألف النظام من عنصرين رئيسيين يعملان جنبا إلى جنب:

```mermaid
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: معالجة الوصلات الصادرة

### المُكَكينة Archive Madadwater

هذا البرمجيات الوسيطة تتصدّى لـ HTML وتقوم بثلاثة أشياء:

1. مقتطفات جميع الوصلات لأغراض التحقق من المعلومات الخلفية
2. يُستعاض عن الوصلات الخارجية المفككة المعروفة مع النسخ المحفوظة.org
3. استخدام ابحث ابحث ابحث إلى ابحث استبدال لـ لـ داخلي

الفكرة الرئيسية هنا هي أننا نريد أن نجد أرشيف. org لقطات من *في الوقت الذي كُتِب فيه البريد*لقطة من عام 2024 لمقالة عام 2006 قد تشير إلى محتوى مختلف تماماً، لذا نراجع تاريخ نشر التدوينة ونسأل الأرشيف. org عن أقرب لقطة.

ها هو الهيكل الأساسي:

```csharp
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]` تتيح لنا الشبكة جيلاً تجمّعياً في الوقت المناسب، يكون أسرع وخالياً من التخصيص على حد سواء:

```csharp
[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://html-agility-pack.net/) / https://github.com/AngleSharp/AngleSharp أو ما شابه HTML (وغير ذلك) parsers هنا لسيناريوهات أكثر تعقيداً ولكنها كانت مبالغة لهذا الغرض - نحن فقط بحاجة لاستخراج hrefs بسرعة.

### تسجيل روابط تدقيق المعلومات الخلفية

لا نريد أن نحجب الاستجابة أثناء التحقق من الروابط. بدلاً من ذلك، نطلق مهمة خلفية:

```csharp
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 بديل (من فحص خلفية سابق)، نتبادلها مع أداة مفيدة:

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

بالنسبة للوصلات الداخلية المكسورة، نجرب البحث الدلالي أولاً:

```csharp
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: خلفية الخلفية

البرمجيات الوسيطة لا تتحقق من الروابط خلال الطلب - ذلك سيكون بطيئاً جداً جداً. بدلاً من ذلك، فإنها تقوم بفرز الروابط المكتشفة لمعالجة الخلفية عن طريق جدول قاعدة بيانات، وa `BrokenLinkCheckerBackgroundService` يُعالجُ الفحصَ الفعليَ.

الخدمة تعمل بالساعة وتفعل شيئان:

1. **صحة** - يطلب المقر الرئيسي طلب معرفة ما إذا كانت الروابط لا تزال على قيد الحياة
2. **or org** - للوصلات المكسورة، العثور على أقرب لقطة تاريخية

نحن مواطنون جيدون حول هذا - المدقق يستخدم عامل مخصص المستخدم الذي يحدد نفسه ويربطه بالموقع:

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

هذا يسمح لمداري الموقع برؤية ما الذي يضرب خادمهم، ويمكنهم أن يبحثوا عنا إذا كانوا فضوليين. نحن أيضاً نخنق الطلبات (2 ثانية بين الشيكات) لتجنب ضرب خادم أي شخص.

وفيما يلي، فإن الروابط هي: **التي أعيد التحقق منها**الوصلة التي كانت تعمل الأسبوع الماضي قد تكون ميتة اليوم. الخدمة تلتقط الوصلات التي لم يتم التحقق منها في الـ 24 ساعة الماضية وتتحقق منها مرة أخرى. على أية حال، بمجرد أن نجد أرشيف. org بديل لوصلة مكسورة،

```mermaid
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
```

### الأرشيف.org CDX API

Arche.org يوفر ADX (مؤشر) API الذي يسمح لنا بالسؤال عن اللقطات. الجزء الذكي هو الترشيح حسب التاريخ:

```csharp
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، وليس نسخة حديثة قد تكون مختلفة تماماً.

### 

نتتبع الروابط مع الكيان المناسب:

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

### مراجعة دورية

المفتاح إلى اصطياد المستقبل هو إعادة التدقيق. الخدمة إستفسارات لـ روابط ليس مؤكّد مؤخرًا:

```csharp
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` دعونا نكون متسامحين قليلاً - نحن لا نضع علامة على رابط كما كسر بعد خطأ عابر واحد.

## الجزء 3: معالجة الطلبات الواردة

الآن بالنسبة للجانب الآخر من العملة: الناس (ومحركات البحث) الذين يطلبون URLات غير موجودة. وهذا يشمل:

- **المخطط** - مدونتي القديمة تستخدم مسارات مثل `/archive/2006/05/15/123.aspx`- لا يزال بعضها مفهرساً أو معلماً أو مرتبطاً بمواقع أخرى.
- **يد** -شخص ما يلصق الدهون بالعنوان أو ينسخه بشكل غير صحيح
- **جزئيات** - محركات البحث في بعض الأحيان مؤشر الشظايا الغريبة.

نظام البحث الدلالي مفيد بشكل خاص هنا. حتى بدون وجود تطابق كشطي مباشر، يمكننا استخراج مصطلحات ذات معنى من العنوان المطلوب والعثور على المحتوى الذي يتطابق مع الدلالية. يعرف النظام كلاً من الركيزة ويدمج البيانات لكل عمود، حتى يتمكن من العثور على مباريات "اغلق بما فيه الكفاية" حتى لبنيات العنوان المختلفة تماماً.

### لماذا ليس من "أوسط" ؟

كانت غريزتي الاولى ان اتعامل مع اعادة التوجيه في البرامج المتوسطة - اعتراض الطلبات في وقت مبكر ، التحقق من عمليات اعادة التوجيه المعروفة ، وإعادة التوجيه قبل ان يبدأ التوجيه حتى . *يمكن* العمل، وهنا كيف سيبدو شكله:

```csharp
// 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، نعرض عليهم اقتراحات باستخدام مضاهاة الأوتار الغامضة و (أولاً) البحث الدلالي. إذا ضغطوا على إقتراح، نسجل ذلك. بعد النقرات الكافية بثقة كافية، نبدأ بإعادة توجيه ذاتي.

```mermaid
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 40

كلّ إعادة توجيه كلّ المُجال الحيّات في `ErrorController`SP. SP. net `UseStatusCodePagesWithReExecute` واعادة توجيه الطلب من خلال مُعالج الخطأ عندما يحدث 404:

```csharp
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` طرق معالجة كل من عمليات إعادة التوجيه المدروسة والمباريات ذات الثقة العالية لأول مرة:

```csharp
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 (المسافة المدية) بالإضافة إلى بعض أدوات التهوية:

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

### التعلم من سلوك المستخدم

عندما ينقر مستخدم على اقتراح ما، نسجله ونحدث درجات الثقة:

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

ويراعي حساب درجة الثقة النقرات والانطباعات على السواء:

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

### 2Tir 2Tiir تلقائي- إعادة توجيه

لدينا مستويين من إعادة التوجيه التلقائي:

1. **درجة عالية من الثقة في الدرجة الأولى لأول درجة )٢٠٣(**إذا كانت درجة التشابه > = 0.85، وهناك فجوة كبيرة إلى ثاني أفضل مباراة، فإننا نعيد توجيهها فورا مع 302. هذا يلتقط خطأ واضح.

2. **)٣٠١(**بعد 5+ نقرات مع 70% + ثقة، نستخدم إعادة توجيه دائمة 301. هذه حالة "البشر قد تكلموا".

```csharp
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 للمعالجة يقوم فقط بتشغيل هذا المنطق عندما نكون قد حددنا مسبقاً أن الصفحة غير موجودة -- أكثر كفاءة بكثير.

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

والتدفق يبدو هكذا:

```mermaid
flowchart LR
    subgraph Request["Request Processing"]
        direction TB
        A[Request] --> B[Static Files]
        B --> C[Routing]
        C --> D{Page exists?}
        D -->|Yes| E[MVC/Endpoints]
        D -->|No| F[ErrorController 404]
        F --> G{Auto-redirect?}
        G -->|Yes| H[Redirect]
        G -->|No| I[Show suggestions]
    end

    subgraph Response["Response Processing"]
        direction TB
        E --> J[BrokenLinkArchiveMiddleware]
        J --> K[Link Extraction]
        K --> L[Link Replacement]
        L --> M[Response]
    end

    subgraph Background["Background Processing"]
        direction TB
        N[BrokenLinkCheckerService] --> O[Check URLs]
        O --> P[Fetch Archive.org]
        P --> Q[Update Database]
    end

    K -.->|Register links| N

    style F stroke:#ef4444,stroke-width:3px
    style J stroke:#8b5cf6,stroke-width:3px
    style N stroke:#f59e0b,stroke-width:3px
```

## ثالثاً - استنتاج

هذا النظام يعمل منذ فترة الآن، وهو بالأحرى مرضٍ لمشاهدته وهو يتعلم. قاعدة البيانات تمتلئ تدريجياً بخرائط إعادة توجيه،

وفيما يلي المبادئ الأساسية:

- **لا يَتوقّفُ الطلباتَ** - تجهيز المعلومات الأساسية للعمليات المكلفة
- **التعلم من المستعملين** -نقراتهم هي إشارات قيّمة
- **كن متحفظاً مع توجيهات تلقائية** - إعادة التوجيه فقط عندما تكون واثقاً
- **حفظ الوقت المحدد** - اعثر على أرشيف. org معاينة من وقت كتابة المحتوى
- **** - إذا لم تكن الخدمات متاحة، فقط الاستمرار بشكل عادي

ليس مثالياً، بعض الروابط ذهبت إلى الأبد، والبحث الدلالي لا يجد البديل الصحيح دائماً، لكنّه منظر أفضل من ترك آلاف الروابط المكسورة

## قريبًا : البحث الدال في العمق

هذا الأسبوع (w/c 24th تشرين الثاني/نوفمبر 2025) سوف أقوم بنشر سلسلة البحث الدلالي التي تدخل في تفاصيل أكثر بكثير حول كيفية عمل البحث القائم على المتجه تحت الغطاء. هذه السلسلة سوف تغطي توليد التضمين، تخزين متجه Qdrant، وكيف يرتبط كل ذلك في نظام المناولة هذا 404. إذا كنت فضولياً عن كيفية `ISemanticSearchService.SearchAsync` في الواقع تجد محتوى "مشابه"، حيث سترغب في النظر.

المصدر الكامل موجود في المستودع إذا كنت تريد أن تسرق أياً منه لمشاريعك الخاصة.