Refactor blog components for improved tag filtering and related content display

- Added `getBlogTagFacets` API call to fetch blog tags based on city scope.
- Updated `BlogsPage` to read selected tag from URL and handle tag changes with URL updates.
- Modified `Title` component to display tags from the new API and reflect active tag state.
- Enhanced breadcrumb navigation to link categories to their respective pages.
- Adjusted related content section to display articles from the same category and fixed layout issues.
- Corrected heading hierarchy across various components for better SEO compliance.
- Ensured consistent styling and spacing in related content items.
This commit is contained in:
hamed
2026-07-29 14:23:37 +03:30
parent 533449c504
commit eb83762016
13 changed files with 714 additions and 65 deletions
+32 -6
View File
@@ -128,12 +128,28 @@ async function Blog({ params }) {
// پستِ شهر دیگر نشان داده می‌شود که حالا ۴۰۴ می‌گیرد.
const { matchedCity, isRoot } = await getStateInfo();
const scopeCityId = domainScopeCityId({ matchedCity, isRoot });
let relatedBlogs = [];
const relatedResponse = await fetchReq(
`${API_URL}/api/v1/blogs?page=1&limit=6${scopeCityId != null ? `&city_id=${scopeCityId}` : ""}`
const scopeQuery = scopeCityId != null ? `&city_id=${scopeCityId}` : "";
// دستهٔ مقاله = تگ اول؛ هم بردکرامب و هم مقالات مرتبط از همین می‌آیند.
const categoryName = Array.isArray(blog?.tag) ? blog.tag[0]?.name : null;
// اول هم‌دسته‌ها؛ دسته‌های کم‌پست (مثلاً «قلب و عروق» با یک مقاله) با آخرین
// مقالات پر می‌شوند تا ستون خالی یا کوتاه نماند.
const sameTagResponse = categoryName
? await fetchReq(
`${API_URL}/api/v1/blogs?page=1&limit=6&tag=${encodeURIComponent(categoryName)}${scopeQuery}`
)
: null;
const latestResponse = await fetchReq(
`${API_URL}/api/v1/blogs?page=1&limit=6${scopeQuery}`
);
relatedBlogs = (relatedResponse?.data || [])
.filter((b) => b.uuid !== blog?.uuid)
const seen = new Set([blog?.uuid]);
const relatedBlogs = [
...(sameTagResponse?.data || []),
...(latestResponse?.data || []),
]
.filter((b) => !seen.has(b.uuid) && seen.add(b.uuid))
.slice(0, 4)
.map(normalizeBlog);
@@ -188,8 +204,18 @@ async function Blog({ params }) {
itemListElement: [
{ "@type": "ListItem", position: 1, name: "خانه", item: origin },
{ "@type": "ListItem", position: 2, name: "مقالات", item: `${origin}/blogs` },
// دستهٔ مقاله = همان چیزی که بردکرامب HTML نشان می‌دهد؛ مقالهٔ بدون تگ
// سه‌سطحی می‌ماند، چون سطح خالی کل BreadcrumbList را نامعتبر می‌کند.
...(categoryName
? [{
"@type": "ListItem",
position: 3,
name: categoryName,
item: `${origin}/blogs?tag=${encodeURIComponent(categoryName)}`,
}]
: []),
// آیتم آخر بدون `item` کل BreadcrumbList را نامعتبر می‌کند
{ "@type": "ListItem", position: 3, name: blog.title,
{ "@type": "ListItem", position: categoryName ? 4 : 3, name: blog.title,
item: `${origin}/blog/${slug}` },
],
}
+6 -1
View File
@@ -1,3 +1,4 @@
import { Suspense } from "react";
import BlogsPage from "@/components/blogs";
import Layout from "@/components/layout/StLayout";
import { getStateInfo } from "@/lib/getStateInfo";
@@ -33,7 +34,11 @@ export default async function Blogs() {
// تشخیص شهر سمت سرور انجام می‌شود (همان الگوی app/specialties/page.js).
return (
<Layout name="/blogs">
<BlogsPage cityId={isRoot ? null : (matchedCity?.id ?? null)} />
{/* BlogsPage تگ انتخابی را از useSearchParams می‌خواند؛ Next 15 برای آن
مرز Suspense لازم دارد وگرنه prerender صفحه می‌شکند. */}
<Suspense fallback={null}>
<BlogsPage cityId={isRoot ? null : (matchedCity?.id ?? null)} />
</Suspense>
</Layout>
);
}