- Removed the newly added city-focused section from the homepage. - Fixed 404 error for Persian slugs in specialties and blog pages by implementing a decode helper. - Removed the "مخصوص شهر" label from article headers. - Linked article tags to their respective filter pages. - Adjusted related content images to prevent distortion. - Added a section in the "About Us" page for physician registration guidance. - Updated the logo in the "About Us" header to the correct version. - Added tests for the new functionality and ensured existing tests are updated accordingly.
156 lines
6.0 KiB
JavaScript
156 lines
6.0 KiB
JavaScript
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 (
|
|
<>
|
|
<script
|
|
type="application/ld+json"
|
|
dangerouslySetInnerHTML={{ __html: safeJsonLd(pageJsonLd) }}
|
|
/>
|
|
<script
|
|
type="application/ld+json"
|
|
dangerouslySetInnerHTML={{ __html: safeJsonLd(faqJsonLd) }}
|
|
/>
|
|
<Layout name="/specialties">
|
|
<SpecialtyDetailPage
|
|
specialty={specialty}
|
|
specialtyName={specialty.name}
|
|
cityName={cityName}
|
|
doctors={doctors}
|
|
intro={intro}
|
|
faq={faq}
|
|
origin={origin}
|
|
slug={slug}
|
|
/>
|
|
</Layout>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default SpecialtyDetail;
|