Files
nobat724_front/components/home/CityHighlights.js
T
hamed b494473e40 feat: رفع لینک /doctor/undefined و متمایزسازی صفحهٔ اصلی شهرها
- ایجاد پرامپت برای رفع خطای 404 ناشی از لینک `/doctor/undefined` در سایت‌های بهبهان و یاسوج.
- افزودن کامپوننت `CityHighlights` برای نمایش محتوای یکتا و آمار پزشکان و تخصص‌های پرمراجعه در صفحهٔ اصلی هر شهر.
- به‌روزرسانی تست‌های مربوط به کامپوننت‌های `ItemDoctor` و `CityHighlights` برای اطمینان از عدم وجود لینک‌های نامعتبر و نمایش صحیح اطلاعات.
- ایجاد سند جدید برای مستندسازی خطاهای Search Console که باگ نیستند و نیاز به رفع ندارند.
- افزودن تست‌های واحد برای توابع `buildCityIntro` و دیگر توابع مرتبط با تولید متن یکتا برای صفحات لیست.
2026-08-08 19:14:50 +03:30

139 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Link from "next/link";
import { getStateInfo } from "@/lib/getStateInfo";
import { fetchReq } from "@/lib/req";
import { buildCityIntro } from "@/lib/listingIntro";
import { isNoindexDoctor } from "@/lib/entityQuality";
const API_URL = process.env.NEXT_PUBLIC_API_URL;
const SHOWN_SPECIALTIES = 8;
const SHOWN_DOCTORS = 4;
/**
* بخش شهرمحور صفحهٔ اصلی — متن یکتا، آمار واقعی و تخصص‌های پرمراجعهٔ همان شهر.
*
* چرا لازم است: صفحهٔ اصلی تا پیش از این فقط `site_name` و `slogan` را شهری می‌کرد و
* متن رندرشدهٔ دو دامنه ۹۹٫۰٪ یکسان بود. گوگل canonicalِ درستِ ما را رد می‌کرد و
* صفحهٔ اصلی شهر را با دامنهٔ دیگر تجمیع می‌کرد. تمایز فقط با تگ حل نمی‌شود؛ محتوا
* باید واقعاً فرق کند، و آمارِ هر شهر خودبه‌خود فرق دارد.
*
* جدا از FrequentSearches است: آن فهرست ثابتِ تخصص‌هاست و روی همهٔ دامنه‌ها یکی؛ این
* یکی از داده‌ی همان شهر ساخته می‌شود.
*/
async function CityHighlights() {
const { matchedCity, isRoot } = await getStateInfo();
// دامنهٔ ریشه شهر نیست؛ آمار و متن شهری آنجا معنا ندارد.
if (isRoot || !matchedCity?.id) return null;
const [counts, doctors] = await Promise.all([
fetchCityCounts(matchedCity.id),
fetchCityDoctors(matchedCity.id),
]);
if (counts.length === 0) return null;
const doctorCount = counts.reduce((sum, s) => sum + s.number_of_doctors, 0);
if (doctorCount === 0) return null;
const top = counts.slice(0, SHOWN_SPECIALTIES);
const cityName = matchedCity.name;
return (
<section className="w-full max-w-[900px] z-[50]">
<p className="text-[#525252] text-[13px] md:text-[15px] font-normal leading-8 text-justify">
{buildCityIntro(cityName, doctorCount, top[0].name)}
</p>
<p className="mt-[20px] text-[#3B3B3B] text-[14px] md:text-[16px] font-bold">
تخصصهای پرمراجعه در {cityName}
</p>
<ul className="mt-[12px] flex flex-wrap items-center gap-[8px]">
{top.map((specialty) => (
<li key={specialty.id}>
<Link
href={`/specialties/${specialty.slug}`}
className="flex items-center gap-1.5 py-[6px] px-[12px] rounded-[4px] bg-[#FFF] border border-[#D7D7D7] text-[12px] lg:text-[14px] font-normal text-[#3B3B3B] hover:bg-[#EFEFEF] transition-all duration-500"
>
<span>{specialty.name}</span>
<span className="text-[#7E7E7E]">{specialty.number_of_doctors}</span>
</Link>
</li>
))}
</ul>
{doctors.length > 0 && (
<>
<p className="mt-[20px] text-[#3B3B3B] text-[14px] md:text-[16px] font-bold">
پزشکان {cityName}
</p>
<ul className="mt-[12px] flex flex-wrap items-center gap-[8px]">
{doctors.map((doctor) => (
<li key={doctor.uuid}>
<Link
href={`/doctor/${doctor.uuid}`}
className="flex items-center gap-1.5 py-[6px] px-[12px] rounded-[4px] bg-[#FFF] border border-[#D7D7D7] text-[12px] lg:text-[14px] font-normal text-[#3B3B3B] hover:bg-[#EFEFEF] transition-all duration-500"
>
<span>{doctor.display_name || doctor.name}</span>
{doctor.specialty && (
<span className="text-[#7E7E7E]">{doctor.specialty}</span>
)}
</Link>
</li>
))}
</ul>
</>
)}
</section>
);
}
/**
* تخصص‌های همان شهر که پزشک دارند، از پرپزشک به کم‌پزشک.
*
* خطا اینجا بلعیده می‌شود و آرایهٔ خالی برمی‌گردد: صفحهٔ اصلی مهم‌ترین صفحهٔ سایت است
* و قطعیِ API نباید سفیدش کند. این همان مرز واقعیِ error handling است — یک I/O boundary.
*/
/**
* چند پزشکِ همان شهر برای لینک مستقیم از صفحهٔ اصلی.
*
* بک‌اند خودش «دارای نوبت» را بالاتر مرتب می‌کند، پس ترتیب پیش‌فرض همان چیزی است که
* می‌خواهیم. پروفایل noindex کنار گذاشته می‌شود — همان سیاستی که sitemap اعمال
* می‌کند؛ لینک دادن از صفحهٔ اصلی به صفحه‌ای که خودمان از ایندکس بیرونش گذاشته‌ایم،
* سیگنال متناقض است.
*/
async function fetchCityDoctors(cityId) {
const data = await fetchReq(
`${API_URL}/api/v1/doctors?city_id=${cityId}&limit=12`
);
const items = data?.data ?? [];
if (!Array.isArray(items)) return [];
return items
.filter((d) => !isNoindexDoctor(d))
.slice(0, SHOWN_DOCTORS)
.map((d) => ({
uuid: d.uuid,
name: d.name,
display_name: d.display_name,
specialty: d.specialties?.[0]?.name ?? null,
}));
}
async function fetchCityCounts(cityId) {
const data = await fetchReq(
`${API_URL}/api/v1/specialties/doctor-counts?city_id=${cityId}`
);
const items = data?.data?.data ?? [];
if (!Array.isArray(items)) return [];
return items
.map((s) => ({ ...s, number_of_doctors: Number(s?.number_of_doctors) || 0 }))
.filter((s) => s.slug && s.number_of_doctors > 0)
.sort((a, b) => b.number_of_doctors - a.number_of_doctors);
}
export default CityHighlights;