feat: enhance SEO for listing and specialty pages, fix search autocomplete issue

This commit is contained in:
hamed
2026-07-20 10:26:03 +03:30
parent 9011b230b3
commit 30c3fe8637
14 changed files with 460 additions and 69 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ export async function generateMetadata({ searchParams }) {
return {
title,
description,
...listingRobots(awaitedParams, { matchedCity, matchedState }),
...listingRobots(awaitedParams),
openGraph: { title, description, images: [image] },
twitter: { card: "summary_large_image", title, description, images: [image] },
};
+1 -1
View File
@@ -22,7 +22,7 @@ export async function generateMetadata({ searchParams }) {
return {
title,
description,
...listingRobots(awaitedParams, { matchedCity, matchedState }),
...listingRobots(awaitedParams),
openGraph: { title, description, images: [image] },
twitter: { card: "summary_large_image", title, description, images: [image] },
};
+4 -1
View File
@@ -6,6 +6,7 @@ import { isRootCity } from '@/lib/rootCity';
import { extractEntityCityId, findDomainByCityId } from '@/lib/domainHelpers';
import { isThinClinic, isThinDoctor } from '@/lib/entityQuality';
import specialtiesData from '@/data/specialties.json';
import { isIndexableSpecialty } from '@/lib/specialtyIndexing';
// sitemap باید در زمان اجرا (per-request) روی سرور production تولید شود، نه در build.
// در build کانتینر به API دسترسی شبکه ندارد → fetch failed. force-dynamic این را قطعی می‌کند.
@@ -165,9 +166,11 @@ async function getClinicUrls(baseUrl, scope) {
// نمی‌شمارد)، و اگر sitemap از یک منبع و خودِ صفحه از منبع دیگر تصمیم بگیرد، صفحهٔ
// ایندکس‌پذیرِ اعلام‌نشده یا برعکس می‌سازیم. این‌طور هیچ فراخوانی اضافه‌ای هم ندارد.
function getSpecialtyUrls(baseUrl, doctors) {
// والدها noindex هستند (lib/specialtyIndexing)؛ اعلامشان در sitemap سیگنال
// متناقض به گوگل می‌دهد، پس اصلاً منتشر نمی‌شوند.
const slugById = new Map(
specialtiesData
.filter((s) => s?.slug && s.status === 1)
.filter((s) => s?.slug && s.status === 1 && isIndexableSpecialty(s))
.map((s) => [String(s.id), s.slug])
);
+7 -2
View File
@@ -7,6 +7,7 @@ import { getStateInfo } from "@/lib/getStateInfo";
import { getRequestOrigin } from "@/lib/getCanonicalUrl";
import { resolveCityDisplayName } from "@/lib/domainHelpers";
import { buildSpecialtyFaq, buildSpecialtyIntro } from "@/lib/specialtyContent";
import { isCategorySpecialty } from "@/lib/specialtyIndexing";
import { safeJsonLd } from "@/lib/sanitize";
import { fetchReq } from "@/lib/req";
import { isNextRedirectError } from "@/lib/maintenance";
@@ -58,10 +59,14 @@ export async function generateMetadata({ params }) {
// داخلی‌اش دنبال شوند. به‌محض افزوده‌شدن اولین پزشک، خودکار ایندکس‌پذیر می‌شود.
const { total } = await getDoctors(specialty.id, isRoot ? null : matchedCity?.id);
// دستهٔ سطح‌بالا (دارای زیرشاخهٔ فعال) محتوای مستقلی ندارد و با فرزندانش روی همان
// کوئری رقابت می‌کند؛ صفحهٔ قابل‌ایندکس، همان زیرشاخه است.
const noindex = isCategorySpecialty(specialty) || total === 0;
return {
title,
description,
...(total === 0 && { robots: { index: false, follow: true } }),
...(noindex && { robots: { index: false, follow: true } }),
// C1-a — صفحهٔ فرود تخصص per-domain است؛ canonical لایهٔ layout (self) درست است.
openGraph: { title, description, images: [image] },
twitter: { card: "summary_large_image", title, description, images: [image] },
@@ -83,7 +88,7 @@ async function SpecialtyDetail({ params }) {
);
const intro = buildSpecialtyIntro(specialty.name, cityName, total);
const faq = buildSpecialtyFaq(specialty.name, cityName, total);
const faq = buildSpecialtyFaq(specialty.name, cityName, total, { isRoot });
const pageUrl = `${origin}/specialties/${specialty.slug}`;
const pageJsonLd = {