import { cache } from "react"; import BlogPage from "@/components/blog"; import Layout from "@/components/layout/StLayout"; import { notFound } from "next/navigation"; import { fetchReq } from "@/lib/req"; import { getStateInfo } from "@/lib/getStateInfo"; import { getEntityOrigin } from "@/lib/getCanonicalUrl"; import { extractEntityCityId, findCityById } from "@/lib/domainHelpers"; import { safeJsonLd } from "@/lib/sanitize"; import { normalizeBlog, imageUrl } from "@/helper"; const FALLBACK_IMG = "/assets/images/og-image.png"; const API_URL = process.env.NEXT_PUBLIC_API_URL; const getBlog = cache(async (slug) => { const res = await fetch(`${API_URL}/api/v1/blog/${slug}`, { next: { revalidate: 3600, tags: [`blog-${slug}`] }, }); if (res.status === 404 || res.status === 400) return null; if (!res.ok) throw new Error(`Failed to fetch blog: ${res.status}`); const json = await res.json(); return json?.data?.data ?? null; }); export async function generateMetadata({ params }) { const { slug } = await params; const { matchedCity } = await getStateInfo(); const siteName = matchedCity?.site_name || "نوبت 724"; const blog = await getBlog(slug); if (!blog) notFound(); try { const title = `${blog.title} | ${siteName}`; const rawText = (blog.summary || blog.body || "") .replace(/<[^>]*>/g, "") .trim(); 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: `${origin}/blog/${slug}` }, openGraph: { title, description, type: "article", ...(blog.created_at && { publishedTime: new Date(blog.created_at * 1000).toISOString(), }), images: [image], }, twitter: { card: "summary_large_image", title, description, images: [image], }, }; } catch (error) { console.error(`[blog/${slug}] generateMetadata failed:`, error); return {}; } } async function Blog({ params }) { const { slug } = await params; 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` ); relatedBlogs = (relatedResponse?.data || []) .filter((b) => b.uuid !== blog?.uuid) .slice(0, 4) .map(normalizeBlog); const jsonLd = blog ? { "@context": "https://schema.org", "@type": "Article", headline: blog.title, ...(blog.images?.[0]?.url && { image: imageUrl(blog.images[0].url) }), ...(blog.created && { datePublished: new Date(blog.created * 1000).toISOString(), }), ...(blog.author && { author: { "@type": "Person", name: blog.author } }), // پست شهریافته حوزهٔ جغرافیایی‌اش را اعلام می‌کند؛ پست سراسری این فیلد را ندارد. ...(blogCity && { spatialCoverage: { "@type": "Place", name: blogCity.name }, }), } : null; const breadcrumbJsonLd = blog ? { "@context": "https://schema.org", "@type": "BreadcrumbList", itemListElement: [ { "@type": "ListItem", position: 1, name: "خانه", item: origin }, { "@type": "ListItem", position: 2, name: "مقالات", item: `${origin}/blogs` }, { "@type": "ListItem", position: 3, name: blog.title }, ], } : null; return ( <> {jsonLd && (