From 4d7b87ee1a019fecc13a9837f023d77c6e9398ae Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Sun, 19 Jul 2026 09:02:14 +0330 Subject: [PATCH] feat(blog): enhance metadata generation and blog fetching by city context --- app/blog/[slug]/page.js | 13 +++++++++++-- app/blogs/page.js | 8 ++++++-- app/sitemap.js | 15 +++++++++------ components/blogs/index.js | 8 ++++++-- lib/domainHelpers.test.js | 14 ++++++++++++++ 5 files changed, 46 insertions(+), 12 deletions(-) diff --git a/app/blog/[slug]/page.js b/app/blog/[slug]/page.js index 6bb60af..286d9a5 100644 --- a/app/blog/[slug]/page.js +++ b/app/blog/[slug]/page.js @@ -31,12 +31,21 @@ export async function generateMetadata({ params }) { if (!blog) notFound(); try { + // برند از شهرِ خودِ پست می‌آید، نه از دامنه‌ای که اتفاقاً آن را سرو می‌کند — + // وگرنه پست یاسوجی که روی دامنهٔ یزد باز شود عنوانش «یزد نوبت» می‌شد در حالی + // که canonical آن به یاسوج اشاره می‌کند. پست سراسری برند دامنهٔ جاری را می‌گیرد. + const blogCity = findCityById(extractEntityCityId(blog)); + const brand = blogCity?.site_name || siteName; - const title = `${blog.title} | ${siteName}`; + const title = `${blog.title} | ${brand}`; const rawText = (blog.summary || blog.body || "") .replace(/<[^>]*>/g, "") .trim(); - const description = rawText.slice(0, 160) || blog.title; + const baseDescription = rawText.slice(0, 160) || blog.title; + // نام شهر فقط برای پست شهریافته؛ پست سراسری هیچ شهری نمی‌گیرد. + const description = blogCity + ? `${baseDescription}`.slice(0, 150) + ` | ${blogCity.name}` + : baseDescription; const image = blog.image_url ? imageUrl(blog.image_url) : FALLBACK_IMG; // C1-b — پست شهریافته به دامنهٔ همان شهر canonical می‌شود؛ diff --git a/app/blogs/page.js b/app/blogs/page.js index 1a0c4b0..b55392f 100644 --- a/app/blogs/page.js +++ b/app/blogs/page.js @@ -18,10 +18,14 @@ export async function generateMetadata() { }; } -export default function Blogs() { +export default async function Blogs() { + const { matchedCity, isRoot } = await getStateInfo(); + + // روی دامنهٔ شهری: پست‌های همان شهر + سراسری. روی دامنهٔ ریشه: همهٔ پست‌ها. + // تشخیص شهر سمت سرور انجام می‌شود (همان الگوی app/specialties/page.js). return ( - + ); } diff --git a/app/sitemap.js b/app/sitemap.js index c63e19f..fb1edbe 100644 --- a/app/sitemap.js +++ b/app/sitemap.js @@ -175,14 +175,18 @@ function getSpecialtyUrls(baseUrl) { } // هر پست فقط در sitemap دامنهٔ canonical خودش: پست شهریافته → دامنهٔ آن شهر، -// پست سراسری (بدون شهر) → دامنهٔ اصلی. -async function getBlogUrls(baseUrl, scope, currentDomain) { +// پست سراسری (یا شهرِ بدون دامنهٔ اختصاصی) → دامنهٔ اصلی. +// +// تطبیق با شناسهٔ شهرِ scope انجام می‌شود نه با رشتهٔ دامنه: scope خودش از subdomain +// استخراج شده، پس با پورت و TLD متفاوت (محیط محلی) هم درست کار می‌کند. +async function getBlogUrls(baseUrl, scope) { const blogs = await fetchAllPages('/api/v1/blogs'); return blogs .filter((b) => b?.slug || b?.uuid) .filter((b) => { - const cityDomain = findDomainByCityId(extractEntityCityId(b)); - return cityDomain ? cityDomain === currentDomain : scope.isRoot; + const cityId = extractEntityCityId(b); + if (!findDomainByCityId(cityId)) return scope.isRoot; + return !scope.isRoot && Number(scope.matchedCity?.id) === Number(cityId); }) .map((b) => withLastModified( @@ -201,12 +205,11 @@ export default async function sitemap() { const domain = await getCurrentDomain(); const baseUrl = getBaseUrl(domain); const scope = getCityScope(domain); - const normalizedDomain = domain.toLowerCase().replace(/^www\./, '').split(':')[0]; const [doctorUrls, clinicUrls, blogUrls] = await Promise.all([ getDoctorUrls(baseUrl, scope), getClinicUrls(baseUrl, scope), - getBlogUrls(baseUrl, scope, normalizedDomain), + getBlogUrls(baseUrl, scope), ]); const allUrls = [ diff --git a/components/blogs/index.js b/components/blogs/index.js index cdfab08..1bce97b 100644 --- a/components/blogs/index.js +++ b/components/blogs/index.js @@ -7,7 +7,7 @@ import LatestArticles from "./latestArticles"; import { request } from "@/services/response"; import { normalizeBlog } from "@/helper"; -function BlogsPage() { +function BlogsPage({ cityId = null }) { const [blogs, setBlogs] = useState([]); const [page, setPage] = useState(1); const [totalPages, setTotalPages] = useState(1); @@ -25,6 +25,10 @@ function BlogsPage() { if (selectedTag) { params.tag = selectedTag; } + // backend با city_id پست‌های آن شهر «و» پست‌های سراسری را برمی‌گرداند + if (cityId) { + params.city_id = cityId; + } const response = await request.getBlogs(params); setBlogs((response?.data || []).map(normalizeBlog)); setTotalPages(response?.meta?.totalPages || 1); @@ -37,7 +41,7 @@ function BlogsPage() { }; fetchBlogs(); - }, [page, selectedTag]); + }, [page, selectedTag, cityId]); const handlePageChange = (event, value) => { setPage(value); diff --git a/lib/domainHelpers.test.js b/lib/domainHelpers.test.js index dc980c3..a9f46f8 100644 --- a/lib/domainHelpers.test.js +++ b/lib/domainHelpers.test.js @@ -75,3 +75,17 @@ describe("extractEntityCityId", () => { expect(extractEntityCityId(null)).toBeNull(); }); }); + +describe("شکل پاسخ بلاگ (city آبجکت یا null)", () => { + it("پست شهریافته → شناسه و دامنهٔ همان شهر", () => { + const post = { slug: "x", city: { id: "123", name: "یاسوج" } }; + expect(extractEntityCityId(post)).toBe("123"); + expect(findDomainByCityId(extractEntityCityId(post))).toBe("yasuj-nobat.ir"); + }); + + it("پست سراسری (city: null) → بدون شهر، بدون دامنه", () => { + const post = { slug: "x", city: null }; + expect(extractEntityCityId(post)).toBeNull(); + expect(findDomainByCityId(extractEntityCityId(post))).toBeNull(); + }); +});