# ساده‌سازی 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`: ```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` در پاسخ هست): ```js .filter((c) => !scope.isRoot || !findDomainByCityId(extractEntityCityId(c))) ``` ## وظایف ### ۱. حذف ۳۵ sweep و یکسان‌سازی با الگوی کلینیک وقتی `city` در پاسخ لیست پزشکان آمد: - `getDoctorsForScope` و `CITY_IDS_WITH_DOMAIN` حذف شوند. - روی دامنهٔ شهری: همان `fetchAllPages('/api/v1/doctors', cityFilterParams(scope))` بماند. - روی دامنهٔ ریشه: یک fetch کامل + همان فیلتر کلینیک‌ها: ```js 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 را مقایسه کن: ```bash curl -s https://yasuj-nobat.ir/sitemap.xml | grep -c "" curl -s https://nobat724.com/sitemap.xml | grep -c "" ``` مقدار مرجع در زمان نگارش: یاسوج **۵۵۴** URL (۴۵۴ پزشک)، ریشه **۱۰۰** URL (۰ پزشک — همهٔ پزشکان شهر دامنه‌دار دارند). ### ۲. آستانهٔ sitemap-index (مستقل از backend) الان همهٔ URLها در یک `sitemap.xml` هستند. سقف استاندارد **۵۰٬۰۰۰ URL** در هر فایل است. مقیاس فعلی: ۲۳۴۱ پزشک، ۲ کلینیک، ۹۳ صفحهٔ تخصص، ۰ بلاگ ⇒ حداکثر ~۲٬۵۰۰ URL در هر دامنه. **بسیار زیر سقف، پس sitemap-index الان لازم نیست** و عمداً پیاده نشده. کاری که این وظیفه می‌خواهد: - یک هشدار صریح اضافه کن که وقتی تعداد URL از یک آستانه (مثلاً ۴۵٬۰۰۰) گذشت، در لاگ دیده شود: ```js 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`) — این عمدی و درست است: ```js 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 .`