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>
157 lines
7.1 KiB
Markdown
157 lines
7.1 KiB
Markdown
<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>
|