import type { Metadata } from "next"; import Link from "next/link"; import { notFound, redirect } from "next/navigation"; import PageHero from "@/components/PageHero"; import Reveal from "@/components/Reveal"; import { Section } from "@/components/Section"; import { isLocale } from "@/i18n/config"; import { getDictionary } from "@/i18n/dictionaries"; import { pageImages } from "@/content"; import { getPostById } from "@/lib/blog"; type Params = { locale: string; id: string; slug: string }; /** A malformed escape sequence must not take the page down. */ function safeDecode(value: string) { try { return decodeURIComponent(value); } catch { return value; } } export async function generateMetadata({ params, }: { params: Promise; }): Promise { const { locale, id } = await params; const post = await getPostById(Number(id)); if (!post) return {}; return { title: post.title, description: post.abstract ?? undefined, alternates: { canonical: `/${locale}/blog/${post.id}/${encodeURIComponent(post.slug)}` }, openGraph: post.imageUrl ? { images: [post.imageUrl], type: "article" } : undefined, }; } export default async function BlogPostPage({ params }: { params: Promise }) { const { locale, id, slug } = await params; if (!isLocale(locale)) notFound(); const numericId = Number(id); if (!Number.isInteger(numericId)) notFound(); const post = await getPostById(numericId); if (!post) notFound(); // The slug segment is SEO-only. If it drifts from the canonical form (an old // link, or the title was edited), send the reader to the canonical URL. // // Two encoding traps here, both of which produce a 500 rather than a redirect: // the incoming param arrives percent-ENCODED, so it must be decoded before // comparing against a Persian slug; and a Location header may not contain raw // non-ASCII, so the target must be encoded on the way back out. const decodedSlug = safeDecode(slug); if (decodedSlug !== post.slug) { redirect(`/${locale}/blog/${post.id}/${encodeURIComponent(post.slug)}`); } const t = await getDictionary(locale); const publishedAt = post.publishedAt ? new Intl.DateTimeFormat(locale, { day: "numeric", month: "long", year: "numeric" }).format( new Date(post.publishedAt) ) : null; return ( <>
{publishedAt && (

{t.blog.publishedOn}{" "} {post.author && · {post.author}}

)} {post.abstract && (

{post.abstract}

)} {post.bodyHtml && ( /* Body arrives as HTML from the Meshkee editor — authored in the client's own dashboard, trusted the way a CMS body is. `.prose-brand` in globals.css styles it without a plugin. */
)}
{t.blog.backToList}
); }