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>
465 lines
40 KiB
Markdown
465 lines
40 KiB
Markdown
<div dir="rtl" markdown="1">
|
||
|
||
# تقسیم ممیزی SEO به پرامپتهای کوچک اجرایی — nobat724
|
||
|
||
> **وضعیت: P1 تا P11 پیاده و روی build محلی production تأیید شدهاند (هنوز deploy نشدهاند).**
|
||
> این فایل حالا **مرجع ممیزی** است، نه فهرست کار. کارهای باقیمانده به پرامپتهای کوچکتر تقسیم شدهاند — جدول «کارهای باقیمانده» پایینتر.
|
||
> بخشهای P1–P11 برای ثبت تصمیمها و یافتههای اصلی نگه داشته شدهاند.
|
||
|
||
## چکلیست کلی — انجامشده
|
||
|
||
| # | پرامپت | وضعیت | نتیجه |
|
||
| --- | --------------------------------------- | ----- | ----- |
|
||
| P1 | بررسی robots.txt | ✅ | مشکلی نبود؛ همهٔ مسیرها مجاز، sitemap معرفی شده. بدون تغییر کد |
|
||
| P2 | هلپرهای مشترک | ✅ | `lib/domainHelpers.js` (+ `findCityById`، `extractEntityCityId`) |
|
||
| P3 | رفع noindex سیستمیک لیستها | ✅ | علت واقعی: **mutate شدن شیء `searchParams` مشترک** بین بدنهٔ صفحه و `generateMetadata` — نه منطق شمارنده. `lib/listingRobots.js` |
|
||
| P4 | استراتژی canonical سهدسته + pagination | ✅ | `lib/getCanonicalUrl.js` بازنویسی شد؛ `x-search` به `proxy.js` اضافه شد؛ تناقض canonical/JSON-LD رفع شد |
|
||
| P5 | بازسازی sitemap | ✅ | معیار «claimed» → `lib/entityQuality.js`؛ **باگ کشفشده:** سقف خاموش ۵۰ رکوردی API، یاسوج ۴۸ → ۴۵۴ پزشک |
|
||
| P6 | صفحات موجودیت: 404 و noindex | ✅ | علت واقعی Soft-404: **`loading.js`** (Suspense → استریم ۲۰۰). هر سه حذف شدند |
|
||
| P7 | اصلاح H1ها | ✅ | همهٔ صفحات دقیقاً یک h1؛ کلینیک ۴ → ۱؛ Title ریشه اصلاح شد |
|
||
| P8 | یکپارچهسازی Breadcrumb | ✅ | `Pageguide` سمانتیک شد + `BreadcrumbList` از همان منبع داده |
|
||
| P9 | صفحات فرود تخصص | ✅ | `app/specialties/[slug]/` + مهاجرت ~۹۰ لینک کارت |
|
||
| P10 | بلاگ شهر-محور | ⚠️ | **کد آماده ولی غیرفعال** — API بلاگ صفر رکورد دارد و فیلد شهر ندارد. نیازمند تغییر backend |
|
||
| P11 | FAQ schema + متن مقدمه | ✅ | `FAQPage` جدا در صفحات پزشک و تخصص؛ متن مقدمهٔ یکتا با تست شمارش کلمه |
|
||
|
||
## کارهای باقیمانده — پرامپتهای کوچک
|
||
|
||
به ترتیب وابستگی. **backend اول.**
|
||
|
||
| # | پرامپت | چرا |
|
||
|---|--------|-----|
|
||
| ۱ | `clinicpro/.claude/prompt/doctors-list-city-and-limit.md` | افزودن `city` به پاسخ لیست پزشکان + شفافکردن سقف `limit` |
|
||
| ۲ | `clinicpro/.claude/prompt/blog-city-field.md` | افزودن شهر به Entity بلاگ — پیشنیاز P10 |
|
||
| ۳ | `clinicpro/.claude/prompt/cleanup-polluted-doctor-clinic-records.md` | پاکسازی رکوردهای `name = شمارهتلفن` و `"test"` + اعتبارسنجی |
|
||
| ۴ | `nobat724_front/.claude/prompt/seo-post-deploy-verification.md` | اجرای معیارهای پذیرش روی دامنههای واقعی بعد از deploy |
|
||
| ۵ | `nobat724_front/.claude/prompt/blog-city-scoping-activate.md` | فعالسازی P10 — وابسته به ۲ |
|
||
| ۶ | `nobat724_front/.claude/prompt/sitemap-simplify-with-city.md` | حذف ۳۵ sweep پرهزینه — وابسته به ۱ |
|
||
|
||
پرامپت ۴ مستقل است و میتواند بلافاصله بعد از deploy اجرا شود.
|
||
|
||
## تصمیمهای آگاهانه (side-effect نیستند)
|
||
|
||
- **M3-B:** صفحات `?page=2+` خودشان canonical دارند و ایندکسپذیر میمانند (collapse به صفحهٔ ۱ انتخاب نشد).
|
||
- **sitemap-index پیاده نشد:** ~۲٬۵۰۰ URL در هر دامنه، بسیار زیر سقف ۵۰k. جزئیات در پرامپت ۶.
|
||
- **`loading.js` سه مسیر موجودیت حذف شد:** بهای رفع Soft-404. اسکلت لودینگ هنگام ناوبری کلاینتی از دست رفت.
|
||
- **«خانه» به breadcrumb صفحهٔ `/clinics` اضافه شد:** تغییر محتوایی عمدی، همراستا با schema که از قبل روی `/clinic/[id]` بود.
|
||
- **h1 صفحهٔ اصلی روی شعار نشست** (نه یک جملهٔ کیوردی جدید): شعار هر شهر در `city.json` از قبل شهرمحور و کیورددار است.
|
||
|
||
---
|
||
|
||
## بلوک زمینهٔ مشترک (ابتدای هر پرامپت کپی شود)
|
||
|
||
```
|
||
## نقش
|
||
تو یک متخصص ارشد Technical SEO + مهندس Next.js 15 App Router هستی. هر تغییر با کمترین ریسک و مطابق آخرین Best Practiceهای Google Search و Next.js. فقط قابلیتهای رسمی و غیرمنسوخ Next.js. زبان خروجی و رشتهها فارسی، RTL، تقویم جلالی.
|
||
|
||
## زمینه
|
||
پروژه `nobat724_front`: یک deployment واحد Next.js 15 که ۳۵ دامنهٔ شهری مستقل (data/city.json — مثل tehran-nobat.ir, yazd-nobat.ir) + دامنهٔ اصلی nobat724.com را سرو میکند. تشخیص شهر از host header (lib/getStateInfo.js سرور، context/ProvinceProvider.js کلاینت). رکورد id:600 در city.json دامنهٔ ریشه است نه شهر (lib/rootCity.js). slug پزشک/کلینیک = uuid. صفحات SSR (streaming با Suspense).
|
||
|
||
## فایلهای کلیدی
|
||
lib/getCanonicalUrl.js (تولید canonical — ریشهٔ باگ canonical) · app/sitemap.js (sitemap per-domain) · app/doctors/page.js (metadata لیست + listingRobots — باگ noindex سیستمیک) · app/clinics/page.js + components/clinics/ · app/doctor/[slug]/page.js (Physician schema + noindex برای unclaimed) · app/clinic/[slug]/page.js (canonical متناقض با JSON-LD، بدون noindex برای کلینیک خالی، چند h1) · app/specialties/page.js + components/specialties/ · app/blog/[slug]/page.js + app/blog/page.js · app/page.js + components/home/ · data/city.json + data/specialties.json + data/state.json · next.config.js (security headers — دستنخورده)
|
||
|
||
## محدودیتهای عمومی
|
||
- security headers و CSP در next.config.js را نشکن.
|
||
- هر تغییر canonical/robots را با curl روی حداقل یک دامنهٔ شهری + دامنهٔ اصلی تأیید کن.
|
||
- خارج از دامنهٔ اپ اقدام نکن (تنظیمات CDN/ArvanCloud = DevOps، فقط در گزارش یادآوری کن).
|
||
- پس از تغییر کد، `graphify update .` را اجرا کن.
|
||
- در پایان: گزارش کوتاه تغییرات + خروجی curl معیارهای پذیرش.
|
||
|
||
## محدودیتهای UI (در همهٔ پرامپتها الزامی)
|
||
- **UI سایت نباید بههم بریزد.** تغییرات SEO (تعویض تگ h1↔span/p/h2، canonical، robots، schema) نباید هیچ تغییر بصری ایجاد کنند — هنگام تعویض تگ، className و استایلهای عنصر قبلی را عیناً روی تگ جدید نگهدار.
|
||
- **صفحهٔ جدید = کامپوننتهای موجود.** برای هر صفحهٔ جدید (صفحات تخصص، breadcrumb، بلاگ) از کامپوننتهای موجود پروژه استفاده کن (کارت پزشک، لیست، دکمهها، layout و header/footer فعلی) — کامپوننت جدید فقط وقتی بساز که هیچ معادل موجودی نباشد، و در آن صورت با همان design tokenها و کلاسهای Tailwind پروژه.
|
||
- **ساختار صفحات جدید دقیقاً شبیه UI فعلی باشد.** صفحهٔ /specialties/[slug] باید از نظر چیدمان، فاصلهگذاری، تایپوگرافی و رنگ مثل /doctors فعلی به نظر برسد؛ کاربر نباید حس کند وارد بخش متفاوتی از سایت شده.
|
||
- بعد از هر تغییر، صفحات تغییریافته را از نظر بصری با نسخهٔ قبل مقایسه کن (اسکرینشات یا بازبینی دستی) و در گزارش تأیید کن که رگرسیون بصری نداریم.
|
||
|
||
## نقاط قوت فعلی (تخریب نشوند)
|
||
اسکیمای Physician/Review/Geo · هدرهای امنیتی HSTS/CSP · ریدایرکت www→non-www با 301 · صفحهٔ ۴۰۴ عمومی درست.
|
||
```
|
||
|
||
---
|
||
|
||
## مرجع: فهرست کامل یافتههای زندهٔ ممیزی (تأییدشده با HTML واقعی)
|
||
|
||
این بخش مرجع است؛ هر پرامپت یافتههای مرتبط خودش را جداگانه دارد، اما تصویر کامل اینجاست:
|
||
|
||
- `yazd-nobat.ir/` و `yazd-nobat.ir/doctors` → canonical به `nobat724.com` (باگ سراسری canonical).
|
||
- `yasuj-nobat.ir/doctors` **بدون هیچ query** → `robots: noindex, follow` + canonical به دامنهٔ اصلی. علت: `city`/`state` خودکار از host تزریق میشوند و شمارندهٔ `listingRobots` همیشه ۲+ میشود ⇒ صفحهٔ اصلی لیست روی **همهٔ ۳۵ دامنه همیشه noindex** است.
|
||
- `yasuj-nobat.ir/clinics` → همان دو مشکل (noindex + canonical غلط) ⇒ باگ فقط `/doctors` نیست.
|
||
- `yasuj-nobat.ir/specialties` → canonical غلط اما **بدون** noindex ⇒ `listingRobots` بین doctors/clinics مشترک است، specialties مسیر جداست. دو `<h1>` بدون نام شهر. ~۹۰ لینک کارت تخصص به `/doctors?specialty=X&city=…&state=…`.
|
||
- `yasuj-nobat.ir/clinic/9c163d69-…` → **تناقض زنده:** canonical به `nobat724.com` اما `MedicalClinic.url` و کل `BreadcrumbList` به `yasuj-nobat.ir`. کلینیک `is_active:false` با نام «09398631203» بدون noindex ایندکسپذیر (نامتقارن با پزشکان). چهار `<h1>` (نام + سه عنوان بخش). schema هست ولی breadcrumb بصری نیست (معکوس `/clinics`). Title = «09398631203 | یاسوج نوبت».
|
||
- `yasuj-nobat.ir/contact-us` → canonical به دامنهٔ اصلی، **بدون** noindex — برای صفحات استاتیک مشترک این canonical احتمالاً درست است (استثنای C1-c).
|
||
- `yasuj-nobat.ir/about-us` → canonical مشابه اما **با** noindex ⇒ robots صفحات استاتیک ناسازگار است.
|
||
- دادهٔ embedded یاسوج: `totalRecords: 456, totalPages: 38` ⇒ با query-stripping فعلی، صفحات ۲ تا ۳۸ به صفحهٔ ۱ collapse میشوند.
|
||
- HTML اولیه ۶ کارت اسکلت با `href="/doctor/undefined"` دارد ⇒ رندر streaming با Suspense است، نه SSR همزمان کامل.
|
||
- رکوردهای دادهٔ آلوده: پزشک با `name:"09390039833"` و `name:"test"` با `owner_status:"claimed"` در نتایج عمومی.
|
||
- `nobat724.com/doctors` → بدون هیچ `<h1>`. H1 صفحهٔ اصلی = «با نوبت 724». Title روت /doctors = «پزشکان نوبت 724 …» (نام رکورد root بهجای شهر).
|
||
- `/doctor/<uuid-نامعتبر>` → کد 200 (Soft-404).
|
||
- `nobat724.com/sitemap.xml` → فقط ۱۳ URL (۷ استاتیک + ۴ پزشک claimed + ۲ کلینیک + ۰ بلاگ).
|
||
- هیچ روت تمیز specialty/city و هیچ `FAQPage` schema در کل سایت نیست.
|
||
- بلاگ شهر-محور نیست با اینکه هر شهر نمایندهٔ محتوایی دارد.
|
||
|
||
---
|
||
|
||
## P1 — بررسی robots.txt (پیشنیاز اعتبارسنجی)
|
||
|
||
> پیشنیاز: هیچ. اجرا: قبل از همه — اگر مسیرها مسدود باشند بقیهٔ کارها بیاثرند.
|
||
|
||
```
|
||
[بلوک زمینهٔ مشترک]
|
||
|
||
## تسک
|
||
فقط بررسی و گزارش — بدون تغییر کد مگر مشکل پیدا شود.
|
||
|
||
1. robots.txt دامنهٔ اصلی و حداقل دو دامنهٔ شهری را بگیر:
|
||
curl -s https://nobat724.com/robots.txt
|
||
curl -s https://yasuj-nobat.ir/robots.txt
|
||
curl -s https://tehran-nobat.ir/robots.txt
|
||
2. تأیید کن:
|
||
- مسیرهای /doctors, /clinics, /specialties, /blog, /doctor/, /clinic/ مسدود (Disallow) نیستند.
|
||
- sitemap.xml با خط Sitemap: معرفی شده است.
|
||
- مسیرهای پنل/لاگین (در صورت وجود) Disallow هستند.
|
||
3. اگر مسیری بهاشتباه مسدود است، فایل/روت تولیدکنندهٔ robots.txt را پیدا و اصلاح کن (احتمالاً app/robots.js یا فایل استاتیک public/robots.txt).
|
||
|
||
## معیار پذیرش
|
||
خروجی کامل robots.txt هر سه دامنه در گزارش نقل شود + جدول «مسیر × وضعیت (مجاز/مسدود)». اگر اصلاحی انجام شد، curl بعد از deploy هم ضمیمه شود.
|
||
```
|
||
|
||
---
|
||
|
||
## P2 — هلپرهای مشترک (زیرساخت)
|
||
|
||
> پیشنیاز: هیچ. دو تابع که چند پرامپت بعدی مصرف میکنند — دو پیادهسازی جدا ریسک واگرایی دارد، پس اول و یکبار ساخته میشوند.
|
||
|
||
```
|
||
[بلوک زمینهٔ مشترک]
|
||
|
||
## تسک
|
||
دو هلپر مشترک بساز (محل پیشنهادی: lib/domainHelpers.js یا کنار getStateInfo):
|
||
|
||
### ۱) findDomainByCityId(cityId)
|
||
- ورودی: city_id یک موجودیت (پزشک/کلینیک/پست بلاگ).
|
||
- خروجی: دامنهٔ شهری متناظر از data/city.json (مثلاً "yasuj-nobat.ir")، یا null اگر شهر دامنهٔ اختصاصی ندارد.
|
||
- رکورد id:600 (دامنهٔ ریشه) هرگز بهعنوان «شهر» برنگردد.
|
||
- مصرفکنندگان آینده: canonical موجودیتها (P4)، sitemap (P5)، بلاگ (P10).
|
||
|
||
### ۲) resolveCityDisplayName(cityInfo)
|
||
- ورودی: آبجکت شهر از getStateInfo.
|
||
- خروجی: نام قابلنمایش شهر برای H1/Title. اگر isRoot (رکورد id:600) → «ایران»، نه نام رکورد root («نوبت 724»).
|
||
- مصرفکنندگان آینده: H1 صفحات لیست (P7)، صفحات تخصص (P9).
|
||
|
||
## نکته
|
||
یافتهٔ زنده: Title روت /doctors الان «پزشکان نوبت 724 …» رندر میشود چون نام رکورد root بهجای شهر مینشیند — resolveCityDisplayName دقیقاً برای رفع این است.
|
||
|
||
## معیار پذیرش
|
||
- تست واحد یا اسکریپت کوچک: findDomainByCityId برای city_id یاسوج → "yasuj-nobat.ir"؛ برای city_id بدون دامنه → null؛ برای id:600 → null.
|
||
- resolveCityDisplayName برای رکورد root → «ایران»؛ برای یاسوج → «یاسوج».
|
||
```
|
||
|
||
---
|
||
|
||
## P3 — رفع noindex سیستمیک صفحات لیست (C5 — بدترین باگ)
|
||
|
||
> پیشنیاز: P2 (برای تشخیص «شهر host»). مهمترین فیکس تکخطینما با بیشترین اثر.
|
||
|
||
```
|
||
[بلوک زمینهٔ مشترک]
|
||
|
||
## یافتهٔ زندهٔ تأییدشده
|
||
- yasuj-nobat.ir/doctors و yasuj-nobat.ir/clinics — هر دو بدون هیچ query string — robots: noindex, follow دارند.
|
||
- علت: city/state روی سرور خودکار از host استنتاج و به شیء پارامترها تزریق میشوند؛ منطق listingRobots («۲+ پارامتر → noindex») همیشه این دو را میشمارد و همیشه ۲+ میشود.
|
||
- نتیجه: صفحات اصلی لیست پزشکان و کلینیکها روی هر ۳۵ دامنه همیشه noindex هستند — مهمترین صفحات هر دامنه برای «پزشک + شهر».
|
||
- /specialties این باگ را ندارد (مسیر جدا) — یعنی listingRobots بین /doctors و /clinics مشترک است؛ همهٔ مصرفکنندگانش را پوشش بده، نه فقط app/doctors/page.js.
|
||
- یافتهٔ مرتبط: about-us هم noindex دارد ولی contact-us ندارد — robots در کل اپ ناسازگار است؛ هنگام فیکس، همهٔ نقاط اعمال robots را یکجا فهرست و بازبینی کن.
|
||
|
||
## تسک
|
||
1. در listingRobots: پارامترهای city/state وقتی مقدارشان برابر شهر/استان استنتاجشده از host فعلی است، از شمارش «۲+ فیلتر» کاملاً حذف شوند — چه از query آمده باشند چه تزریق سمت سرور.
|
||
2. فقط فیلترهای صریحاً کاربر-انتخابشده بشمارند: تخصص، جنسیت، مرتبسازی غیرپیشفرض، city/state متفاوت از host.
|
||
3. قاعدهٔ اصلی (noindex برای جستجوی نام یا فیلتر واقعی چندگانه) حفظ شود.
|
||
4. robots صفحهٔ about-us را هم به index برگردان (صفحهٔ استاتیک نباید noindex باشد؛ canonical آن در P4 تعیین میشود).
|
||
|
||
## معیار پذیرش
|
||
curl -s https://yasuj-nobat.ir/doctors | grep -i robots # → index یا غایب
|
||
curl -s https://yasuj-nobat.ir/clinics | grep -i robots # → index یا غایب
|
||
curl -s https://tehran-nobat.ir/doctors | grep -i robots # → index یا غایب
|
||
curl -s https://yazd-nobat.ir/doctors | grep -i robots # → index یا غایب
|
||
curl -s https://yasuj-nobat.ir/about-us | grep -i robots # → index یا غایب
|
||
curl -s "https://yasuj-nobat.ir/doctors?city=تهران&specialty=X" | grep -i robots # → noindex بماند
|
||
تست حداقل روی ۳ دامنهٔ متفاوت و هر دو مسیر /doctors و /clinics — باگ سیستمیک است، یک URL کافی نیست.
|
||
```
|
||
|
||
---
|
||
|
||
## P4 — استراتژی canonical سهدسته + تصمیم pagination (C1-a/b/c + M3-B)
|
||
|
||
> پیشنیاز: P2 (findDomainByCityId)، P3 (تا canonical و robots همجهت شوند). بزرگترین پرامپت — قلب فیکس.
|
||
|
||
```
|
||
[بلوک زمینهٔ مشترک]
|
||
|
||
## یافتههای زندهٔ تأییدشده
|
||
- getCanonicalUrl() در lib/getCanonicalUrl.js همهٔ صفحات روی همهٔ دامنهها را به nobat724.com canonical میکند (buildMainCanonicalUrl). تأییدشده روی: /doctors، /clinics، /specialties، /clinic/[id]، /contact-us، /about-us.
|
||
- تناقض زنده در clinic/9c163d69: canonical به nobat724.com اما MedicalClinic.url و BreadcrumbList به yasuj-nobat.ir — سیگنال متناقض به گوگل.
|
||
- pagination واقعی: یاسوج totalPages: 38 — با query-stripping فعلی همهٔ صفحات ۲+ به صفحهٔ ۱ collapse میشوند.
|
||
|
||
## قاعدهٔ سهدسته
|
||
| دسته | صفحات | canonical |
|
||
|------|-------|-----------|
|
||
| C1-a: per-domain | / ، /doctors ، /clinics ، /specialties ، /specialties/[slug] ، /blog (لیست) | self-canonical روی همان دامنه |
|
||
| C1-b: موجودیت | /doctor/[uuid] ، /clinic/[uuid] ، /blog/[slug] شهریافته | دامنهٔ شهرِ همان موجودیت (با findDomainByCityId از P2) |
|
||
| C1-c: استاتیک مشترک | /contact-us ، /about-us ، /terms ، /privacy ، … | دامنهٔ اصلی nobat724.com (استثنای عمدی — محتوا روی همهٔ دامنهها یکسان است) |
|
||
|
||
## تسک
|
||
1. getCanonicalUrl را بازنویسی کن تا searchParams هم بپذیرد (الان فقط x-pathname میخواند و query را کامل نادیده میگیرد):
|
||
- STATIC_SHARED_PAGES (فهرست C1-c، با تیم نهایی شود) → buildMainCanonicalUrl.
|
||
- /doctors با searchParams فقط specialty (تکفیلتر) → canonical به /specialties/[slug] (مقصد در P9 ساخته میشود؛ فعلاً تابع buildSpecialtyCanonicalUrl را آماده کن و پشت feature-flag یا شرط وجود روت بگذار).
|
||
- پارامتر page>1 → تصمیم M3-B: گزینهٔ پیشنهادی page را در canonical نگهدار (?page=2 خود canonical) تا صفحات ۲+ ایندکسپذیر بمانند. اگر گزینهٔ collapse به صفحهٔ ۱ انتخاب شد، در گزارش صریحاً بهعنوان تصمیم آگاهانه مستند کن، نه side-effect.
|
||
- سایر queryها (فیلتر چندگانه، sort، نام) → حذف از canonical (رفتار فعلی درست است).
|
||
- سایر مسیرها → self-canonical با host فعلی.
|
||
2. برای موجودیتها تابع getEntityCanonical(entity, pathname, currentHost) بساز:
|
||
- cityDomain = findDomainByCityId(entity?.city_id)؛ اگر null → self-canonical (fallback؛ هرگز canonical شکسته نده).
|
||
- حالتهای لبهای: پزشک چند-شهری → یک شهر اصلی (اولین/پرترافیکترین مطب) روی همهٔ دامنهها؛ شهر بدون دامنه → nobat724.com و self.
|
||
- در app/doctor/[slug]/page.js و app/clinic/[slug]/page.js اعمال کن.
|
||
3. JSON-LD را هماهنگ کن: فیلدهای url/@id در Physician و MedicalClinic و آیتمهای BreadcrumbList باید همان مقصد canonical را منعکس کنند (تناقض زندهٔ بالا را رفع و تست کن).
|
||
4. buildMainCanonicalUrl را فقط اگر جای دیگری (JSON-LD/sitemap) مصرف نمیشود حذف کن؛ وگرنه دستنخورده.
|
||
5. گزینهٔ 301 بهجای canonical برای موجودیتها را فقط در گزارش پیشنهاد بده (عیبها: redirect chain بکلینکها، پرش دامنه در UX پورتال). بدون تأیید صریح پیاده نکن.
|
||
|
||
## معیار پذیرش
|
||
# C1-a
|
||
curl -s https://yazd-nobat.ir/doctors | grep canonical # → yazd-nobat.ir/doctors
|
||
curl -s https://nobat724.com/doctors | grep canonical # → nobat724.com/doctors
|
||
curl -s https://yasuj-nobat.ir/specialties | grep canonical # → yasuj-nobat.ir/specialties
|
||
# C1-b
|
||
curl -s https://yasuj-nobat.ir/doctor/beaca548-816f-4613-937d-360db01bd7c8 | grep canonical # → self
|
||
curl -s https://nobat724.com/doctor/beaca548-816f-4613-937d-360db01bd7c8 | grep canonical # → yasuj-nobat.ir
|
||
curl -s https://yasuj-nobat.ir/clinic/9c163d69-0051-4745-a423-c830135b1c01 | grep canonical # → self (شهر=یاسوج)
|
||
# C1-c
|
||
curl -s https://yasuj-nobat.ir/contact-us | grep canonical # → nobat724.com/contact-us
|
||
curl -s https://yasuj-nobat.ir/about-us | grep canonical # → nobat724.com/about-us
|
||
# M3-B (اگر گزینهٔ پیشنهادی)
|
||
curl -s "https://yasuj-nobat.ir/doctors?page=2" | grep canonical # → …/doctors?page=2
|
||
# هماهنگی JSON-LD
|
||
curl -s https://yasuj-nobat.ir/clinic/9c163d69-… | grep -o '"url":"[^"]*"' # → همان دامنهٔ canonical
|
||
```
|
||
|
||
---
|
||
|
||
## P5 — بازسازی sitemap (C2)
|
||
|
||
> پیشنیاز: P4 (sitemap باید فقط URLهای canonical را سابمیت کند).
|
||
|
||
```
|
||
[بلوک زمینهٔ مشترک]
|
||
|
||
## یافتهٔ زنده
|
||
nobat724.com/sitemap.xml فقط ۱۳ URL دارد (۷ استاتیک + ۴ پزشک + ۲ کلینیک + ۰ بلاگ). علت: فیلتر owner_status === 'claimed' در getDoctorUrls (app/sitemap.js). یک مارکتپلیس عملاً ۴ صفحهٔ پزشک ایندکسپذیر دارد.
|
||
|
||
## تسک
|
||
1. معیار ورود به sitemap: از «claimed» به «دارای حداقل دادهٔ معنادار» (uuid + حداقل یک تخصص یا آدرس/شهر).
|
||
2. در app/doctor/[slug]/page.js شرط isUnclaimed → noindex را نرم کن: فقط پزشکان بدون هیچ محتوای معنادار noindex بمانند.
|
||
3. هماهنگی با canonical (P4): getDoctorUrls از همان findDomainByCityId استفاده کند. sitemap هر دامنه فقط موجودیتهایی را شامل شود که canonical آنها همان دامنه است. sitemap دامنهٔ اصلی فقط موجودیتهای بدون دامنهٔ شهری.
|
||
4. برای مقیاس بالای ~۵۰k URL، sitemap-index با فایلهای جدا (doctors/clinics/blogs/static) بساز.
|
||
|
||
## معیار پذیرش
|
||
curl -s https://nobat724.com/sitemap.xml | grep -c "<loc>" # بسیار بیشتر از ۱۳
|
||
curl -s https://nobat724.com/sitemap.xml | grep "beaca548-816f-4613-937d-360db01bd7c8" # → خالی (canonical این پزشک یاسوج است)
|
||
curl -s https://yasuj-nobat.ir/sitemap.xml | grep "beaca548-816f-4613-937d-360db01bd7c8" # → موجود
|
||
پزشک معتبر unclaimed دیگر noindex نگیرد (curl روی یک نمونه).
|
||
```
|
||
|
||
---
|
||
|
||
## P6 — صفحات موجودیت: Soft-404 و noindex کلینیک خالی (H4 + H7)
|
||
|
||
> پیشنیاز: P4 (تا سیاست robots/canonical موجودیتها معلوم باشد). دو فیکس کوچک همحوزه.
|
||
|
||
```
|
||
[بلوک زمینهٔ مشترک]
|
||
|
||
## یافتههای زنده
|
||
- /doctor/<uuid-نامعتبر> کد 200 میدهد (Soft-404) — API برای uuid ناموجود 200 با data:null برمیگرداند و کد به notFound() نمیرسد.
|
||
- clinic/9c163d69 با is_active:false، نام «09398631203» (شمارهتلفن)، بدون caption/logo/services — کاملاً ایندکسپذیر است. منطق «thin → noindex» که برای پزشک unclaimed هست، برای کلینیک اصلاً وجود ندارد (نامتقارن).
|
||
|
||
## تسک
|
||
### H4 — Soft-404
|
||
1. در getDoctor (app/doctor/[slug]/page.js): null-check کامل روی json?.data?.data → notFound().
|
||
2. همان الگو برای app/clinic/[slug]/page.js و app/blog/[slug]/page.js.
|
||
(روت /specialties/[slug] هنوز ساخته نشده — در P9 همین الگو اعمال میشود.)
|
||
|
||
### H7 — noindex کلینیک خالی
|
||
1. در app/clinic/[slug]/page.js: کلینیک با is_active:false یا بدون محتوای معنادار (نام واقعی/خدمات/توضیح) → noindex.
|
||
2. تصمیم بگیر و مستند کن: is_active:false یعنی «حذفشده» (→ 404/410) یا «موقتاً غیرفعال» (→ noindex)؟
|
||
3. هماهنگی با P5: موجودیت noindex نباید در sitemap باشد.
|
||
4. در گزارش برای تیم عملیات: رکوردهای آلوده (نام = شمارهتلفن، name:"test" با owner_status:"claimed") باید در لایهٔ داده پاک شوند — خارج از کد.
|
||
|
||
## معیار پذیرش
|
||
curl -o /dev/null -w "%{http_code}" https://nobat724.com/doctor/invalid-xxx # → 404
|
||
curl -o /dev/null -w "%{http_code}" https://nobat724.com/clinic/invalid-xxx # → 404
|
||
curl -s https://yasuj-nobat.ir/clinic/9c163d69-0051-4745-a423-c830135b1c01 | grep -i robots # → noindex (یا 404/410 طبق تصمیم)
|
||
curl -s https://yasuj-nobat.ir/sitemap.xml | grep "9c163d69" # → خالی
|
||
```
|
||
|
||
---
|
||
|
||
## P7 — اصلاح H1ها در همهٔ صفحات (H2 + H3)
|
||
|
||
> پیشنیاز: P2 (resolveCityDisplayName). مستقل از canonical — میتواند موازی P4-P6 اجرا شود.
|
||
|
||
```
|
||
[بلوک زمینهٔ مشترک]
|
||
|
||
## یافتههای زنده (الگوی مشترک: Title/meta شهرمحور درستاند، H1 از منبع جدا و استاتیک میآید)
|
||
| صفحه | H1 فعلی | مشکل |
|
||
|------|---------|-------|
|
||
| / (اصلی) | «با نوبت 724» | برندمحور، بدون کیورد |
|
||
| /doctors | «کهگیلویه و بویراحمد / یاسوج» | متن دکمهٔ انتخاب موقعیت داخل <button> — h1 نیست |
|
||
| /specialties | دو h1: «تخصص مورد نظر شما چیست؟» + «لیست تخصص ها» | دو h1، بدون نام شهر |
|
||
| /clinics | «لیست کلینیک ها و درمانگاه های تخصصی» | استاتیک، بدون نام شهر |
|
||
| /clinic/[id] | چهار h1: نام + «بیمه های طرف قرارداد» + «تخصص ها» + «خدمات» | عنوان بخشها با h1 |
|
||
| Title روت /doctors | «پزشکان نوبت 724 …» | نام رکورد root بهجای شهر |
|
||
|
||
## تسک
|
||
1. صفحهٔ اصلی: H1 → «نوبتدهی آنلاین پزشکان و کلینیکهای سراسر ایران» (برند جای دیگر).
|
||
2. /doctors: عنصر دکمه از h1 به span/p؛ h1 واقعی بالای لیست: «پزشکان و متخصصان {resolveCityDisplayName} | رزرو نوبت آنلاین» (components/doctors/).
|
||
3. /specialties: «تخصص مورد نظر شما چیست؟» غیرعنوانی شود؛ h1 یکتا: «لیست تخصصهای پزشکی در {resolveCityDisplayName}».
|
||
4. /clinics: h1 پویا: «کلینیکها و مراکز درمانی {resolveCityDisplayName}».
|
||
5. /clinic/[id]: فقط نام کلینیک h1؛ سه عنوان بخش → h2. همین را در /doctor/[id] هم بررسی و در صورت وجود اصلاح کن.
|
||
6. همهٔ الگوهای «پزشکان {cityName}» از resolveCityDisplayName استفاده کنند (رفع Title روت).
|
||
7. هر صفحه دقیقاً یک h1 (نه صفر، نه چند).
|
||
8. **بدون تغییر بصری:** هر تعویض تگ (h1→span، h1→h2) className و استایل عنصر قبلی را عیناً حفظ کند؛ h1 جدیدی که اضافه میشود با تایپوگرافی موجود صفحه هماهنگ باشد. بعد از تغییر، ظاهر صفحات با قبل مقایسه و در گزارش تأیید شود.
|
||
|
||
## معیار پذیرش
|
||
for p in doctors specialties clinics; do
|
||
curl -s "https://yasuj-nobat.ir/$p" | grep -c "<h1" # → 1
|
||
curl -s "https://yasuj-nobat.ir/$p" | grep -o "<h1[^>]*>[^<]*" # → شامل «یاسوج»
|
||
done
|
||
curl -s https://yasuj-nobat.ir/clinic/9c163d69-… | grep -c "<h1" # → 1 (فعلاً 4)
|
||
curl -s https://nobat724.com/ | grep -o "<h1[^>]*>[^<]*" # → کیورددار، نه «با نوبت 724»
|
||
curl -s https://nobat724.com/doctors | grep -o "<title>[^<]*" # → بدون تکرار «نوبت 724» بهجای شهر
|
||
```
|
||
|
||
---
|
||
|
||
## P8 — یکپارچهسازی Breadcrumb (H6)
|
||
|
||
> پیشنیاز: P4 (URLهای schema باید با canonical همجهت باشند)، P7 (h2های اشتباه همانجا پاک میشوند).
|
||
|
||
```
|
||
[بلوک زمینهٔ مشترک]
|
||
|
||
## یافتههای زنده (دو نیمهٔ ناقص یک قابلیت)
|
||
- /clinics: breadcrumb بصری با تگ <h2> داخل <ul> ساخته شده («کلینیک ها > یاسوج»)، بدون لینک <a>، بدون BreadcrumbList schema.
|
||
- /clinic/[id]: دقیقاً برعکس — BreadcrumbList JSON-LD دارد (به yasuj-nobat.ir اشاره میکند) اما هیچ breadcrumb بصری ندارد.
|
||
|
||
## تسک
|
||
1. یک کامپوننت Breadcrumb واحد بساز: <nav aria-label="breadcrumb"> + <ol> + <a> (آیتم فعلی <span>). هیچ h2ای. **ظاهرش دقیقاً همان breadcrumb فعلی /clinics باشد** (همان کلاسهای Tailwind، فاصلهها، جداکنندهٔ «>») — فقط تگها و ساختار سمانتیک عوض میشود، نه شکل بصری.
|
||
2. BreadcrumbList JSON-LD همراه همان کامپوننت (script مستقل، با safeJsonLd موجود) — یک منبع داده برای بصری و schema تا هرگز واگرا نشوند.
|
||
3. روی /clinics و /clinic/[id] اعمال کن؛ /doctors و /specialties و /doctor/[id] را هم بررسی و در صورت نیاز اضافه کن.
|
||
4. URLهای آیتمها با مقصد canonical صفحه (P4) همجهت باشند.
|
||
|
||
## معیار پذیرش
|
||
curl -s https://yasuj-nobat.ir/clinics | grep -o "<h2[^>]*>[^<]*" # → بدون «کلینیک ها»/«یاسوج»
|
||
curl -s https://yasuj-nobat.ir/clinics | grep "BreadcrumbList" # → موجود
|
||
curl -s https://yasuj-nobat.ir/clinic/9c163d69-… | grep -c "aria-label=\"breadcrumb\"" # → 1 (بصری اضافه شد)
|
||
```
|
||
|
||
---
|
||
|
||
## P9 — صفحات فرود تخصص و «تخصص + شهر» (C3 — بزرگترین فرصت رشد)
|
||
|
||
> پیشنیاز: P4 (canonical و buildSpecialtyCanonicalUrl)، P5 (sitemap)، P7 (resolveCityDisplayName و الگوی h1).
|
||
|
||
```
|
||
[بلوک زمینهٔ مشترک]
|
||
|
||
## یافتهٔ زنده
|
||
- کیوردهای پرارزش («متخصص پوست تهران») هیچ صفحهٔ فرودی ندارند؛ فقط /doctors?specialty=… (کوئری فارسی).
|
||
- صفحهٔ /specialties با ~۹۰ کارت، همه به /doctors?specialty=X&city=یاسوج&state=… لینک میدهند — همان الگویی که پیش از P3 باعث noindex میشد؛ این ۹۰ لینک باید مقصد تمیز بگیرند.
|
||
|
||
## تسک
|
||
1. روت app/specialties/[slug]/page.js بساز — slug انگلیسی/تمیز از data/specialties.json. روی هر دامنهٔ شهری خودکار «تخصص + آن شهر» میشود (شهر از host). **این صفحه از کامپوننتهای موجود ساخته شود** — همان کارت پزشک، همان layout و header/footer، و همان کامپوننت لیستی که /doctors استفاده میکند؛ چیدمان و ظاهر نهایی باید از نظر کاربر دقیقاً همخانوادهٔ /doctors فعلی باشد. هیچ کامپوننت/استایل جدیدی نساز مگر معادل موجود نداشته باشد.
|
||
2. هر صفحه شامل:
|
||
- h1: «متخصص {specialty} در {resolveCityDisplayName}».
|
||
- پاراگراف مقدمهٔ یکتا (تعداد پزشک، میانگین امتیاز شهر، توضیح خدمت).
|
||
- لیست پزشکان SSR (/api/v1/doctors با فیلتر تخصص/شهر).
|
||
- بلوک FAQ + FAQPage schema (script مستقل).
|
||
- کامپوننت Breadcrumb از P8 + ItemList/MedicalWebPage.
|
||
- generateMetadata کیوردمحور، canonical self-domain (طبق C1-a در P4).
|
||
3. slug نامعتبر → notFound() (الگوی P6).
|
||
4. این URLها به sitemap اضافه شوند (P5): ضرب specialties.json × دامنهها.
|
||
5. لینکهای داخلی: کارتهای /specialties و فوتر → روت جدید. Breadcrumb صفحهٔ پزشک از /doctors?specialty= به URL تمیز.
|
||
6. فعالسازی canonical کوئری قدیم (آمادهشده در P4): /doctors?specialty=X تکفیلتر → canonical به /specialties/[slug].
|
||
|
||
## معیار پذیرش
|
||
curl -s https://tehran-nobat.ir/specialties/dermatology | grep -c "<h1>" # → 1 (شامل نام شهر)
|
||
curl -s https://nobat724.com/sitemap.xml | grep "specialties/" # → موجود
|
||
curl -s "https://tehran-nobat.ir/doctors?specialty=dermatology" | grep canonical # → …/specialties/dermatology
|
||
curl -o /dev/null -w "%{http_code}" https://tehran-nobat.ir/specialties/invalid-slug # → 404
|
||
curl -s https://yasuj-nobat.ir/specialties | grep -c "doctors?specialty=" # → 0 (لینکها مهاجرت کردهاند)
|
||
```
|
||
|
||
---
|
||
|
||
## P10 — بلاگ شهر-محور (C4)
|
||
|
||
> پیشنیاز: P4 (الگوی canonical موجودیت)، P5 (sitemap). ممکن است نیاز به تغییر بکاند/مدل داده داشته باشد — اول بررسی، بعد کد.
|
||
|
||
```
|
||
[بلوک زمینهٔ مشترک]
|
||
|
||
## یافتهٔ زنده
|
||
sitemap صفر URL بلاگ دارد. هر شهر نمایندهٔ اختصاصی دارد اما ساختار بلاگ ارتباط پست با شهر را در URL/عنوان/متادیتا منعکس نمیکند.
|
||
|
||
## پیش از کد، بررسی کن (اسکیمای دادهٔ بلاگ مستند نیست)
|
||
- آیا پست بلاگ فیلد شهر (city_id یا مشابه) دارد؟ اگر نه، افزودنش قدم اول است (با امکان «سراسری» برای پستهای عمومی).
|
||
- آیا نویسنده/نماینده به شهر مشخصی مرتبط است که بتوان از آن استنتاج کرد؟
|
||
|
||
## تسک
|
||
1. نمایش صریح شهر در HTML قابلمشاهده (نه فقط متادیتا): عنوان/زیرعنوان مثل «{title} | مخصوص {cityName}» یا بلوک «این مطلب برای شهر {cityName} نوشته شده». این عنصر با استایل موجود صفحهٔ بلاگ هماهنگ باشد (مثلاً همان کامپوننت badge/برچسبی که در پروژه هست) — عنصر بصری ناهماهنگ با بقیهٔ صفحه اضافه نکن.
|
||
2. canonical طبق الگوی موجودیت (P4): پست شهریافته → دامنهٔ همان شهر (با همان findDomainByCityId — تابع مشترک، نه پیادهسازی جدا). پست سراسری → self روی دامنهٔ اصلی.
|
||
3. لیست بلاگ per-domain: app/blog/page.js روی هر دامنهٔ شهری = پستهای آن شهر + سراسری؛ خود لیست self-canonical (C1-a).
|
||
4. sitemap (P5): هر پست فقط در sitemap دامنهٔ canonical خودش.
|
||
5. Soft-404 (الگوی P6): بعد از افزودن فیلد شهر، null-check همچنان درست باشد.
|
||
6. JSON-LD: BlogPosting/Article با areaServed یا spatialCoverage (script مستقل).
|
||
|
||
## معیار پذیرش
|
||
curl -s https://nobat724.com/sitemap.xml | grep -c "/blog/" # → بیشتر از ۰
|
||
curl -s https://yasuj-nobat.ir/blog/<slug-پست-یاسوجی> | grep -i "یاسوج" # → نام شهر در HTML
|
||
curl -s https://yasuj-nobat.ir/blog/<slug-پست-یاسوجی> | grep canonical # → self (دامنهٔ شهر)
|
||
curl -s https://nobat724.com/blog/<slug-پست-یاسوجی> | grep canonical # → yasuj-nobat.ir (نه self)
|
||
```
|
||
|
||
---
|
||
|
||
## P11 — FAQPage schema و متن مقدمهٔ یکتا (M1 + M2)
|
||
|
||
> پیشنیاز: P9 (صفحات تخصص باید وجود داشته باشند). آخرین لایه — بهبود، نه رفع باگ.
|
||
|
||
```
|
||
[بلوک زمینهٔ مشترک]
|
||
|
||
## تسک
|
||
### M1 — FAQPage schema
|
||
1. بلوک «سوالات متداول» + FAQPage JSON-LD به صفحات پزشک، تخصص و شهر.
|
||
2. FAQPage حتماً یک <script type="application/ld+json"> مستقل باشد (نه ادغام در @graph بدون تست) — schemaهای موجود Physician/Breadcrumb/Review/Geo نقطهٔ قوتاند و نباید بشکنند. از safeJsonLd موجود استفاده کن.
|
||
|
||
### M2 — متن مقدمهٔ یکتا
|
||
بالای /doctors و /clinics و صفحات تخصص: ۱۵۰–۳۰۰ کلمه متن یکتای شهری/تخصصی (رفع Thin Content — مکمل canonical؛ محتوای یکسان با canonical درست هنوز duplicate است).
|
||
نکته: متنها الگوی خام «{کار} کنید، {کار} کنید» نداشته باشند؛ برای هر شهر/تخصص ساختار جمله متفاوت باشد.
|
||
|
||
## معیار پذیرش
|
||
curl -s https://yasuj-nobat.ir/specialties/dermatology | grep "FAQPage" # → موجود
|
||
Rich Results Test گوگل روی یک صفحهٔ پزشک: schemaهای قبلی سالم + FAQPage معتبر.
|
||
شمارش کلمات بلوک مقدمه در ۳ شهر مختلف: بین ۱۵۰ تا ۳۰۰ و متنها یکسان نباشند.
|
||
```
|
||
|
||
---
|
||
|
||
## یادداشتهای سراسری (در گزارش نهایی هر فاز تکرار شود)
|
||
|
||
- **کیفیت داده (خارج از کد):** رکوردهای پزشک/کلینیک با name = شمارهتلفن یا "test" با owner_status:"claimed" باید توسط تیم عملیات پاکسازی شوند.
|
||
- **DevOps:** بررسی bot-challenge ArvanCloud روی دامنهٔ اصلی — کد نیست، فقط یادآوری.
|
||
- **پس از هر deploy مؤثر بر canonical/robots:** در Search Console هر دامنهٔ درگیر، URL Inspection روی صفحات کلیدی + انتظار چند هفته برای re-index.
|
||
- **SSR:** رندر لیستها streaming با Suspense است (۶ کارت اسکلت با href="/doctor/undefined" در HTML اولیه). فعلاً مشکلساز نیست اما در گزارش ذکر شود؛ اگر فرصت شد href اسکلتها خالی/بدون لینک شود.
|
||
|
||
</div>
|