Files
guilan-ace-frontend/src/app/blog/[slug]/page.tsx
Amirhossein Khalili 42f2087b7c
Some checks failed
Frontend CI/CD / build (push) Has been cancelled
Frontend CI/CD / deploy (push) Has been cancelled
migrate to Next.js
2026-05-20 09:46:17 +03:30

157 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}