# فعالسازی بلاگ شهر-محور (بعد از افزودن شهر در backend)
## پروژه
`nobat724_front`
**پیشنیاز قطعی:** `clinicpro/.claude/prompt/blog-city-field.md` اجرا و deploy شده باشد.
تا وقتی `GET /api/v1/blogs` فیلد شهر برنگرداند، این پرامپت کاری برای انجام ندارد.
## زمینه
منطق شهر-محورِ بلاگ سمت فرانت **از قبل نوشته شده** ولی چون دادهاش وجود ندارد غیرفعال است. این پرامپت آن را فعال، تکمیل و تست میکند.
وضعیت داده هنگام نگارش: `GET /api/v1/blogs` صفر رکورد دارد (`totalRecords: 0`) و هیچ فیلد شهری ندارد.
## وضعیت فعلی (کدی که آمادهٔ فعالشدن است)
`app/blog/[slug]/page.js` — canonical و شهر از همان هلپر مشترک:
```js
const blogCityId = extractEntityCityId(blog);
const blogCity = findCityById(blogCityId);
const origin = await getEntityOrigin(blogCityId);
return {
title,
description,
alternates: { canonical: `${origin}/blog/${slug}` },
// ...
};
```
JSON-LD مقاله فقط با وجود شهر `spatialCoverage` میگیرد:
```js
...(blogCity && {
spatialCoverage: { "@type": "Place", name: blogCity.name },
}),
```
`components/blog/head/index.js` — برچسب بصری، فقط با `cityName`:
```jsx
{cityName && (
)}
```
`lib/domainHelpers.js` — استخراج شهر، هر سه شکل پاسخ را میپذیرد:
```js
export function extractEntityCityId(entity) {
if (!entity) return null;
const candidates = [
entity.city_id,
...(Array.isArray(entity.city) ? entity.city.map((c) => c?.id) : [entity.city?.id]),
...(Array.isArray(entity.address) ? entity.address.map((a) => a?.city?.id) : []),
].filter((id) => id != null);
const withDomain = candidates.find((id) => findDomainByCityId(id));
return withDomain ?? candidates[0] ?? null;
}
```
`app/sitemap.js` — مسیریابی پست به sitemap دامنهٔ canonical خودش:
```js
async function getBlogUrls(baseUrl, scope, currentDomain) {
const blogs = await fetchAllPages('/api/v1/blogs');
return blogs
.filter((b) => b?.slug || b?.uuid)
.filter((b) => {
const cityDomain = findDomainByCityId(extractEntityCityId(b));
return cityDomain ? cityDomain === currentDomain : scope.isRoot;
})
// ...
}
```
## وظایف
### ۱. تأیید سازگاری شکل پاسخ
اول پاسخ واقعی را ببین:
```bash
curl -s "https://clinic-pro.ir/api/v1/blogs?page=1&limit=5" | jq '.data.data[0]'
```
بررسی کن `extractEntityCityId` روی شکل واقعی جواب میدهد. اگر backend شکل دیگری داد (مثلاً `city` رشته بهجای آبجکت)، `extractEntityCityId` را گسترش بده — **نه** یک استخراج جدا برای بلاگ بنویس.
`normalizeBlog` در `helper/index.js` نباید فیلد شهر را حذف کند؛ اگر فیلدها را صریح map میکند، `city` را اضافه کن.
### ۲. لیست بلاگ per-domain
`app/blogs/page.js` روی هر دامنهٔ شهری باید پستهای آن شهر + سراسری را بگیرد. با پارامتر `city_id` که backend اضافه میکند:
- روی دامنهٔ شهری: `city_id` شهر همان دامنه (از `getStateInfo`)
- روی دامنهٔ ریشه (`isRoot`): بدون پارامتر — همهٔ پستها
canonical لیست بلاگ **self روی همان دامنه** بماند (C1-a). این از قبل درست است — کانونیکال hardcode شده به دامنهٔ اصلی قبلاً حذف شده:
```js
// C1-a — لیست بلاگ per-domain است (پستهای همان شهر + سراسری)؛
// canonical لایهٔ layout (self روی همان دامنه) درست است.
```
### ۳. عنوان و متادیتای شهرمحور
`generateMetadata` صفحهٔ پست: اگر پست شهر دارد، نام شهر در `title`/`description` بیاید. اگر سراسری است، هیچ نام شهری اضافه نشود (نه نام شهرِ دامنهٔ سروکننده).
### ۴. تست
با دادهٔ واقعی (حداقل یک پست شهری + یک پست سراسری) تأیید کن:
```bash
S=
G=
# پست شهری: canonical به دامنهٔ شهر، روی هر دامنهای
curl -s "https://yasuj-nobat.ir/blog/$S" | grep -o 'rel="canonical" href="[^"]*"' # → yasuj-nobat.ir
curl -s "https://nobat724.com/blog/$S" | grep -o 'rel="canonical" href="[^"]*"' # → yasuj-nobat.ir
curl -s "https://yasuj-nobat.ir/blog/$S" | grep -o "یاسوج" # نام شهر در HTML قابلمشاهده
curl -s "https://yasuj-nobat.ir/blog/$S" | grep -o 'spatialCoverage'
# پست سراسری: self-canonical، بدون نام شهر
curl -s "https://nobat724.com/blog/$G" | grep -o 'rel="canonical" href="[^"]*"' # → nobat724.com
curl -s "https://yasuj-nobat.ir/blog/$G" | grep -c 'spatialCoverage' # → 0
# sitemap: هر پست فقط در دامنهٔ canonical خودش
curl -s https://yasuj-nobat.ir/sitemap.xml | grep -c "$S" # → 1
curl -s https://nobat724.com/sitemap.xml | grep -c "$S" # → 0
curl -s https://nobat724.com/sitemap.xml | grep -c "$G" # → 1
curl -s https://nobat724.com/sitemap.xml | grep -c "/blog/" # → بیشتر از ۰
```
تست واحد برای `extractEntityCityId` روی شکل واقعی پاسخ بلاگ به `lib/domainHelpers.test.js` اضافه کن.
## نکات مهم
- **پست سراسری حالت دائمی است، نه استثنا.** پستی بدون شهر باید روی دامنهٔ اصلی self-canonical شود و در همهٔ لیستها دیده شود. هرگز شهرِ دامنهٔ سروکننده را به پست بدون شهر نسبت نده.
- `findCityById` رکورد ریشه (`id: 600` = `nobat724.com`) را شهر حساب نمیکند و `null` برمیگرداند — یعنی پستی که بهاشتباه به رکورد ریشه وصل شده، سراسری تلقی میشود. رفتار درست است.
- برچسب بصری شهر با تایپوگرافی «نویسنده/تاریخ» همخانواده است؛ عنصر بصری ناهماهنگ اضافه نکن.
- الگوی Soft-404 (`app/blog/[slug]/page.js`) بعد از تغییرات همچنان باید کار کند:
`curl -o /dev/null -w "%{http_code}" https://nobat724.com/blog/invalid-xxx` → `404`.
توجه: `app/blog/[slug]/loading.js` عمداً حذف شده — دوباره اضافهاش نکن، وگرنه Soft-404 برمیگردد.