- 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.
19 KiB
ممیزی و رفع کامل 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 دامنههای شهری را از کار میاندازد (شرح در «وضعیت فعلی»). هدف این پرامپت: ممیزی کامل، تصمیمگیری صریح درباره استراتژی چند-دامنهای، و پیادهسازی اصلاحات.
مشکل / هدف
- تعیین تکلیف استراتژی canonical در معماری چند-دامنهای (مهمترین تصمیم — بقیه کارها به آن وابستهاند).
- رفع مشکلات شناساییشده در sitemap، robots، metadata، JSON-LD، status codeها و rendering.
- ممیزی سیستماتیک بقیه حوزهها (تصاویر، 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 بلاک میشوند، متایnoindexper-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است؛ ناسازگاری هاست). keywordsmeta استفاده شده (بیاثر برای گوگل؛ تصمیم بگیر نگهداری یا حذف).- 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).