157 lines
4.9 KiB
TypeScript
157 lines
4.9 KiB
TypeScript
import type { Metadata } from "next";
|
||
import Link from "next/link";
|
||
import { notFound } from "next/navigation";
|
||
import Markdown from "@/components/Markdown";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||
import { Button } from "@/components/ui/button";
|
||
import { PublicApiError, getPublicPost } from "@/lib/public-api";
|
||
import { apiBaseUrl, siteUrl, toAbsoluteUrl } from "@/lib/site";
|
||
import { formatJalali } from "@/lib/utils";
|
||
|
||
type Params = Promise<{ slug: string }>;
|
||
|
||
function cleanText(value?: string | null) {
|
||
if (!value) return "";
|
||
return value.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
||
}
|
||
|
||
async function loadPost(slug: string) {
|
||
try {
|
||
return await getPublicPost(slug);
|
||
} catch (error) {
|
||
if (error instanceof PublicApiError && error.status === 404) {
|
||
notFound();
|
||
}
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
export async function generateMetadata({
|
||
params,
|
||
}: {
|
||
params: Params;
|
||
}): Promise<Metadata> {
|
||
const { slug } = await params;
|
||
const post = await loadPost(slug);
|
||
const description = cleanText(post.excerpt || post.content).slice(0, 160);
|
||
const image = toAbsoluteUrl(
|
||
post.absolute_featured_image_url || post.featured_image,
|
||
apiBaseUrl,
|
||
) ?? `${siteUrl}/favicon.ico`;
|
||
|
||
return {
|
||
title: post.title,
|
||
description,
|
||
alternates: { canonical: `/blog/${post.slug}` },
|
||
openGraph: {
|
||
title: post.title,
|
||
description,
|
||
url: `${siteUrl}/blog/${post.slug}`,
|
||
siteName: "انجمن علمی کامپیوتر شرق گیلان",
|
||
type: "article",
|
||
images: [image],
|
||
locale: "fa_IR",
|
||
publishedTime: post.published_at || post.created_at,
|
||
modifiedTime: post.updated_at,
|
||
},
|
||
twitter: {
|
||
card: "summary_large_image",
|
||
title: post.title,
|
||
description,
|
||
images: [image],
|
||
},
|
||
};
|
||
}
|
||
|
||
export default async function BlogDetailPage({
|
||
params,
|
||
}: {
|
||
params: Params;
|
||
}) {
|
||
const { slug } = await params;
|
||
const post = await loadPost(slug);
|
||
const description = cleanText(post.excerpt || post.content).slice(0, 160);
|
||
const image = toAbsoluteUrl(
|
||
post.absolute_featured_image_url || post.featured_image,
|
||
apiBaseUrl,
|
||
) ?? `${siteUrl}/favicon.ico`;
|
||
|
||
const structuredData = {
|
||
"@context": "https://schema.org",
|
||
"@type": "BlogPosting",
|
||
headline: post.title,
|
||
description,
|
||
image: [image],
|
||
datePublished: post.published_at || post.created_at,
|
||
dateModified: post.updated_at,
|
||
url: `${siteUrl}/blog/${post.slug}`,
|
||
author: {
|
||
"@type": "Person",
|
||
name: [post.author.first_name, post.author.last_name].filter(Boolean).join(" ") || post.author.username,
|
||
},
|
||
publisher: {
|
||
"@type": "Organization",
|
||
name: "انجمن علمی کامپیوتر شرق گیلان",
|
||
logo: {
|
||
"@type": "ImageObject",
|
||
url: `${siteUrl}/favicon.ico`,
|
||
},
|
||
},
|
||
};
|
||
|
||
return (
|
||
<div className="min-h-screen bg-background" dir="rtl">
|
||
<script
|
||
type="application/ld+json"
|
||
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
|
||
/>
|
||
<div className="container mx-auto px-4 py-8">
|
||
<div className="mb-6 flex items-center justify-between gap-3">
|
||
<Button variant="outline" asChild>
|
||
<Link href="/blog">بازگشت به وبلاگ</Link>
|
||
</Button>
|
||
<div className="text-sm text-muted-foreground">
|
||
{formatJalali(post.published_at || post.created_at, false)}
|
||
</div>
|
||
</div>
|
||
|
||
<Card>
|
||
{image && (
|
||
<div className="w-full aspect-video overflow-hidden rounded-t-lg bg-muted">
|
||
<img
|
||
src={image}
|
||
alt={post.title}
|
||
className="h-full w-full object-cover"
|
||
loading="eager"
|
||
/>
|
||
</div>
|
||
)}
|
||
<CardHeader>
|
||
<div className="flex flex-wrap gap-2">
|
||
{post.category?.name ? <Badge variant="secondary">{post.category.name}</Badge> : null}
|
||
{post.tags.map((tag) => (
|
||
<Badge key={tag.id} variant="outline">
|
||
{tag.name}
|
||
</Badge>
|
||
))}
|
||
</div>
|
||
<CardTitle className="text-3xl leading-relaxed">{post.title}</CardTitle>
|
||
<CardDescription className="text-sm">
|
||
نویسنده: {[post.author.first_name, post.author.last_name].filter(Boolean).join(" ") || post.author.username}
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="space-y-6">
|
||
{post.excerpt ? (
|
||
<p className="rounded-lg border bg-muted/30 p-4 text-sm leading-7 text-muted-foreground">
|
||
{post.excerpt}
|
||
</p>
|
||
) : null}
|
||
<Markdown content={post.content} justify size="base" />
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|