feat: update specialty URL generation logic and improve indexing strategy for specialties

This commit is contained in:
hamed
2026-07-20 11:54:03 +03:30
parent 30c3fe8637
commit 984511653b
2 changed files with 22 additions and 37 deletions
+15 -29
View File
@@ -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,
+7 -8
View File
@@ -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,