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:
hamed
2026-07-19 07:52:50 +03:30
parent 36816eded2
commit fd48b48613
48 changed files with 3951 additions and 733 deletions
-18
View File
@@ -1,18 +0,0 @@
import { Skeleton } from "@mui/material";
export default function Loading() {
return (
<div className="pt-[40px] sm:pt-[70px] md:pt-[100px] lg:pt-[136px] mt-[40px] padding-responsive">
<div className="flex gap-6">
<Skeleton variant="rounded" width={100} height={32} />
<Skeleton variant="rounded" width={100} height={32} />
<Skeleton variant="rounded" width={100} height={32} />
</div>
<div className="mt-[24px] sm:mt-[27px] md:mt-[30px] lg:mt-[32px] flex flex-col gap-4">
<Skeleton variant="rounded" height={200} className="!rounded-lg" />
<Skeleton variant="rounded" height={20} className="!w-full" />
<Skeleton variant="rounded" height={20} width={300} />
</div>
</div>
);
}
+22 -22
View File
@@ -4,7 +4,9 @@ import Layout from "@/components/layout/StLayout";
import { notFound } from "next/navigation";
import { fetchReq } from "@/lib/req";
import { getStateInfo } from "@/lib/getStateInfo";
import { getRequestOrigin } from "@/lib/getCanonicalUrl";
import { getEntityOrigin } from "@/lib/getCanonicalUrl";
import { extractEntityCityId } from "@/lib/domainHelpers";
import { isThinClinic } from "@/lib/entityQuality";
import { safeJsonLd } from "@/lib/sanitize";
import { imageUrl } from "@/helper";
@@ -38,9 +40,15 @@ export async function generateMetadata({ params }) {
? [imageUrl(clinic.images_clinic[0].url)]
: ["/assets/images/og-image.png"];
// C1-b — کلینیک به دامنهٔ شهر خودش canonical می‌شود.
const origin = await getEntityOrigin(extractEntityCityId(clinic));
return {
title,
description,
alternates: { canonical: `${origin}/clinic/${clinic.uuid}` },
// H7 — کلینیک غیرفعال/بی‌محتوا نباید ایندکس شود (تقارن با پزشک بی‌محتوا)
...(isThinClinic(clinic) && { robots: { index: false, follow: true } }),
openGraph: {
title,
description,
@@ -54,7 +62,8 @@ export async function generateMetadata({ params }) {
images: image,
},
};
} catch {
} catch (error) {
console.error(`[clinic/${slug}] generateMetadata failed:`, error);
return {};
}
}
@@ -74,13 +83,15 @@ function computeClinicRating(doctors) {
async function Clinic({ params, searchParams }) {
const { slug } = await params;
const origin = await getRequestOrigin();
const sp = await searchParams;
const page = Number(sp?.page) || 1;
const limit = 50;
const clinic = await getClinic(slug);
if (!clinic) notFound();
// همان مقصد canonical — تا url/@id و breadcrumb سیگنال متناقض ندهند.
const origin = await getEntityOrigin(extractEntityCityId(clinic));
const reqDoctors = await fetchReq(
`${API_URL}/api/v1/clinic/doctor-list/${slug}?page=${page}&limit=${limit}`
);
@@ -149,18 +160,6 @@ async function Clinic({ params, searchParams }) {
}
: null;
const breadcrumbJsonLd = clinic
? {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
{ "@type": "ListItem", position: 1, name: "خانه", item: origin },
{ "@type": "ListItem", position: 2, name: "کلینیک‌ها", item: `${origin}/clinics` },
{ "@type": "ListItem", position: 3, name: clinic.title },
],
}
: null;
return (
<>
{jsonLd && (
@@ -169,14 +168,15 @@ async function Clinic({ params, searchParams }) {
dangerouslySetInnerHTML={{ __html: safeJsonLd(jsonLd) }}
/>
)}
{breadcrumbJsonLd && (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: safeJsonLd(breadcrumbJsonLd) }}
/>
)}
{/* BreadcrumbList از خودِ کامپوننت Pageguide می‌آید — یک منبع داده برای بصری و schema */}
<Layout>
<ClinicPage data={clinic} doctors={doctors} slug={slug} pages={pagedoctors} />
<ClinicPage
data={clinic}
doctors={doctors}
slug={slug}
pages={pagedoctors}
origin={origin}
/>
</Layout>
</>
);