Files
majd-trading/src/app/[locale]/blog/[id]/[slug]/page.tsx
T
Alireza HassaniandClaude Opus 5 02aaf61bb6 Initial import: Majd Trading website
Next.js 15 site for گروه بازرگانی بین المللی آریو مجد آسیا, rebuilt from
majdtrading.com. Persian, English and Arabic; RTL and LTR.

- Pages: home, about, contact, six product categories, blog index and detail
- i18n: prose in per-locale JSON, structure in TypeScript, drift checked by
  `npm run i18n:check`. Blog stays single-language by design
- Meshkee backend: blog content, tenant resolution, session via the customer
  portal, and /meshkee/sitemap-config.json
- Live market data from livedata.ir, cached server-side once a minute
- Self-hosted fonts: IRANYekan (fa), IBM Plex Sans Arabic (ar), Space Grotesk
  and BBH Bartle (Latin)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 19:16:32 +03:30

122 lines
3.9 KiB
TypeScript

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<Params>;
}): Promise<Metadata> {
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<Params> }) {
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 (
<>
<PageHero
title={post.title}
latin={t.blog.headingLatin}
image={post.imageUrl ?? pageImages.about}
/>
<Section>
<article className="mx-auto max-w-3xl">
<Reveal>
{publishedAt && (
<p className="text-xs text-muted">
{t.blog.publishedOn}{" "}
<time dateTime={post.publishedAt ?? undefined} className="text-brand/70">
{publishedAt}
</time>
{post.author && <span className="text-brand/70"> · {post.author}</span>}
</p>
)}
<span className="mt-5 block h-1 w-16 bg-brand" />
{post.abstract && (
<p className="mt-8 text-justify text-base font-medium leading-9 text-brand">
{post.abstract}
</p>
)}
{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. */
<div
className="prose-brand mt-8"
dangerouslySetInnerHTML={{ __html: post.bodyHtml }}
/>
)}
</Reveal>
</article>
<div className="mx-auto mt-12 max-w-3xl border-t border-brand-100 pt-8">
<Link
href={`/${locale}/blog`}
className="text-sm font-bold text-brand transition-colors hover:text-brand-500"
>
{t.blog.backToList}
</Link>
</div>
</Section>
</>
);
}