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
+68 -17
View File
@@ -1,21 +1,72 @@
function Pageguide({ list }) {
import Link from "next/link";
import { safeJsonLd } from "@/lib/sanitize";
// Breadcrumb واحد سایت: ظاهر دقیقاً همان نسخهٔ قبلی (همان کلاس‌ها، همان جداکنندهٔ «>»)
// ولی ساختار سمانتیک درست (nav/ol/a) و BreadcrumbList از همان یک منبع داده تولید می‌شود
// تا نسخهٔ بصری و schema هرگز واگرا نشوند.
//
// list: Array<string | { name, href }> — آیتم آخر همیشه صفحهٔ جاری است (بدون لینک).
const ITEM_CLASS = "text-[16px] font-normal";
function normalize(list) {
return list
.map((item) => (typeof item === "string" ? { name: item } : item))
.filter((item) => item?.name);
}
function Pageguide({ list = [], origin }) {
const items = normalize(list);
if (items.length === 0) return null;
const jsonLd = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: items.map((item, idx) => ({
"@type": "ListItem",
position: idx + 1,
name: item.name,
...(item.href && origin && { item: `${origin}${item.href}` }),
})),
};
return (
<ul className="flex items-center justify-start gap-1">
{list.map((item, idx) => (
<li key={idx} className="flex items-center justify-start gap-1">
<h2
className={`text-[16px] font-normal ${ list.length > idx + 1 ? "text-[#9B9B9B]" : "text-[#525252]" } `}
>
{item}
</h2>
<span
className={` text-[#9B9B9B] text-[16px] font-normal ${list.length > idx + 1 ? "flex" : "hidden"} `}
>
{">"}
</span>
</li>
))}
</ul>
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: safeJsonLd(jsonLd) }}
/>
<nav aria-label="breadcrumb">
<ol className="flex items-center justify-start gap-1">
{items.map((item, idx) => {
const isLast = idx === items.length - 1;
const colorClass = isLast ? "text-[#525252]" : "text-[#9B9B9B]";
return (
<li key={idx} className="flex items-center justify-start gap-1">
{item.href && !isLast ? (
<Link href={item.href} className={`${ITEM_CLASS} ${colorClass}`}>
{item.name}
</Link>
) : (
<span
className={`${ITEM_CLASS} ${colorClass}`}
{...(isLast && { "aria-current": "page" })}
>
{item.name}
</span>
)}
<span
className={`text-[#9B9B9B] ${ITEM_CLASS} ${isLast ? "hidden" : "flex"}`}
aria-hidden="true"
>
{">"}
</span>
</li>
);
})}
</ol>
</nav>
</>
);
}
+5 -3
View File
@@ -1,12 +1,14 @@
function HeadPageList({ children, title, detail }) {
// titleTag: صفحاتی که h1 خودشان را جای دیگری دارند «p» می‌فرستند تا صفحه دو h1 نگیرد.
// کلاس‌ها مستقل از تگ‌اند، پس ظاهر در هر دو حالت یکسان است.
function HeadPageList({ children, title, detail, titleTag: TitleTag = "h1" }) {
return (
<div className="bg-head-menu !bg-no-repeat !bg-cover !bg-center">
<div
className=" padding-responsive flex flex-col items-center gap-[25px] pt-[calc(12px_+_75px_+_32px)] sm:pt-[calc(21px_+_75px_+_45px)] md:pt-[calc(30px_+_75px_+_58px)] lg:pt-[calc(40px_+_75px_+_78px)] pb-[62px] sm:pb-[69px] md:pb-[76px] lg:pb-[85px] "
>
<h1 className="text-[#3B3B3B] text-[16px] sm:text-[19px] md:text-[22px] lg:text-[24px] text-center font-bold">
<TitleTag className="text-[#3B3B3B] text-[16px] sm:text-[19px] md:text-[22px] lg:text-[24px] text-center font-bold">
{title}
</h1>
</TitleTag>
<p className="text-[#3B3B3B] text-[14px] md:text-[16px] text-center font-normal">
{detail}
</p>