feat: add canonical URL handling and entity quality checks
- Implemented canonical URL strategies for city-specific domains and entities. - Added helper functions for domain and city resolution. - Created tests for canonical URL generation and domain resolution. - Introduced entity quality checks for doctors and clinics to ensure meaningful content. - Developed unique introductory texts for listing pages to avoid duplicate content. - Established robots.txt policies for listing pages to manage indexing based on user filters. - Enhanced specialty content with dynamic introductions and FAQs to improve SEO.
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
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 { safeJsonLd } from "@/lib/sanitize";
|
||||
import { fetchReq } from "@/lib/req";
|
||||
|
||||
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 میآید.
|
||||
async function getDoctors(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 {
|
||||
return { items: [], total: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }) {
|
||||
const { slug } = await params;
|
||||
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";
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
// 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 } = await params;
|
||||
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);
|
||||
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
|
||||
specialtyName={specialty.name}
|
||||
cityName={cityName}
|
||||
doctors={doctors}
|
||||
intro={intro}
|
||||
faq={faq}
|
||||
origin={origin}
|
||||
/>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SpecialtyDetail;
|
||||
@@ -3,9 +3,10 @@ import SpecialtiesPage from "@/components/specialties";
|
||||
import { getStateInfo } from "@/lib/getStateInfo";
|
||||
|
||||
export async function generateMetadata() {
|
||||
const { matchedCity } = await getStateInfo();
|
||||
const { matchedCity, isRoot } = await getStateInfo();
|
||||
const siteName = matchedCity?.site_name || "نوبت 724";
|
||||
const cityName = matchedCity?.name || "";
|
||||
// روی دامنهٔ ریشه نام رکورد («نوبت 724») برند است نه شهر — نباید در Title بنشیند.
|
||||
const cityName = isRoot ? "" : matchedCity?.name || "";
|
||||
const title = cityName
|
||||
? `تخصصهای پزشکی در ${cityName} | ${siteName}`
|
||||
: `تخصصهای پزشکی | ${siteName}`;
|
||||
|
||||
Reference in New Issue
Block a user