Files
nobat724_front/.claude/prompt/blog-city-scoping-activate.md
hamedandClaude Opus 4.8 21f5f07bc6 chore(prompt): record SEO audit and split remaining work
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>
2026-07-19 08:09:21 +03:30

157 lines
7.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<div dir="rtl" markdown="1">
# فعال‌سازی بلاگ شهر-محور (بعد از افزودن شهر در 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 && (
<div className="flex items-center justify-start gap-1">
<p className="text-[#9B9B9B] text-[11px] md:text-[13px] lg:text-[14px] font-normal">
مخصوص شهر:
</p>
<p className="text-[#9B9B9B] text-[11px] md:text-[14px] lg:text-[16px] font-medium">
{cityName}
</p>
</div>
)}
```
`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=<slug-پست-یاسوجی>
G=<slug-پست-سراسری>
# پست شهری: 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 برمی‌گردد.
</div>