Reprocess: SEO-safe applicants-only default (don't churn indexed shift/job URLs)
CI/CD / CI · dotnet build (push) Successful in 2m11s
CI/CD / Deploy · hamkadr (push) Successful in 2m10s

Reprocess deletes+rebuilds aggregated listings, which changes their IDs. Shift/Job
detail pages are indexed and in the sitemap, so churning them would 404 ranked
URLs. «آماده به کار» pages are NoIndex + Disallow, so rebuilding them has zero SEO
impact — and that's where all the duplicate/sprawl problems were.

ReprocessAsync(talentOnly: true) now only deletes/rebuilds TalentListings and
skips non-talent raws (leaving shift/job listings + their RawListing links
untouched). Admin button relabelled «پردازش مجددِ آماده به کارها (امن برای SEO)».
Shifts/jobs self-clean via normal ingestion turnover.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-20 16:08:20 +03:30
parent e582597b20
commit fb7bfad9ce
3 changed files with 25 additions and 10 deletions
+3 -3
View File
@@ -49,13 +49,13 @@
کش حذف تکراری و آگهی‌های جمع‌آوری‌شده پاک و از نو با AI پردازش می‌شوند. (آگهی‌های مراکز حذف نمی‌شوند.)
</p>
<form method="post" onsubmit="return confirm('آگهی‌های منتشرشده از جمع‌آوری حذف و از روی متنِ خامِ ذخیره‌شده (بدون واکشی مجدد) دوباره با هوش مصنوعی پردازش می‌شوند — برای پاک‌سازی داده‌های موجود (حذف موارد تکراری، اصلاح نقش/گروه/تگ). هیچ آیتمی از دست نمی‌رود. در پس‌زمینه اجرا می‌شود. ادامه؟');">
<form method="post" onsubmit="return confirm('آگهی‌های «آماده به کار» از روی متنِ خامِ ذخیره‌شده (بدون واکشی) دوباره با هوش مصنوعی پردازش می‌شوند — برای پاک‌سازی (حذف موارد تکراری، اصلاح نقش/گروه/تگ، افزودن موقعیت تقریبی). شیفت/استخدام دست‌نخورده می‌مانند (برای حفظ SEO). هیچ آیتمی از دست نمی‌رود. در پس‌زمینه اجرا می‌شود. ادامه؟');">
<button type="submit" asp-page-handler="ReprocessStored" class="btn btn-primary btn-block" style="margin-top:10px;">
🧹 پردازش مجددِ آیتم‌های ذخیره‌شده (بدون واکشی)
🧹 پردازش مجددِ «آماده به کار»‌ها (امن برای SEO)
</button>
</form>
<p class="muted" style="font-size:11px; margin:6px 0 0;">
توصیه‌شده برای پاک‌سازیِ داده‌های فعلی: متنِ خام نگه داشته می‌شود و فقط آگهی‌ها با منطقِ جدید (یک‌نفر=یک‌آگهی، نقش پایه، گروه ثابت، تگ تمیز) بازساخته می‌شوند.
توصیه‌شده برای پاک‌سازیِ آماده‌به‌کارها: متنِ خام نگه داشته می‌شود و فقط با منطقِ جدید (یک‌نفر=یک‌آگهی، نقش پایه، گروه ثابت، تگ تمیز، موقعیت تقریبی) بازساخته می‌شوند. صفحاتِ «آماده به کار» ایندکس نمی‌شوند، پس آدرسِ ایندکس‌شده‌ای تغییر نمی‌کند؛ شیفت/استخدام به‌مرور با ایمیجستِ تازه پاک می‌شوند.
</p>
<hr style="border:none; border-top:1px solid var(--line); margin:16px 0;" />
@@ -111,7 +111,9 @@ public class IndexModel : PageModel
using var scope = _scopes.CreateScope();
var svc = scope.ServiceProvider.GetRequiredService<IngestionService>();
var log = scope.ServiceProvider.GetRequiredService<ILogger<IndexModel>>();
try { await svc.ReprocessAsync(); }
// talentOnly: «آماده به کار» is NoIndex/Disallow → rebuilding it doesn't churn any indexed
// URL. Shift/Job detail pages ARE indexed, so they're left to self-clean via turnover.
try { await svc.ReprocessAsync(talentOnly: true); }
catch (Exception ex) { log.LogError(ex, "Background reprocess failed"); }
});
IngestMessage = "پردازش مجدد آیتم‌های ذخیره‌شده در پس‌زمینه آغاز شد. نتیجه پس از اتمام در «تاریخچهٔ اجرا» نمایش داده می‌شود (بسته به تعداد آیتم‌ها و سرعت هوش مصنوعی، چند دقیقه طول می‌کشد).";
@@ -176,7 +176,11 @@ public class IngestionService
/// Deletes the old aggregated posts, then republishes from the stored raw text. Long-running
/// (one AI call per item) — call it on a background scope, not inside a request.
/// </summary>
public async Task<IngestionSummary> ReprocessAsync(CancellationToken ct = default)
/// <param name="talentOnly">SEO-safe default: only «آماده به کار» (which is NoIndex/Disallow) is
/// deleted &amp; rebuilt, so no INDEXED url changes. Shift/Job detail pages are indexed + in the
/// sitemap, so churning their IDs would 404 ranked pages — instead they self-clean via turnover.
/// Pass false only when you accept that SEO hit.</param>
public async Task<IngestionSummary> ReprocessAsync(bool talentOnly = true, CancellationToken ct = default)
{
var settings = await _settings.GetAsync();
var roles = await _db.Roles.ToListAsync(ct);
@@ -189,19 +193,28 @@ public class IngestionService
// Drop previously-published aggregated content; it's regenerated below from the raw text.
// DB cascade clears their ContactMethods/Applications/InterestEvents; RawListing back-refs SetNull.
await _db.Shifts.Where(s => s.Source == ShiftSource.Aggregated).ExecuteDeleteAsync(ct);
await _db.JobOpenings.Where(j => j.Source == ShiftSource.Aggregated).ExecuteDeleteAsync(ct);
await _db.TalentListings.Where(t => t.Source == ShiftSource.Aggregated).ExecuteDeleteAsync(ct);
if (!talentOnly)
{
await _db.Shifts.Where(s => s.Source == ShiftSource.Aggregated).ExecuteDeleteAsync(ct);
await _db.JobOpenings.Where(j => j.Source == ShiftSource.Aggregated).ExecuteDeleteAsync(ct);
}
int fetched = 0, queued = 0, published = 0, flagged = 0, spam = 0;
var raws = await _db.RawListings.OrderBy(r => r.Id).ToListAsync(ct);
foreach (var raw in raws)
{
ct.ThrowIfCancellationRequested();
fetched++;
raw.LinkedShiftId = null; raw.LinkedTalentId = null; // old links were just deleted
var parsed = _parser.Parse(raw.RawText, roleNames, cityNames, districtNames);
// SEO-safe scope: in talent-only mode, leave indexed shift/job listings (and their
// RawListing links/status) completely untouched — only applicants are rebuilt.
if (talentOnly && parsed.Kind != ListingKind.Talent) continue;
fetched++;
raw.LinkedTalentId = null; // talent rows were just deleted
if (!talentOnly) raw.LinkedShiftId = null;
var val = _validator.Validate(raw.RawText, parsed);
// Stale-applicant filter — age from the Persian "time ago" phrase in the text (Divar).