# فعال‌سازی بلاگ شهر-محور (بعد از افزودن شهر در 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 && (

مخصوص شهر:

{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 برمی‌گردد.