Files
nobat724_front/.claude/prompt/technical-seo-audit-multidomain.md
T
hamed b04bb45ca1 Implement multi-domain SEO improvements:
- Add metadata to login and login-verify pages to prevent indexing.
- Update robots.txt to disallow additional sensitive paths.
- Enhance sitemap generation to filter by city and include accurate last modified dates.
- Refactor canonical URL generation to support multi-domain architecture, ensuring self-canonicalization for city domains.
- Remove deprecated CanonicalHandler component and streamline canonical URL handling.
- Introduce safe JSON-LD output to prevent XSS vulnerabilities.
- Add payment layout with appropriate metadata to prevent indexing.
- Conduct a comprehensive technical SEO audit and implement necessary fixes across the application.
2026-07-05 15:47:47 +03:30

212 lines
19 KiB
Markdown
Raw 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.
# ممیزی و رفع کامل Technical SEO با محوریت معماری چند-دامنه‌ای
## پروژه
`nobat724_front`
## نقش
تو یک متخصص ارشد Technical SEO با تسلط کامل بر Next.js 15 App Router (SSR/SSG/ISR/RSC/Streaming/PPR)، Metadata API، Core Web Vitals، Structured Data، Crawlability و International/Multi-domain SEO هستی. فقط از قابلیت‌های رسمی و غیرمنسوخ Next.js استفاده کن و آخرین Best Practiceهای Google Search و Vercel را مبنا قرار بده.
## زمینه
سایت نوبت‌دهی پزشکی «نوبت 724» یک deployment واحد Next.js 15 است که **ده‌ها دامنه شهری مستقل** را سرو می‌کند (مثل `arak-nobat.ir`، `yazd-nobat.ir`، ...) به‌علاوه دامنه اصلی `nobat724.com`. تشخیص شهر از روی `host` header انجام می‌شود (`lib/getStateInfo.js` سمت سرور، `context/ProvinceProvider.js` سمت کلاینت). منبع داده دامنه‌ها و متادیتای هر شهر `data/city.json` است (فیلدهای `domain`، `title`، `description`, `keywords`, `site_name`). زبان کل سایت فارسی، RTL، تقویم جلالی است. slug پزشک/کلینیک همان `uuid` است (`/doctor/${uuid}`).
وضعیت فعلی چند تناقض معماری جدی دارد که عملاً SEO دامنه‌های شهری را از کار می‌اندازد (شرح در «وضعیت فعلی»). هدف این پرامپت: ممیزی کامل، تصمیم‌گیری صریح درباره استراتژی چند-دامنه‌ای، و پیاده‌سازی اصلاحات.
## مشکل / هدف
1. تعیین تکلیف استراتژی canonical در معماری چند-دامنه‌ای (مهم‌ترین تصمیم — بقیه کارها به آن وابسته‌اند).
2. رفع مشکلات شناسایی‌شده در sitemap، robots، metadata، JSON-LD، status codeها و rendering.
3. ممیزی سیستماتیک بقیه حوزه‌ها (تصاویر، performance، pagination، accessibility، caching، internal linking) و رفع موارد یافت‌شده.
## فایل‌های مرتبط
| فایل | نقش |
|------|-----|
| `app/layout.js` | metadata سطح ریشه، canonical، JSON-LD سازمان/وب‌سایت، viewport، GA |
| `lib/getCanonicalUrl.js` | تولید canonical — **همه دامنه‌های شهری را به دامنه اصلی canonical می‌کند** |
| `components/CanonicalHandler.js` | تزریق canonical سمت کلاینت با DOM (آنتی‌پترن) |
| `middleware.js` | تزریق `x-pathname` برای canonical |
| `app/sitemap.js` + `utils/sitemap.js` | sitemap داینامیک per-domain |
| `app/robots.js` | robots per-domain + گیت `DEV_MODE` |
| `next.config.js` | security headers (CSP/HSTS/...)، images remotePatterns |
| `app/doctor/[slug]/page.js` | صفحه پزشک: `generateMetadata` + JSON-LD Physician |
| `app/clinic/[slug]/page.js` | صفحه کلینیک: مشابه پزشک (MedicalClinic) |
| `app/blog/[slug]/page.js` | صفحه بلاگ (Article) |
| `app/doctors/page.js`، `app/clinics/page.js` | لیست‌های فیلترشونده با searchParams |
| `app/page.js`، `app/specialties/page.js`، `app/blogs/page.js`، `app/about-us/page.js`، `app/contact-us/` | سایر صفحات عمومی |
| `data/city.json`، `data/state.json` | منبع دامنه/متادیتای شهرها |
| `lib/getStateInfo.js`، `lib/rootCity.js` | تشخیص شهر از host |
## وضعیت فعلی (یافته‌های ممیزی اولیه — از کد واقعی)
### ۱. Canonical همه دامنه‌های شهری را نابود می‌کند (Critical)
`lib/getCanonicalUrl.js` هر دامنه‌ای غیر از `nobat724.com` را به دامنه اصلی canonical می‌کند:
```js
// lib/getCanonicalUrl.js
if (normalizedHost === "nobat724.com") {
return null; // No canonical needed for main domain
}
// ...
if (matchedCity || normalizedHost !== "nobat724.com") {
return `${MAIN_DOMAIN}${cleanPathname}`; // MAIN_DOMAIN = "https://nobat724.com"
}
```
یعنی `arak-nobat.ir/doctors` به گوگل می‌گوید «نسخه اصلی من `nobat724.com/doctors` است» — هیچ دامنه شهری هرگز ایندکس/رتبه نمی‌گیرد. ضمناً:
- دامنه اصلی **هیچ self-canonical ندارد** (`return null`).
- شرط `matchedCity || normalizedHost !== "nobat724.com"` همیشه true است (شاخه دوم)، پس `matchedCity` بی‌اثر است.
- canonical فقط در layout ست می‌شود؛ صفحات dynamic (doctor/clinic/blog) که `generateMetadata` خودشان را دارند و `alternates` برنمی‌گردانند، متادیتای layout را override می‌کنند و **عملاً بدون canonical می‌مانند**.
- query stringها در canonical مدیریت نمی‌شوند (`x-pathname` فقط pathname است — صفحات پارامتری `/doctors?specialty=...` بدون canonical).
- `components/CanonicalHandler.js` با `useEffect` تگ canonical به DOM تزریق می‌کند — گوگل ممکن است ببیند یا نبیند؛ آنتی‌پترن و باید حذف شود.
### ۲. Sitemap (High)
```js
// app/sitemap.js
const NOW = new Date(); // module-level — در build/بوت ثابت می‌شود؛ lastmod جعلی
function getCurrentDomain() {
const headersList = headers(); // Next 15: باید await شود
...
}
```
- `lastModified` برای doctor/clinic همیشه `NOW` است (جعلی — گوگل lastmod غیرقابل‌اعتماد را کلاً نادیده می‌گیرد).
- sitemap هر دامنه شهری **همه** پزشکان/کلینیک‌های کشور را لیست می‌کند (fetch بدون فیلتر شهر) — در حالی که صفحات لیست همان دامنه به شهر فیلتر می‌شوند؛ تناقض با استراتژی چند-دامنه‌ای.
- `headers()` بدون `await` (در Next 15 deprecated و در نسخه‌های بعدی می‌شکند).
- fetch با `limit=2000` تک‌صفحه‌ای — بالای ۲۰۰۰ رکورد silent truncation.
- `utils/sitemap.js` دارای `escapeXml` و `createSitemapUrl` است که با Metadata API Route (`app/sitemap.js`) لازم نیستند (Next خودش escape می‌کند) — کد مرده/گمراه‌کننده.
### ۳. Robots (High)
```js
// app/robots.js
return {
rules: { userAgent: '*', allow: '/', disallow: ['/dashboard'] },
sitemap: `${baseUrl}/sitemap.xml`,
};
```
- `/panel`، `/login`، `/login-verify`، `/payment`، `/appointment` (فلوی رزرو کاربر) disallow نشده‌اند.
- `headers()` بدون `await`.
- در حالت `DEV_MODE=TRUE` مسیر sitemap هم حذف می‌شود (درست) ولی صفحات auth-gated فقط با robots بلاک می‌شوند، متای `noindex` per-page ندارند.
### ۴. Metadata سطح ریشه (High)
```js
// app/layout.js
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
```
- viewport با تگ دستی به‌جای `export const viewport` (روش رسمی Next 15).
- **`metadataBase` هیچ‌جا تعریف نشده** — در معماری چند-دامنه‌ای باید per-request از host ساخته شود تا URLهای نسبی OG/canonical درست resolve شوند.
- تصاویر OG/Twitter همه صفحات لوگوی `https://www.nobat724.com/assets/images/logo.png` است (با `www.` — در حالی که canonical بدون `www` است؛ ناسازگاری هاست).
- `keywords` meta استفاده شده (بی‌اثر برای گوگل؛ تصمیم بگیر نگه‌داری یا حذف).
- JSON-LD `Organization` و `WebSite` روی **همه دامنه‌ها** به `https://www.nobat724.com` اشاره می‌کند — روی `arak-nobat.ir` داده ساختاریافته متعلق به دامنه دیگر است. `SearchAction` هم فقط به دامنه اصلی.
### ۵. صفحات dynamic — soft 404 و metadata ناقص (High)
```js
// app/doctor/[slug]/page.js
const doctor = await getDoctor(slug);
if (!doctor) return {}; // metadata خالی
```
- وقتی پزشک پیدا نشود `generateMetadata` آبجکت خالی برمی‌گرداند؛ بررسی کن آیا کامپوننت صفحه `notFound()` صدا می‌زند یا با status 200 صفحه نیمه‌خالی رندر می‌شود (**soft 404**). الگوی درست: در صورت null بودن، `notFound()` در body صفحه.
- هیچ‌کدام از صفحات doctor/clinic/blog در `generateMetadata` خود `alternates.canonical` برنمی‌گردانند.
- ISR با `revalidate: 3600` وجود دارد (خوب) — ولی صحت `dynamicParams` و رفتار برای slugهای نامعتبر باید بررسی شود.
### ۶. صفحات لیستی و پارامتری (Medium)
`app/doctors/page.js` با `searchParams` رندر SSR می‌شود (force-dynamic ضمنی). صفحات فیلترشده (`?specialty=...&state=...`) hیچ canonical/robots مشخصی ندارند → ریسک ایندکس بی‌نهایت URL پارامتری duplicate. pagination (اگر با پارامتر `page` است) نه `rel prev/next` دارد نه canonical.
### ۷. Security headers — وضعیت خوب
`next.config.js` HSTS/X-Frame-Options/CSP/Referrer-Policy/Permissions-Policy دارد. فقط `'unsafe-inline' 'unsafe-eval'` در script-src از دید سختگیرانه ضعیف است — گزارش بده ولی تغییر آن اولویت SEO نیست.
## وظایف
### ۰. تصمیم استراتژی چند-دامنه‌ای (پیش‌نیاز همه‌چیز)
دو استراتژی ممکن را مقایسه و **گزینه A را پیاده‌سازی کن** (مگر اینکه در حین کار شواهدی خلافش پیدا کنی؛ در آن صورت قبل از ادامه به کاربر گزارش بده):
- **گزینه A — دامنه‌های شهری first-class:** هر دامنه شهری self-canonical دارد و مستقل ایندکس می‌شود. لازمه‌اش: canonical per-host، sitemap فیلترشده به همان شهر، JSON-LD با URL همان دامنه، محتوای متمایز per-city (که با title/description/فیلتر شهریِ موجود در `city.json` و لیست‌های فیلترشده فراهم است). این با معماری فعلی محصول (فیلتر خودکار شهر در `app/doctors/page.js` بر اساس دامنه) سازگار است.
- **گزینه B — تجمیع روی دامنه اصلی:** وضعیت فعلی canonical، ولی آنگاه وجود دامنه‌های شهری از نظر SEO بی‌معناست.
خروجی این وظیفه: بازنویسی `lib/getCanonicalUrl.js` به یک `buildCanonical(pathname)` که **همیشه** self-canonical روی هاست جاری برمی‌گرداند (بدون www، lowercase، بدون trailing slash، بدون query string به‌جز پارامترهای معنادار whitelisted). حذف کامل `components/CanonicalHandler.js` و همه usageهایش.
### ۱. `metadataBase` و canonical per-page
- در `app/layout.js` از host جاری `metadataBase` بساز:
```js
export async function generateMetadata() {
const headersList = await headers();
const host = (headersList.get("host") || "nobat724.com").toLowerCase().replace(/^www\./, "");
const metadataBase = new URL(`https://${host}`);
// ... alternates: { canonical: pathname } — با metadataBase به URL مطلق resolve می‌شود
}
```
- در `generateMetadata` تک‌تک صفحات (doctor/clinic/blog/doctors/clinics/specialties/blogs/about-us/contact-us/home) `alternates.canonical` نسبی اضافه کن (مثلاً `/doctor/${slug}`).
- صفحات پارامتری: canonical به نسخه بدون پارامتر (یا فقط با پارامترهای whitelisted مثل `specialty`).
- `viewport` را به `export const viewport` منتقل کن و تگ دستی را حذف کن.
### ۲. اصلاح sitemap
- `headers()` را `await` کن.
- sitemap هر دامنه شهری را به همان شهر فیلتر کن (همان پارامترهای `state`/`city` که `app/doctors/page.js` استفاده می‌کند)؛ دامنه اصلی همه را بگیرد. اگر API فیلتر شهر برای doctors/clinics دارد از همان استفاده کن — قرارداد را از `buildDoctorParams` در `helper` و `services/` استخراج کن.
- `lastModified` جعلی (`NOW`) را حذف کن: اگر فیلد تاریخ واقعی (`updated`/`created`) در پاسخ API هست استفاده کن، وگرنه `lastModified` را برای آن entry اصلاً نفرست.
- pagination فراخوانی API (به‌جای `limit=2000` تک‌صفحه) یا حداقل log هشدار در truncation.
- توابع بلااستفاده `escapeXml`/`createSitemapUrl` در `utils/sitemap.js` را حذف یا مستند کن.
### ۳. اصلاح robots
- `await headers()`.
- disallow: `/panel`, `/dashboard`, `/login`, `/login-verify`, `/payment`, `/appointment` (بررسی کن `appointment` صفحه عمومی SEO-دار نباشد — اگر فلوی رزرو شخصی است بلاک شود).
- به صفحات auth-gated (panel/dashboard/login) `robots: { index: false }` per-page اضافه کن (layout آن route group).
### ۴. JSON-LD چند-دامنه‌ای
- `Organization`/`WebSite` در `app/layout.js`: `url` و `SearchAction.target` را از host جاری بساز؛ `name` از `matchedCity.site_name`.
- صفحه doctor: schema `Physician` موجود را validate کن (فیلدهای `address`, `medicalSpecialty`, `image`, `url` مطلق روی دامنه جاری). clinic (`MedicalClinic`) و blog (`Article`) همین‌طور.
- `BreadcrumbList` به صفحات doctor/clinic/blog اضافه کن (خانه → لیست → آیتم) با URLهای دامنه جاری.
- خروجی JSON-LD را با `JSON.stringify(...).replace(/</g, '\\u003c')` یا sanitize موجود در `lib/sanitize.js` در برابر XSS امن کن.
### ۵. Soft 404 و status codes
- در `app/doctor/[slug]/page.js`، `app/clinic/[slug]/page.js`، `app/blog/[slug]/page.js`: اگر fetch نتیجه null داد `notFound()` صدا بزن (از `next/navigation`). `generateMetadata` در حالت null هم `robots: { index: false }` یا metadata حداقلی برگرداند.
- `app/not-found.js` و `app/error.js` را بررسی کن که متای noindex و status صحیح داشته باشند.
### ۶. ممیزی rendering و performance
- برای هر route مشخص کن الان static است یا dynamic (خروجی `npm run build` را بخوان). صفحات محتوایی (about-us, contact-us, specialties, blogs) نباید بی‌دلیل dynamic باشند — دقت کن `headers()` در layout همه‌چیز را dynamic می‌کند؛ این trade-off معماری چند-دامنه‌ای است، مستندش کن و جایی که ممکن است ISR per-route حفظ شود.
- LCP: تصویر hero/بنر صفحه اصلی `priority` داشته باشد؛ استفاده از `next/image` را در کامپوننت‌های اصلی (home, doctor card, doctor page) بررسی کن — هر `<img>` خام را گزارش و به `next/image` با `sizes` مناسب تبدیل کن (ابعاد مشخص → جلوگیری از CLS).
- فونت Vazir: `font-display: swap` موجود است؛ preload فایل woff2 اصلی را در layout اضافه کن.
- اسکریپت GA با `@next/third-parties` لود می‌شود (بهینه است — دست نزن).
### ۷. Pagination و صفحات پارامتری
- در لیست doctors/clinics: لینک‌های صفحه بعد/قبل باید `<a href>` واقعی قابل crawl باشند (نه فقط onClick). بررسی کن `components/doctors/` چطور pagination می‌سازد؛ در صورت client-only بودن، به `<Link>` با href پارامتردار تبدیل کن.
- صفحات با فیلترهای ترکیبی: `robots: { index: false, follow: true }` برای ترکیب‌های بیش از یک فیلتر، تا crawl budget هدر نرود (الگوی رایج سایت‌های listing).
### ۸. گزارش نهایی ممیزی
برای هر مشکل یافت‌شده/رفع‌شده گزارش بده با: عنوان، علت، تأثیر بر SEO، اولویت (Critical/High/Medium/Low)، راه‌حل اعمال‌شده، فایل‌های تغییرکرده. مواردی که عمداً تغییر ندادی (مثل `'unsafe-inline'` در CSP یا `keywords` meta) را با دلیل در بخش «بررسی شد — تغییر لازم نیست/تصمیم محصولی» لیست کن. حوزه‌های چک‌لیست که مشکلی نداشتند (mobile viewport، compress، poweredByHeader، HSTS و…) را هم یک‌خطی تأیید کن.
## نکات مهم
- **همه تغییرات باید نسبت به host جاری relative باشند** — هیچ URL هاردکد `nobat724.com` در metadata/JSON-LD/sitemap باقی نماند مگر آگاهانه (لوگوی fallback OG اشکالی ندارد ولی ترجیحاً از دامنه جاری سرو شود اگر asset موجود است).
- ناسازگاری `www.nobat724.com` (در تصاویر OG و JSON-LD) با `nobat724.com` (در canonical) را یکدست کن — نسخه بدون `www` مبنا.
- Next.js 15: `params`، `searchParams` و `headers()` همگی **باید await شوند**.
- hreflang کاربردی ندارد (همه دامنه‌ها fa-IR هستند) — به‌جایش تمایز محتوایی per-city ملاک است؛ hreflang اضافه نکن.
- `DEV_MODE=TRUE` رفتار noindex سراسری دارد — این مکانیزم را نشکن؛ در همه تغییرات robots/metadata حفظش کن.
- تست‌های موجود (`lib/getStateInfo.test.js`, `lib/multiDomainClient.test.js`, `services.test.js`, vitest) را بعد از تغییرات اجرا کن: `npm run test` (یا `npx vitest run`). برای `getCanonicalUrl` بازنویسی‌شده تست بنویس (هاست شهری، هاست اصلی، www، پورت dev، query string).
- تست دستی چند-دامنه‌ای: dev server با `HOST=yazd-nobat.localhost` بالا می‌آید (`npm run dev`)؛ برای دامنه دیگر `HOST` را موقتاً عوض کن. خروجی `curl -s http://yazd-nobat.localhost:3000 | grep -i canonical` و `/sitemap.xml` و `/robots.txt` را برای حداقل دو هاست مقایسه کن.
- بعد از اتمام، `npm run build` باید بدون خطا پاس شود و در خروجی build بررسی کن هیچ صفحه‌ای ناخواسته از static به dynamic (یا برعکس) جابه‌جا نشده باشد.
- استایل/زبان: همه رشته‌های جدید فارسی، RTL؛ کد مطابق الگوهای موجود پروژه (jsx، بدون TypeScript).