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:
@@ -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
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user