diff --git a/app/sitemap.js b/app/sitemap.js index 0750293..83fe327 100644 --- a/app/sitemap.js +++ b/app/sitemap.js @@ -158,35 +158,21 @@ async function getClinicUrls(baseUrl, scope) { // صفحات فرود تخصص per-domain — روی هر دامنه «تخصص + آن شهر» است، پس همه‌جا self. // -// فقط تخصص‌هایی که در همین دامنه واقعاً پزشک دارند اعلام می‌شوند؛ تولید بی‌قید هر ۹۳ -// تخصص برای هر ۳۵ دامنه هزاران صفحهٔ خالی به گوگل معرفی می‌کرد (thin content انبوه). +// هر تخصص ایندکس‌پذیر روی هر دامنه اعلام می‌شود، مستقل از اینکه امروز در آن شهر پزشک +// دارد یا نه — همان سیاستی که صفحه اعمال می‌کند. اگر sitemap به تعداد پزشکان گره +// می‌خورد، صفحهٔ ایندکس‌پذیرِ اعلام‌نشده می‌ساختیم و با هر افت و خیز داده، URLها از +// sitemap در می‌آمدند و برمی‌گشتند. // -// منبع، همان پزشکانی است که این sitemap منتشرشان می‌کند — نه endpoint شمارش. آن -// endpoint با فیلتر لیست پزشکان اختلاف دارد (مثلاً پزشکِ یافت‌شده از آدرس کلینیک را -// نمی‌شمارد)، و اگر sitemap از یک منبع و خودِ صفحه از منبع دیگر تصمیم بگیرد، صفحهٔ -// ایندکس‌پذیرِ اعلام‌نشده یا برعکس می‌سازیم. این‌طور هیچ فراخوانی اضافه‌ای هم ندارد. -function getSpecialtyUrls(baseUrl, doctors) { - // والدها noindex هستند (lib/specialtyIndexing)؛ اعلامشان در sitemap سیگنال - // متناقض به گوگل می‌دهد، پس اصلاً منتشر نمی‌شوند. - const slugById = new Map( - specialtiesData - .filter((s) => s?.slug && s.status === 1 && isIndexableSpecialty(s)) - .map((s) => [String(s.id), s.slug]) - ); - - const slugs = new Set(); - for (const doctor of doctors) { - for (const specialty of doctor?.specialties ?? []) { - const slug = slugById.get(String(specialty?.id)); - if (slug) slugs.add(slug); - } - } - - return [...slugs].map((slug) => ({ - url: `${baseUrl}/specialties/${slug}`, - changeFrequency: 'weekly', - priority: 0.8, - })); +// دسته‌های سطح‌بالا (lib/specialtyIndexing) noindex هستند؛ اعلامشان سیگنال متناقض +// به گوگل می‌دهد، پس منتشر نمی‌شوند. +function getSpecialtyUrls(baseUrl) { + return specialtiesData + .filter((s) => s?.slug && s.status === 1 && isIndexableSpecialty(s)) + .map((s) => ({ + url: `${baseUrl}/specialties/${s.slug}`, + changeFrequency: 'weekly', + priority: 0.8, + })); } // هر پست فقط در sitemap دامنهٔ canonical خودش: پست شهریافته → دامنهٔ آن شهر، @@ -231,7 +217,7 @@ export default async function sitemap() { const allUrls = [ ...getStaticPages(baseUrl), - ...getSpecialtyUrls(baseUrl, doctors), + ...getSpecialtyUrls(baseUrl), ...doctorUrls, ...clinicUrls, ...blogUrls, diff --git a/app/specialties/[slug]/page.js b/app/specialties/[slug]/page.js index ec893c0..a3f618f 100644 --- a/app/specialties/[slug]/page.js +++ b/app/specialties/[slug]/page.js @@ -54,14 +54,13 @@ export async function generateMetadata({ params }) { const description = `لیست بهترین پزشکان ${specialty.name} در ${cityName} همراه با نشانی مطب، ساعات کاری و امتیاز بیماران. رزرو نوبت آنلاین متخصص ${specialty.name}.`; const image = "/assets/images/og-image.png"; - // تخصصی که در این شهر پزشکی ندارد فقط یک عنوان و متن قالبی است — thin content. - // ایندکس نمی‌شود (و در sitemap هم نمی‌آید)، ولی follow می‌ماند تا لینک‌های - // داخلی‌اش دنبال شوند. به‌محض افزوده‌شدن اولین پزشک، خودکار ایندکس‌پذیر می‌شود. - const { total } = await getDoctors(specialty.id, isRoot ? null : matchedCity?.id); - - // دستهٔ سطح‌بالا (دارای زیرشاخهٔ فعال) محتوای مستقلی ندارد و با فرزندانش روی همان - // کوئری رقابت می‌کند؛ صفحهٔ قابل‌ایندکس، همان زیرشاخه است. - const noindex = isCategorySpecialty(specialty) || total === 0; + // ایندکس به تعداد پزشکان شهر گره نمی‌خورد: صفحه روی هر دامنه متن مقدمه و FAQ یکتای + // خودش را دارد (buildSpecialtyIntro با هشِ تخصص+شهر واگرا می‌شود) و شهری که امروز + // پزشک ندارد فردا دارد — نوسان ایندکس بدتر از صفحهٔ کم‌پزشک است. + // + // تنها استثنا دستهٔ سطح‌بالاست (دارای زیرشاخهٔ فعال): محتوای مستقلی ندارد و با + // فرزندانش روی همان کوئری رقابت می‌کند؛ صفحهٔ قابل‌ایندکس، همان زیرشاخه است. + const noindex = isCategorySpecialty(specialty); return { title,