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>
164 lines
5.0 KiB
TypeScript
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>
|
|
);
|
|
}
|