Files
meshkee/src/app/blog/[slug]/page.tsx
T
Alireza HassaniandCursor e6404eaaec Add contact and about pages with shared CTA buttons.
Wire forms to the Meshkee contact API, tighten page-title spacing, and polish outline button hover so it fades cleanly.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-18 17:54:34 +03:30

164 lines
5.0 KiB
TypeScript

import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import { BlogCoverImage } from "@/components/BlogCoverImage";
import { ContentPageShell } from "@/components/ContentPageShell";
import { HtmlContent } from "@/components/HtmlContent";
import {
ApiNotFoundError,
authorName,
formatFaDate,
getBlog,
getBlogCoverImage,
} from "@/lib/api";
type BlogDetailPageProps = {
params: Promise<{ slug: string }>;
};
function stripLeadingCoverImage(html: string, cover: string | null): string {
if (!html) return html;
let next = html.replace(
/^\s*<p[^>]*>\s*<img[^>]*>\s*<\/p>/i,
"",
);
if (cover) {
const escaped = cover.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
next = next.replace(
new RegExp(
`<p[^>]*>\\s*<img[^>]*src=["']${escaped}["'][^>]*>\\s*<\\/p>`,
"i",
),
"",
);
}
return next.trim();
}
export async function generateMetadata({
params,
}: BlogDetailPageProps): Promise<Metadata> {
const { slug } = await params;
try {
const blog = await getBlog(slug);
return {
title: `${blog.title} | مشکی`,
description: blog.abstract ?? undefined,
alternates: { canonical: `/blog/${blog.slug}` },
openGraph: {
title: blog.title,
description: blog.abstract ?? undefined,
type: "article",
},
};
} catch {
return { title: "مقاله | مشکی" };
}
}
export default async function BlogDetailPage({ params }: BlogDetailPageProps) {
const { slug } = await params;
let blog;
try {
blog = await getBlog(slug);
} catch (error) {
if (error instanceof ApiNotFoundError) notFound();
throw error;
}
const cover = getBlogCoverImage(blog);
const date = formatFaDate(blog.publishedAt);
const author = authorName(blog.author);
const category = blog.categoryName;
const bodyHtml = blog.mainTextHtml
? stripLeadingCoverImage(blog.mainTextHtml, cover)
: null;
const titleEn = category || "Blog";
return (
<ContentPageShell flushTop>
<article className="pb-20 lg:pb-28">
<div className="page-title-band mb-8 md:mb-10">
<div className="relative mx-auto max-w-7xl px-5 pb-10 pt-[calc(var(--header-height)+0.15rem)] lg:px-8 lg:pb-12 lg:pt-[calc(var(--header-height)+0.3rem)]">
<p
aria-hidden
className="latin pointer-events-none absolute inset-x-5 top-[calc(var(--header-height)+1.75rem)] z-0 text-[clamp(1.75rem,5.5vw,3.75rem)] font-light uppercase leading-none tracking-[0.05em] text-ink/[0.07] lg:inset-x-8 lg:top-[calc(var(--header-height)+1.95rem)]"
>
{titleEn}
</p>
<div className="reveal relative z-10">
<nav
aria-label="مسیر صفحه"
className="mb-3 flex flex-wrap items-center gap-1.5 text-xs text-muted"
>
<Link href="/blog" className="link-hover">
مقالات
</Link>
{category ? (
<>
<span aria-hidden className="text-line">
/
</span>
<span className="latin uppercase tracking-[0.14em]">
{category}
</span>
</>
) : null}
<span aria-hidden className="text-line">
/
</span>
<span className="line-clamp-1 text-ink-soft">{blog.title}</span>
</nav>
<h1 className="whitespace-normal text-[clamp(1.35rem,3.8vw,2.5rem)] font-normal leading-tight tracking-tight text-ink">
{blog.title}
</h1>
{blog.abstract ? (
<p className="mt-2 text-justify text-sm leading-7 text-muted md:text-base">
{blog.abstract}
</p>
) : null}
</div>
</div>
</div>
<div className="mx-auto max-w-3xl px-5 lg:px-8">
{cover ? (
<div className="mb-5 overflow-hidden rounded-2xl">
<BlogCoverImage
src={cover}
alt={blog.title}
className="mx-auto max-h-[28rem]"
/>
</div>
) : null}
{(date || author) ? (
<div className="mb-8 flex items-center justify-between gap-4 border-b border-line/70 pb-5 text-xs text-muted">
{author ? <span>نویسنده: {author}</span> : <span />}
{date ? (
<time dateTime={blog.publishedAt ?? undefined}>{date}</time>
) : null}
</div>
) : cover ? (
<div className="mb-8 border-b border-line/70" />
) : null}
{bodyHtml ? (
<div className="reveal">
<HtmlContent html={bodyHtml} />
</div>
) : null}
</div>
</article>
</ContentPageShell>
);
}