From b8514e64cf148c7871eaa49a64986e79e15f524b Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Fri, 24 Jul 2026 10:39:41 +0330 Subject: [PATCH] feat(breadcrumb): implement BreadcrumbList structure and validation for SEO compliance --- app/blog/[slug]/page.js | 4 +- app/clinics/page.js | 5 ++ app/component/Pageguide.js | 34 ++++------- app/doctor/[slug]/page.js | 5 +- app/specialties/[slug]/page.js | 1 + components/clinic/index.js | 3 +- components/clinics/index.js | 3 +- components/clinics/list/index.js | 7 ++- components/specialties/detail/index.js | 5 +- lib/breadcrumb.js | 57 +++++++++++++++++ lib/breadcrumb.test.js | 84 ++++++++++++++++++++++++++ 11 files changed, 178 insertions(+), 30 deletions(-) create mode 100644 lib/breadcrumb.js create mode 100644 lib/breadcrumb.test.js diff --git a/app/blog/[slug]/page.js b/app/blog/[slug]/page.js index d6e4e75..a9d3b14 100644 --- a/app/blog/[slug]/page.js +++ b/app/blog/[slug]/page.js @@ -189,7 +189,9 @@ async function Blog({ params }) { itemListElement: [ { "@type": "ListItem", position: 1, name: "خانه", item: origin }, { "@type": "ListItem", position: 2, name: "مقالات", item: `${origin}/blogs` }, - { "@type": "ListItem", position: 3, name: blog.title }, + // آیتم آخر بدون `item` کل BreadcrumbList را نامعتبر می‌کند + { "@type": "ListItem", position: 3, name: blog.title, + item: `${origin}/blog/${slug}` }, ], } : null; diff --git a/app/clinics/page.js b/app/clinics/page.js index 80f13d4..b9d8b35 100644 --- a/app/clinics/page.js +++ b/app/clinics/page.js @@ -5,6 +5,7 @@ import Layout from "@/components/layout/StLayout"; import { getStateInfo } from "@/lib/getStateInfo"; import { buildClinicParams } from "@/helper"; import { listingRobots } from "@/lib/listingRobots"; +import { getRequestOrigin } from "@/lib/getCanonicalUrl"; export async function generateMetadata({ searchParams }) { const awaitedParams = await searchParams; @@ -68,12 +69,16 @@ export default async function Clinics({ searchParams }) { if (isNextRedirectError(err)) throw err; } + // origin برای BreadcrumbList لازم است: گوگل در itemListElement آدرس مطلق می‌خواهد. + const origin = await getRequestOrigin(); + return ( ); diff --git a/app/component/Pageguide.js b/app/component/Pageguide.js index a5c614e..39e8362 100644 --- a/app/component/Pageguide.js +++ b/app/component/Pageguide.js @@ -1,41 +1,31 @@ import Link from "next/link"; import { safeJsonLd } from "@/lib/sanitize"; +import { buildBreadcrumbJsonLd, normalizeBreadcrumb } from "@/lib/breadcrumb"; // Breadcrumb واحد سایت: ظاهر دقیقاً همان نسخهٔ قبلی (همان کلاس‌ها، همان جداکنندهٔ «>») // ولی ساختار سمانتیک درست (nav/ol/a) و BreadcrumbList از همان یک منبع داده تولید می‌شود // تا نسخهٔ بصری و schema هرگز واگرا نشوند. // -// list: Array — آیتم آخر همیشه صفحهٔ جاری است (بدون لینک). +// list: Array — به آیتم آخر (صفحهٔ جاری) هم href بده؛ +// ظاهرش عوض نمی‌شود (هرگز لینک نمی‌شود) ولی schema بدون `item` نامعتبر است. +// `origin` هم لازم است، چون گوگل URL مطلق می‌خواهد؛ بدون آن JSON-LD منتشر نمی‌شود. const ITEM_CLASS = "text-[16px] font-normal"; -function normalize(list) { - return list - .map((item) => (typeof item === "string" ? { name: item } : item)) - .filter((item) => item?.name); -} - function Pageguide({ list = [], origin }) { - const items = normalize(list); + const items = normalizeBreadcrumb(list); if (items.length === 0) return null; - const jsonLd = { - "@context": "https://schema.org", - "@type": "BreadcrumbList", - itemListElement: items.map((item, idx) => ({ - "@type": "ListItem", - position: idx + 1, - name: item.name, - ...(item.href && origin && { item: `${origin}${item.href}` }), - })), - }; + const jsonLd = buildBreadcrumbJsonLd(items, origin); return ( <> -