# ممیزی و رفع کامل 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
``` - 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(/` خام را گزارش و به `next/image` با `sizes` مناسب تبدیل کن (ابعاد مشخص → جلوگیری از CLS). - فونت Vazir: `font-display: swap` موجود است؛ preload فایل woff2 اصلی را در layout اضافه کن. - اسکریپت GA با `@next/third-parties` لود میشود (بهینه است — دست نزن). ### ۷. Pagination و صفحات پارامتری - در لیست doctors/clinics: لینکهای صفحه بعد/قبل باید `` واقعی قابل crawl باشند (نه فقط onClick). بررسی کن `components/doctors/` چطور pagination میسازد؛ در صورت client-only بودن، به `` با 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).