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
+22 -23
View File
@@ -1,6 +1,5 @@
import { headers } from "next/headers";
import { findDomainByCityId } from "@/lib/domainHelpers";
import specialtiesData from "@/data/specialties.json";
// استراتژی canonical سه‌دسته:
// C1-a per-domain : /، /doctors، /clinics، /specialties، /specialties/[slug]، /blogs
@@ -62,29 +61,27 @@ export function buildMainCanonicalUrl(pathname) {
* تصمیم M3-B: صفحات ۲+ لیست خودشان canonical دارند تا ایندکس‌پذیر بمانند.
* (یاسوج به‌تنهایی ۳۸ صفحه دارد؛ collapse به صفحهٔ ۱ همهٔ آن‌ها را از ایندکس حذف می‌کرد.)
*/
// /doctors?specialty=X با تک‌فیلتر، همان صفحهٔ فرود تخصص است — اعتبارش آنجا تجمیع شود.
// city/state تزریق‌شده از host اینجا هم «فیلتر کاربر» حساب نمی‌شوند (هم‌راستا با listingRobots).
const HOST_INJECTED_KEYS = new Set(["city", "state", "specialty", "page"]);
const LISTING_PATHS = new Set(["/doctors", "/clinics"]);
export function buildSpecialtyCanonicalPath(pathname, search) {
if (buildCanonicalPath(pathname) !== "/doctors" || !search) return null;
/**
* صفحات لیستِ فیلتردار طبق lib/listingRobots نوایندکس هستند. کنار noindex، canonical
* به یک URL دیگر سیگنال متناقض است و می‌تواند noindex را به هدف canonical ببرد —
* دقیقاً همان /specialties/[slug] یا /doctors خامی که باید ایندکس بمانند.
* پس این صفحات self-canonical می‌شوند و کل کوئری‌شان حفظ می‌گردد.
*
* پارامترها مرتب می‌شوند تا دو ترتیب متفاوت از یک فیلتر به یک canonical برسند.
*/
export function buildFilteredListingCanonical(host, path, search) {
if (!LISTING_PATHS.has(path) || !search) return null;
const params = new URLSearchParams(search);
const specialtyName = params.get("specialty");
if (!specialtyName) return null;
const filled = [...params.entries()].filter(([, value]) => value !== "");
const hasFilter = filled.some(([key]) => key !== "page");
if (!hasFilter) return null;
// صفحهٔ ۲ به بعد محتوای متفاوتی دارد و صفحهٔ فرود تخصص pagination ندارد؛
// canonical کردن صفحهٔ N به صفحهٔ ۱ خلاف راهنمای گوگل است و یا نادیده گرفته
// می‌شود یا مسیر کشف پزشکان صفحات بعدی را می‌بندد. آن‌ها self-canonical می‌مانند.
if (Number(params.get("page")) > 1) return null;
const hasOtherFilter = [...params.keys()].some((key) => !HOST_INJECTED_KEYS.has(key));
if (hasOtherFilter) return null;
const specialty = specialtiesData.find(
(item) => item.name === specialtyName && item.status === 1
);
return specialty?.slug ? `/specialties/${specialty.slug}` : null;
const normalized = new URLSearchParams(filled);
normalized.sort();
return `${buildCanonicalUrl(host, path)}?${normalized.toString()}`;
}
export function buildCanonicalQuery(search) {
@@ -140,11 +137,13 @@ export async function getCanonicalUrl() {
if (STATIC_SHARED_PAGES.has(path)) return buildMainCanonicalUrl(path);
const search = headersList.get("x-search");
const specialtyPath = buildSpecialtyCanonicalPath(path, search);
if (specialtyPath) return buildCanonicalUrl(headersList.get("host"), specialtyPath);
const host = headersList.get("host");
const filteredListing = buildFilteredListingCanonical(host, path, search);
if (filteredListing) return filteredListing;
const query = buildCanonicalQuery(search);
return `${buildCanonicalUrl(headersList.get("host"), path)}${query}`;
return `${buildCanonicalUrl(host, path)}${query}`;
} catch (error) {
console.error("Error generating canonical URL:", error);
return null;