Files
nobat724_front/.claude/prompt/technical-seo-audit-multidomain.md
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

19 KiB
Raw Permalink Blame History

ممیزی و رفع کامل 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 می‌کند:

// 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)

// 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)

// 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)

// 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)

// 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 بساز:
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).