import { cache } from "react"; import { notFound } from "next/navigation"; import Layout from "@/components/layout/StLayout"; import SpecialtyDetailPage from "@/components/specialties/detail"; import specialtiesData from "@/data/specialties.json"; import { getStateInfo } from "@/lib/getStateInfo"; import { getRequestOrigin } from "@/lib/getCanonicalUrl"; import { resolveCityDisplayName } from "@/lib/domainHelpers"; import { buildSpecialtyFaq, buildSpecialtyIntro } from "@/lib/specialtyContent"; import { isCategorySpecialty } from "@/lib/specialtyIndexing"; import { safeJsonLd } from "@/lib/sanitize"; import { fetchReq } from "@/lib/req"; import { decodeRouteParam } from "@/lib/routeParams"; import { isNextRedirectError } from "@/lib/maintenance"; const API_URL = process.env.NEXT_PUBLIC_API_URL; const DOCTOR_LIMIT = 12; const findSpecialty = (slug) => specialtiesData.find((item) => item.slug === slug && item.status === 1) ?? null; // روی هر دامنهٔ شهری این صفحه خودکار «تخصص + همان شهر» است — شهر از host می‌آید. // cache تا generateMetadata و بدنهٔ صفحه یک بار fetch کنند، نه دو بار. const getDoctors = cache(async (specialtyId, cityId) => { try { const res = await fetchReq(`${API_URL}/api/v1/doctors`, { params: { specialty_id: specialtyId, ...(cityId && { city_id: cityId }), page: 1, limit: DOCTOR_LIMIT, }, }); return { items: res?.data?.data ?? res?.data ?? [], total: Number(res?.data?.meta?.totalRecords ?? res?.meta?.totalRecords ?? 0), }; } catch (error) { // ریدایرکت نکست با throw کار می‌کند؛ بدون این rethrow، ریدایرکت حالت تعمیرات بلعیده می‌شود. if (isNextRedirectError(error)) throw error; return { items: [], total: 0 }; } }); export async function generateMetadata({ params }) { const { slug: rawSlug } = await params; const slug = decodeRouteParam(rawSlug); const specialty = findSpecialty(slug); if (!specialty) notFound(); const { matchedCity, isRoot } = await getStateInfo(); const siteName = matchedCity?.site_name || "نوبت 724"; const cityName = resolveCityDisplayName(isRoot ? null : matchedCity); const title = `متخصص ${specialty.name} در ${cityName} | رزرو نوبت آنلاین | ${siteName}`; const description = `لیست بهترین پزشکان ${specialty.name} در ${cityName} همراه با نشانی مطب، ساعات کاری و امتیاز بیماران. رزرو نوبت آنلاین متخصص ${specialty.name}.`; const image = "/assets/images/og-image.png"; // ایندکس به تعداد پزشکان شهر گره نمی‌خورد: صفحه روی هر دامنه متن مقدمه و FAQ یکتای // خودش را دارد (buildSpecialtyIntro با هشِ تخصص+شهر واگرا می‌شود) و شهری که امروز // پزشک ندارد فردا دارد — نوسان ایندکس بدتر از صفحهٔ کم‌پزشک است. // // تنها استثنا دستهٔ سطح‌بالاست (دارای زیرشاخهٔ فعال): محتوای مستقلی ندارد و با // فرزندانش روی همان کوئری رقابت می‌کند؛ صفحهٔ قابل‌ایندکس، همان زیرشاخه است. const noindex = isCategorySpecialty(specialty); return { title, description, ...(noindex && { 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] }, }; } async function SpecialtyDetail({ params }) { const { slug: rawSlug } = await params; const slug = decodeRouteParam(rawSlug); const specialty = findSpecialty(slug); if (!specialty) notFound(); const { matchedCity, isRoot } = await getStateInfo(); const origin = await getRequestOrigin(); const cityName = resolveCityDisplayName(isRoot ? null : matchedCity); const { items: doctors, total } = await getDoctors( specialty.id, isRoot ? null : matchedCity?.id ); const intro = buildSpecialtyIntro(specialty.name, cityName, total); const faq = buildSpecialtyFaq(specialty.name, cityName, total, { isRoot }); const pageUrl = `${origin}/specialties/${specialty.slug}`; const pageJsonLd = { "@context": "https://schema.org", "@type": "MedicalWebPage", "@id": `${pageUrl}#webpage`, url: pageUrl, name: `متخصص ${specialty.name} در ${cityName}`, description: intro.slice(0, 300), about: { "@type": "MedicalSpecialty", name: specialty.name }, ...(doctors.length > 0 && { mainEntity: { "@type": "ItemList", numberOfItems: doctors.length, itemListElement: doctors.map((doctor, idx) => ({ "@type": "ListItem", position: idx + 1, url: `${origin}/doctor/${doctor.uuid}`, name: doctor.name, })), }, }), }; // FAQPage عمداً یک script مستقل است تا اسکیماهای موجود صفحه را نشکند. const faqJsonLd = { "@context": "https://schema.org", "@type": "FAQPage", mainEntity: faq.map((item) => ({ "@type": "Question", name: item.question, acceptedAnswer: { "@type": "Answer", text: item.answer }, })), }; return ( <>