Fix community IDs and update sitemap functionality

- Updated community IDs for various components in graph.json to reflect correct associations.
- Added new function `getPublishedDoctors()` in app/sitemap.js and established relationships with existing functions.
- Adjusted source locations for several functions in app/sitemap.js to ensure accurate mapping.
- Enhanced canonical URL handling in lib/getCanonicalUrl.js to prevent incorrect canonicalization for paginated specialty pages.
- Updated manifest.json with new modification times and AST hashes for affected files.
This commit is contained in:
hamed
2026-07-19 09:32:00 +03:30
parent 97e990982f
commit 672a7d89bf
8 changed files with 332 additions and 304 deletions
+50 -23
View File
@@ -113,23 +113,28 @@ function cityFilterParams(scope) {
// فیلتری انجام می‌شود که کلینیک‌ها استفاده می‌کنند. پیش از این، نبودِ city مجبورمان
// می‌کرد روی دامنهٔ اصلی کل لیست را ۳۵ بار (یک‌بار به‌ازای هر شهرِ دامنه‌دار) بگیریم و
// تفاضل بگیریم — همان کاری که ~۱۳ ثانیه طول می‌کشید.
async function getDoctorUrls(baseUrl, scope) {
// پزشکانی که این دامنه منتشرشان می‌کند. هم URLهای پزشک و هم صفحات تخصص از همین
// مجموعه ساخته می‌شوند تا نتوانند واگرا شوند.
async function getPublishedDoctors(scope) {
const doctors = await fetchAllPages('/api/v1/doctors', cityFilterParams(scope));
return doctors
.filter((d) => !isThinDoctor(d))
// روی دامنهٔ اصلی فقط پزشکانی که شهرشان دامنهٔ اختصاصی ندارد؛ روی دامنهٔ شهری
// فیلتر city_id سمت API قبلاً کار را کرده است.
.filter((d) => !scope.isRoot || !findDomainByCityId(extractEntityCityId(d)))
.map((d) =>
withLastModified(
{
url: `${baseUrl}/doctor/${d.uuid}`,
changeFrequency: 'weekly',
priority: 0.8,
},
d.updated || d.created
)
);
.filter((d) => !scope.isRoot || !findDomainByCityId(extractEntityCityId(d)));
}
function getDoctorUrls(baseUrl, doctors) {
return doctors.map((d) =>
withLastModified(
{
url: `${baseUrl}/doctor/${d.uuid}`,
changeFrequency: 'weekly',
priority: 0.8,
},
d.updated || d.created
)
);
}
async function getClinicUrls(baseUrl, scope) {
@@ -151,14 +156,34 @@ async function getClinicUrls(baseUrl, scope) {
}
// صفحات فرود تخصص per-domain — روی هر دامنه «تخصص + آن شهر» است، پس همه‌جا self.
function getSpecialtyUrls(baseUrl) {
return specialtiesData
.filter((s) => s?.slug && s.status === 1)
.map((s) => ({
url: `${baseUrl}/specialties/${s.slug}`,
changeFrequency: 'weekly',
priority: 0.8,
}));
//
// فقط تخصص‌هایی که در همین دامنه واقعاً پزشک دارند اعلام می‌شوند؛ تولید بی‌قید هر ۹۳
// تخصص برای هر ۳۵ دامنه هزاران صفحهٔ خالی به گوگل معرفی می‌کرد (thin content انبوه).
//
// منبع، همان پزشکانی است که این sitemap منتشرشان می‌کند — نه endpoint شمارش. آن
// endpoint با فیلتر لیست پزشکان اختلاف دارد (مثلاً پزشکِ یافت‌شده از آدرس کلینیک را
// نمی‌شمارد)، و اگر sitemap از یک منبع و خودِ صفحه از منبع دیگر تصمیم بگیرد، صفحهٔ
// ایندکس‌پذیرِ اعلام‌نشده یا برعکس می‌سازیم. این‌طور هیچ فراخوانی اضافه‌ای هم ندارد.
function getSpecialtyUrls(baseUrl, doctors) {
const slugById = new Map(
specialtiesData
.filter((s) => s?.slug && s.status === 1)
.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,
}));
}
// هر پست فقط در sitemap دامنهٔ canonical خودش: پست شهریافته → دامنهٔ آن شهر،
@@ -193,15 +218,17 @@ export default async function sitemap() {
const baseUrl = getBaseUrl(domain);
const scope = getCityScope(domain);
const [doctorUrls, clinicUrls, blogUrls] = await Promise.all([
getDoctorUrls(baseUrl, scope),
const [doctors, clinicUrls, blogUrls] = await Promise.all([
getPublishedDoctors(scope),
getClinicUrls(baseUrl, scope),
getBlogUrls(baseUrl, scope),
]);
const doctorUrls = getDoctorUrls(baseUrl, doctors);
const allUrls = [
...getStaticPages(baseUrl),
...getSpecialtyUrls(baseUrl),
...getSpecialtyUrls(baseUrl, doctors),
...doctorUrls,
...clinicUrls,
...blogUrls,
+10 -2
View File
@@ -1,3 +1,4 @@
import { cache } from "react";
import { notFound } from "next/navigation";
import Layout from "@/components/layout/StLayout";
import SpecialtyDetailPage from "@/components/specialties/detail";
@@ -16,7 +17,8 @@ const findSpecialty = (slug) =>
specialtiesData.find((item) => item.slug === slug && item.status === 1) ?? null;
// روی هر دامنهٔ شهری این صفحه خودکار «تخصص + همان شهر» است — شهر از host می‌آید.
async function getDoctors(specialtyId, cityId) {
// cache تا generateMetadata و بدنهٔ صفحه یک بار fetch کنند، نه دو بار.
const getDoctors = cache(async (specialtyId, cityId) => {
try {
const res = await fetchReq(`${API_URL}/api/v1/doctors`, {
params: {
@@ -33,7 +35,7 @@ async function getDoctors(specialtyId, cityId) {
} catch {
return { items: [], total: 0 };
}
}
});
export async function generateMetadata({ params }) {
const { slug } = await params;
@@ -48,9 +50,15 @@ 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);
return {
title,
description,
...(total === 0 && { robots: { index: false, follow: true } }),
// C1-a — صفحهٔ فرود تخصص per-domain است؛ canonical لایهٔ layout (self) درست است.
openGraph: { title, description, images: [image] },
twitter: { card: "summary_large_image", title, description, images: [image] },