P1-P11 of the live SEO audit are implemented and verified against a local production build, so the audit file becomes a reference rather than a task list: it now records what shipped, the root causes that differed from the original hypotheses, and the deliberate trade-offs. Remaining work is split into smaller prompts, ordered by dependency: - seo-post-deploy-verification: the acceptance criteria were "curl on production" but were only run against a local build - blog-city-scoping-activate: blocked on the backend blog city column - sitemap-simplify-with-city: drops the 35-sweep workaround once the doctors list exposes city Each names its blocking dependency and carries reference numbers so a regression is visible. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.0 KiB
سادهسازی sitemap با شهرِ موجود در پاسخ + آستانهٔ sitemap-index
پروژه
nobat724_front
پیشنیاز قطعی: clinicpro/.claude/prompt/doctors-list-city-and-limit.md اجرا و deploy شده باشد.
تا وقتی GET /api/v1/doctors فیلد city برنگرداند، وظیفهٔ ۱ قابل انجام نیست (وظیفهٔ ۲ مستقل است).
زمینه
sitemap هر دامنه فقط باید موجودیتهایی را داشته باشد که canonical آنها همان دامنه است. برای کلینیکها ساده است چون پاسخ لیست city دارد. برای پزشکان پاسخ لیست شهر ندارد، پس sitemap دامنهٔ اصلی مجبور است این کار پرهزینه را بکند: کل لیست را بگیرد، بعد ۳۵ بار لیست را با city_id هر شهرِ دامنهدار بگیرد، و تفاضل بگیرد.
نتیجه: تولید sitemap دامنهٔ اصلی ~۱۳ ثانیه (revalidate ساعتی، پس هزینه مستهلک میشود، ولی کد پیچیده و شکننده است).
وضعیت فعلی
app/sitemap.js:
// شهرهایی که دامنهٔ اختصاصی دارند — موجودیتهایشان canonical روی همان دامنه دارند و
// نباید در sitemap دامنهٔ اصلی تکرار شوند.
const CITY_IDS_WITH_DOMAIN = citiesData
.filter((c) => !isRootCity(c) && c.domain)
.map((c) => c.id);
/**
* پاسخ لیست پزشکان شهر ندارد؛ پس تفکیک با فیلتر سمت API انجام میشود:
* روی دامنهٔ شهری با city_id، و روی دامنهٔ اصلی «همه منهای پزشکانِ شهرهای دامنهدار».
*/
async function getDoctorsForScope(scope) {
if (!scope.isRoot) return fetchAllPages('/api/v1/doctors', cityFilterParams(scope));
const [all, ...perCity] = await Promise.all([
fetchAllPages('/api/v1/doctors'),
...CITY_IDS_WITH_DOMAIN.map((cityId) =>
fetchAllPages('/api/v1/doctors', { city_id: String(cityId) })
),
]);
const ownedByCityDomain = new Set(perCity.flat().map((d) => d?.uuid).filter(Boolean));
return all.filter((d) => !ownedByCityDomain.has(d?.uuid));
}
کلینیکها از قبل الگوی ساده و درست را دارند (چون city در پاسخ هست):
.filter((c) => !scope.isRoot || !findDomainByCityId(extractEntityCityId(c)))
وظایف
۱. حذف ۳۵ sweep و یکسانسازی با الگوی کلینیک
وقتی city در پاسخ لیست پزشکان آمد:
getDoctorsForScopeوCITY_IDS_WITH_DOMAINحذف شوند.- روی دامنهٔ شهری: همان
fetchAllPages('/api/v1/doctors', cityFilterParams(scope))بماند. - روی دامنهٔ ریشه: یک fetch کامل + همان فیلتر کلینیکها:
async function getDoctorUrls(baseUrl, scope) {
const doctors = await fetchAllPages('/api/v1/doctors', cityFilterParams(scope));
return doctors
.filter((d) => !isThinDoctor(d))
.filter((d) => !scope.isRoot || !findDomainByCityId(extractEntityCityId(d)))
.map((d) => withLastModified({ /* ... */ }, d.updated || d.created));
}
extractEntityCityId بدون تغییر کار میکند (شکل city: {id} را میپذیرد).
تأیید کن پوشش تغییر نکرده باشد — قبل و بعد از تغییر تعداد URL هر sitemap را مقایسه کن:
curl -s https://yasuj-nobat.ir/sitemap.xml | grep -c "<loc>"
curl -s https://nobat724.com/sitemap.xml | grep -c "<loc>"
مقدار مرجع در زمان نگارش: یاسوج ۵۵۴ URL (۴۵۴ پزشک)، ریشه ۱۰۰ URL (۰ پزشک — همهٔ پزشکان شهر دامنهدار دارند).
۲. آستانهٔ sitemap-index (مستقل از backend)
الان همهٔ URLها در یک sitemap.xml هستند. سقف استاندارد ۵۰٬۰۰۰ URL در هر فایل است.
مقیاس فعلی: ۲۳۴۱ پزشک، ۲ کلینیک، ۹۳ صفحهٔ تخصص، ۰ بلاگ ⇒ حداکثر ~۲٬۵۰۰ URL در هر دامنه. بسیار زیر سقف، پس sitemap-index الان لازم نیست و عمداً پیاده نشده.
کاری که این وظیفه میخواهد:
- یک هشدار صریح اضافه کن که وقتی تعداد URL از یک آستانه (مثلاً ۴۵٬۰۰۰) گذشت، در لاگ دیده شود:
if (allUrls.length > 45000) {
console.warn(`[sitemap] ${domain}: ${allUrls.length} URLs — نزدیک سقف ۵۰k، sitemap-index لازم است`);
}
- تصمیم «فعلاً sitemap-index نداریم و چرا» را بهصورت کامنت کنار همین شرط مستند کن، تا دفعهٔ بعد کسی آن را بهعنوان قلمافتادگی برندارد.
- sitemap-index را پیاده نکن مگر عدد واقعی به آستانه نزدیک شده باشد؛ با
generateSitemapsساختار URL عوض میشود (/sitemap/[id].xml) و ارجاعrobots.txtباید همراهش تغییر کند.
۳. بازبینی سقف limit بعد از تغییر backend
حلقهٔ صفحهبندی الان روی meta.totalPages توقف میکند (نه روی items.length < limit) — این عمدی و درست است:
const PAGE_LIMIT = 50;
const MAX_PAGES = 200;
// ...
if (totalPages && page >= totalPages) break;
if (totalRecords && results.length >= totalRecords) break;
اگر backend سقف limit را بالا برد، PAGE_LIMIT را متناسب بالا ببر و MAX_PAGES را بازبینی کن. شرط توقف را به items.length < PAGE_LIMIT برنگردان — دقیقاً همین باگ باعث شده بود sitemap روی ۵۰ رکورد بریده بماند.
نکات مهم
- این تغییر نباید هیچ URLی را از sitemap حذف یا اضافه کند — فقط راه رسیدن به همان نتیجه سادهتر میشود. اختلاف در تعداد URL یعنی رگرسیون؛ ریشهیابی کن.
isThinDoctorدرlib/entityQuality.jsروی شکل پاسخِ لیست کار میکند (specialtiesدارد،addressندارد). اگر backend شکل پاسخ لیست را عوض کرد، معیارhasLocationرا بازبینی کن — با آمدنcityدر پاسخ، حالاhasLocationمیتواند ازcityهم سیگنال بگیرد و پزشکان بیشتری وارد sitemap شوند.- منطق «چه چیزی noindex است» و «چه چیزی در sitemap است» باید یکی بماند؛ هر دو از
lib/entityQuality.jsمیآیند. اگر یکی را عوض کردی، دیگری خودکار همراه میشود — عمداً همینطور طراحی شده. - بعد از تغییر:
graphify update .