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
@@ -0,0 +1,33 @@
"use client";
import { useState } from "react";
import ItemDoctor from "@/app/component/ItemDoctor";
// همان گرید و همان کارت پزشکِ /doctors — فقط بدون ماشین فیلتر، چون این صفحه
// از قبل روی «تخصص + شهر» فیلتر شده و لیستش SSR می‌آید.
function DoctorGrid({ doctors: initialDoctors = [] }) {
const [doctors, setDoctors] = useState(initialDoctors);
if (!doctors.length) {
return (
<p className="text-center py-10 text-gray-500">
دکتری برای نمایش وجود ندارد.
</p>
);
}
return (
<ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-[24px]">
{doctors.map((doctor, idx) => (
<ItemDoctor
key={doctor.id}
doctor={doctor}
priority={idx < 3}
setDoctors={setDoctors}
/>
))}
</ul>
);
}
export default DoctorGrid;
+30
View File
@@ -0,0 +1,30 @@
// بلوک سوالات متداول — همان منبع دادهٔ FAQPage schema، تا متن دیده‌شده و schema واگرا نشوند.
// استایل با کارت‌های موجود سایت هم‌خانواده است (bg سفید، rounded-lg، border روشن).
function Faq({ items = [] }) {
if (!items.length) return null;
return (
<section className="mt-[48px]">
<h2 className="text-[#3B3B3B] text-[16px] sm:text-[19px] md:text-[22px] lg:text-[24px] font-bold mb-[24px]">
سوالات متداول
</h2>
<div className="flex flex-col gap-[16px]">
{items.map((item) => (
<details
key={item.question}
className="p-4 rounded-lg bg-[#FFF] border border-[#EFEFEF]"
>
<summary className="text-[#3B3B3B] text-[16px] font-bold cursor-pointer">
{item.question}
</summary>
<p className="text-[#525252] text-[14px] md:text-[16px] font-normal mt-[12px] leading-8">
{item.answer}
</p>
</details>
))}
</div>
</section>
);
}
export default Faq;
+36
View File
@@ -0,0 +1,36 @@
import Pageguide from "@/app/component/Pageguide";
import DoctorGrid from "./DoctorGrid";
import Faq from "./Faq";
// چیدمان عمداً هم‌خانوادهٔ /doctors است: همان padding-responsive، همان فاصلهٔ بالای صفحه،
// همان تایپوگرافی عنوان و همان گرید کارت پزشک — کاربر نباید حس کند وارد بخش دیگری شده.
function SpecialtyDetailPage({ specialtyName, cityName, doctors, intro, faq, origin }) {
return (
<div className="pt-[95px] sm:pt-[120px] padding-responsive">
<Pageguide
origin={origin}
list={[
{ name: "خانه", href: "/" },
{ name: "تخصص‌ها", href: "/specialties" },
{ name: specialtyName },
]}
/>
<h1 className="text-[#3B3B3B] text-[16px] sm:text-[19px] md:text-[22px] lg:text-[24px] text-center font-bold mt-[24px]">
متخصص {specialtyName} در {cityName}
</h1>
<p className="text-[#525252] text-[14px] md:text-[16px] font-normal leading-8 mt-[16px] text-justify">
{intro}
</p>
<div className="mt-[24px] sm:mt-[37px] md:mt-[50px] lg:mt-[64px]">
<DoctorGrid doctors={doctors} />
</div>
<Faq items={faq} />
</div>
);
}
export default SpecialtyDetailPage;
+1
View File
@@ -29,6 +29,7 @@ function SpecialtiesPage({ cityId, cityName, stateName }) {
return (
<>
<HeadPageList
titleTag="p"
title="تخصص مورد نظر شما چیست؟"
detail="تخصص مورد نظرتان را جستجو کرده و یا از لیست زیر انتخاب نمایید:"
>
+1 -1
View File
@@ -7,7 +7,7 @@ function List({ specialties = [], cityName, stateName }) {
<div className="flex items-center justify-center gap-[12px] pt-[24px] mb-[40px]">
<MenuS />
<h1 className="text-[#3B3B3B] text-[16px] sm:text-[19px] md:text-[22px] lg:text-[24px] font-bold">
لیست تخصص ها
لیست تخصصهای پزشکی در {cityName || "ایران"}
</h1>
</div>
{specialties.length === 0 ? (