feat: enhance SEO for listing and specialty pages, fix search autocomplete issue
This commit is contained in:
+22
-23
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user