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>
125 lines
7.0 KiB
Markdown
125 lines
7.0 KiB
Markdown
<div dir="rtl" markdown="1">
|
|
|
|
# سادهسازی 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 "<loc>"
|
|
curl -s https://nobat724.com/sitemap.xml | grep -c "<loc>"
|
|
```
|
|
|
|
مقدار مرجع در زمان نگارش: یاسوج **۵۵۴** 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 .`
|
|
|
|
</div>
|