diff --git a/app/blog/[slug]/loading.js b/app/blog/[slug]/loading.js deleted file mode 100644 index d0e878a..0000000 --- a/app/blog/[slug]/loading.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Skeleton } from "@mui/material"; - -export default function Loading() { - return ( -
- - -
- - -
-
-
- - - - -
- -
-
- ); -} diff --git a/app/blog/[slug]/page.js b/app/blog/[slug]/page.js index 3ea0c06..6bb60af 100644 --- a/app/blog/[slug]/page.js +++ b/app/blog/[slug]/page.js @@ -4,7 +4,8 @@ import Layout from "@/components/layout/StLayout"; import { notFound } from "next/navigation"; import { fetchReq } from "@/lib/req"; import { getStateInfo } from "@/lib/getStateInfo"; -import { getRequestOrigin } from "@/lib/getCanonicalUrl"; +import { getEntityOrigin } from "@/lib/getCanonicalUrl"; +import { extractEntityCityId, findCityById } from "@/lib/domainHelpers"; import { safeJsonLd } from "@/lib/sanitize"; import { normalizeBlog, imageUrl } from "@/helper"; @@ -38,11 +39,14 @@ export async function generateMetadata({ params }) { const description = rawText.slice(0, 160) || blog.title; const image = blog.image_url ? imageUrl(blog.image_url) : FALLBACK_IMG; + // C1-b — پست شهریافته به دامنهٔ همان شهر canonical می‌شود؛ + // پست سراسری (بدون شهر) روی دامنهٔ جاری self-canonical می‌ماند. + const origin = await getEntityOrigin(extractEntityCityId(blog)); + return { title, description, - // محتوای بلاگ روی همه‌ی دامنه‌های شهری یکسان است — تجمیع اعتبار روی دامنه‌ی اصلی - alternates: { canonical: `https://nobat724.com/blog/${slug}` }, + alternates: { canonical: `${origin}/blog/${slug}` }, openGraph: { title, description, @@ -59,18 +63,22 @@ export async function generateMetadata({ params }) { images: [image], }, }; - } catch { + } catch (error) { + console.error(`[blog/${slug}] generateMetadata failed:`, error); return {}; } } async function Blog({ params }) { const { slug } = await params; - const origin = await getRequestOrigin(); const blog = normalizeBlog(await getBlog(slug)); if (!blog) notFound(); + const blogCityId = extractEntityCityId(blog); + const blogCity = findCityById(blogCityId); + const origin = await getEntityOrigin(blogCityId); + let relatedBlogs = []; const relatedResponse = await fetchReq( `${API_URL}/api/v1/blogs?page=1&limit=6` @@ -90,6 +98,10 @@ async function Blog({ params }) { datePublished: new Date(blog.created * 1000).toISOString(), }), ...(blog.author && { author: { "@type": "Person", name: blog.author } }), + // پست شهریافته حوزهٔ جغرافیایی‌اش را اعلام می‌کند؛ پست سراسری این فیلد را ندارد. + ...(blogCity && { + spatialCoverage: { "@type": "Place", name: blogCity.name }, + }), } : null; @@ -120,7 +132,7 @@ async function Blog({ params }) { /> )} - + ); diff --git a/app/blogs/page.js b/app/blogs/page.js index 2451a1c..1a0c4b0 100644 --- a/app/blogs/page.js +++ b/app/blogs/page.js @@ -11,8 +11,8 @@ export async function generateMetadata() { return { title, description, - // مقالات روی همه‌ی دامنه‌ها یکسان‌اند — تجمیع روی دامنه‌ی اصلی - alternates: { canonical: "https://nobat724.com/blogs" }, + // C1-a — لیست بلاگ per-domain است (پست‌های همان شهر + سراسری)؛ + // canonical لایهٔ layout (self روی همان دامنه) درست است. openGraph: { title, description, images: [image] }, twitter: { card: "summary_large_image", title, description, images: [image] }, }; diff --git a/app/clinic/[slug]/loading.js b/app/clinic/[slug]/loading.js deleted file mode 100644 index 238a38b..0000000 --- a/app/clinic/[slug]/loading.js +++ /dev/null @@ -1,18 +0,0 @@ -import { Skeleton } from "@mui/material"; - -export default function Loading() { - return ( -
-
- - - -
-
- - - -
-
- ); -} diff --git a/app/clinic/[slug]/page.js b/app/clinic/[slug]/page.js index 27d2863..d0a87e2 100644 --- a/app/clinic/[slug]/page.js +++ b/app/clinic/[slug]/page.js @@ -4,7 +4,9 @@ import Layout from "@/components/layout/StLayout"; import { notFound } from "next/navigation"; import { fetchReq } from "@/lib/req"; import { getStateInfo } from "@/lib/getStateInfo"; -import { getRequestOrigin } from "@/lib/getCanonicalUrl"; +import { getEntityOrigin } from "@/lib/getCanonicalUrl"; +import { extractEntityCityId } from "@/lib/domainHelpers"; +import { isThinClinic } from "@/lib/entityQuality"; import { safeJsonLd } from "@/lib/sanitize"; import { imageUrl } from "@/helper"; @@ -38,9 +40,15 @@ export async function generateMetadata({ params }) { ? [imageUrl(clinic.images_clinic[0].url)] : ["/assets/images/og-image.png"]; + // C1-b — کلینیک به دامنهٔ شهر خودش canonical می‌شود. + const origin = await getEntityOrigin(extractEntityCityId(clinic)); + return { title, description, + alternates: { canonical: `${origin}/clinic/${clinic.uuid}` }, + // H7 — کلینیک غیرفعال/بی‌محتوا نباید ایندکس شود (تقارن با پزشک بی‌محتوا) + ...(isThinClinic(clinic) && { robots: { index: false, follow: true } }), openGraph: { title, description, @@ -54,7 +62,8 @@ export async function generateMetadata({ params }) { images: image, }, }; - } catch { + } catch (error) { + console.error(`[clinic/${slug}] generateMetadata failed:`, error); return {}; } } @@ -74,13 +83,15 @@ function computeClinicRating(doctors) { async function Clinic({ params, searchParams }) { const { slug } = await params; - const origin = await getRequestOrigin(); const sp = await searchParams; const page = Number(sp?.page) || 1; const limit = 50; const clinic = await getClinic(slug); if (!clinic) notFound(); + + // همان مقصد canonical — تا url/@id و breadcrumb سیگنال متناقض ندهند. + const origin = await getEntityOrigin(extractEntityCityId(clinic)); const reqDoctors = await fetchReq( `${API_URL}/api/v1/clinic/doctor-list/${slug}?page=${page}&limit=${limit}` ); @@ -149,18 +160,6 @@ async function Clinic({ params, searchParams }) { } : null; - const breadcrumbJsonLd = clinic - ? { - "@context": "https://schema.org", - "@type": "BreadcrumbList", - itemListElement: [ - { "@type": "ListItem", position: 1, name: "خانه", item: origin }, - { "@type": "ListItem", position: 2, name: "کلینیک‌ها", item: `${origin}/clinics` }, - { "@type": "ListItem", position: 3, name: clinic.title }, - ], - } - : null; - return ( <> {jsonLd && ( @@ -169,14 +168,15 @@ async function Clinic({ params, searchParams }) { dangerouslySetInnerHTML={{ __html: safeJsonLd(jsonLd) }} /> )} - {breadcrumbJsonLd && ( -