feat(blog): enhance metadata generation and blog fetching by city context

This commit is contained in:
hamed
2026-07-19 09:02:14 +03:30
parent 88782e70c2
commit 4d7b87ee1a
5 changed files with 46 additions and 12 deletions
+11 -2
View File
@@ -31,12 +31,21 @@ export async function generateMetadata({ params }) {
if (!blog) notFound(); if (!blog) notFound();
try { 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 || "") const rawText = (blog.summary || blog.body || "")
.replace(/<[^>]*>/g, "") .replace(/<[^>]*>/g, "")
.trim(); .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; const image = blog.image_url ? imageUrl(blog.image_url) : FALLBACK_IMG;
// C1-b — پست شهریافته به دامنهٔ همان شهر canonical می‌شود؛ // C1-b — پست شهریافته به دامنهٔ همان شهر canonical می‌شود؛
+6 -2
View File
@@ -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 ( return (
<Layout name="/blogs"> <Layout name="/blogs">
<BlogsPage /> <BlogsPage cityId={isRoot ? null : (matchedCity?.id ?? null)} />
</Layout> </Layout>
); );
} }
+9 -6
View File
@@ -175,14 +175,18 @@ function getSpecialtyUrls(baseUrl) {
} }
// هر پست فقط در sitemap دامنهٔ canonical خودش: پست شهریافته → دامنهٔ آن شهر، // هر پست فقط در sitemap دامنهٔ canonical خودش: پست شهریافته → دامنهٔ آن شهر،
// پست سراسری (بدون شهر) → دامنهٔ اصلی. // پست سراسری (یا شهرِ بدون دامنهٔ اختصاصی) → دامنهٔ اصلی.
async function getBlogUrls(baseUrl, scope, currentDomain) { //
// تطبیق با شناسهٔ شهرِ scope انجام می‌شود نه با رشتهٔ دامنه: scope خودش از subdomain
// استخراج شده، پس با پورت و TLD متفاوت (محیط محلی) هم درست کار می‌کند.
async function getBlogUrls(baseUrl, scope) {
const blogs = await fetchAllPages('/api/v1/blogs'); const blogs = await fetchAllPages('/api/v1/blogs');
return blogs return blogs
.filter((b) => b?.slug || b?.uuid) .filter((b) => b?.slug || b?.uuid)
.filter((b) => { .filter((b) => {
const cityDomain = findDomainByCityId(extractEntityCityId(b)); const cityId = extractEntityCityId(b);
return cityDomain ? cityDomain === currentDomain : scope.isRoot; if (!findDomainByCityId(cityId)) return scope.isRoot;
return !scope.isRoot && Number(scope.matchedCity?.id) === Number(cityId);
}) })
.map((b) => .map((b) =>
withLastModified( withLastModified(
@@ -201,12 +205,11 @@ export default async function sitemap() {
const domain = await getCurrentDomain(); const domain = await getCurrentDomain();
const baseUrl = getBaseUrl(domain); const baseUrl = getBaseUrl(domain);
const scope = getCityScope(domain); const scope = getCityScope(domain);
const normalizedDomain = domain.toLowerCase().replace(/^www\./, '').split(':')[0];
const [doctorUrls, clinicUrls, blogUrls] = await Promise.all([ const [doctorUrls, clinicUrls, blogUrls] = await Promise.all([
getDoctorUrls(baseUrl, scope), getDoctorUrls(baseUrl, scope),
getClinicUrls(baseUrl, scope), getClinicUrls(baseUrl, scope),
getBlogUrls(baseUrl, scope, normalizedDomain), getBlogUrls(baseUrl, scope),
]); ]);
const allUrls = [ const allUrls = [
+6 -2
View File
@@ -7,7 +7,7 @@ import LatestArticles from "./latestArticles";
import { request } from "@/services/response"; import { request } from "@/services/response";
import { normalizeBlog } from "@/helper"; import { normalizeBlog } from "@/helper";
function BlogsPage() { function BlogsPage({ cityId = null }) {
const [blogs, setBlogs] = useState([]); const [blogs, setBlogs] = useState([]);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1); const [totalPages, setTotalPages] = useState(1);
@@ -25,6 +25,10 @@ function BlogsPage() {
if (selectedTag) { if (selectedTag) {
params.tag = selectedTag; params.tag = selectedTag;
} }
// backend با city_id پست‌های آن شهر «و» پست‌های سراسری را برمی‌گرداند
if (cityId) {
params.city_id = cityId;
}
const response = await request.getBlogs(params); const response = await request.getBlogs(params);
setBlogs((response?.data || []).map(normalizeBlog)); setBlogs((response?.data || []).map(normalizeBlog));
setTotalPages(response?.meta?.totalPages || 1); setTotalPages(response?.meta?.totalPages || 1);
@@ -37,7 +41,7 @@ function BlogsPage() {
}; };
fetchBlogs(); fetchBlogs();
}, [page, selectedTag]); }, [page, selectedTag, cityId]);
const handlePageChange = (event, value) => { const handlePageChange = (event, value) => {
setPage(value); setPage(value);
+14
View File
@@ -75,3 +75,17 @@ describe("extractEntityCityId", () => {
expect(extractEntityCityId(null)).toBeNull(); 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();
});
});