82 lines
3.4 KiB
TypeScript
82 lines
3.4 KiB
TypeScript
import Link from '@/components/LocalizedLink'
|
|
import Image from 'next/image'
|
|
import { notFound } from 'next/navigation'
|
|
import type { Metadata } from 'next'
|
|
import { generateSeoMetadata } from '@/lib/seo'
|
|
import { ArrowLeft } from 'lucide-react'
|
|
import type { Article } from '@homaroma/types'
|
|
|
|
import { API_BASE } from '@/lib/api'
|
|
|
|
async function getArticle(slug: string): Promise<Article | null> {
|
|
try {
|
|
const res = await fetch(`${API_BASE}/articles/${slug}`, { next: { revalidate: 300 } })
|
|
return res.ok ? res.json() : null
|
|
} catch { return null }
|
|
}
|
|
|
|
export async function generateMetadata({ params }: { params: Promise<{ slug: string, lang: string }> }): Promise<Metadata> {
|
|
const { slug, lang } = await params
|
|
const article = await getArticle(slug)
|
|
if (!article) return { title: 'Article Not Found' }
|
|
return generateSeoMetadata({
|
|
title: `${article.title} Scent Guide`,
|
|
description: article.content.replace(/<[^>]+>/g, '').slice(0, 160),
|
|
path: `/article/${slug}`,
|
|
lang,
|
|
openGraph: {
|
|
title: article.title,
|
|
images: article.coverImage ? [article.coverImage] : [],
|
|
type: 'article',
|
|
publishedTime: article.createdAt,
|
|
},
|
|
})
|
|
}
|
|
|
|
export default async function ArticleReaderPage({ params }: { params: Promise<{ slug: string }> }) {
|
|
const { slug } = await params
|
|
const article = await getArticle(slug)
|
|
|
|
if (!article) notFound()
|
|
|
|
return (
|
|
<div className="flex min-h-screen flex-col bg-surface-page text-text-main">
|
|
<main className="flex-1 pb-20 pt-24">
|
|
<article className="relative mx-auto mt-8 w-full max-w-4xl">
|
|
<Link
|
|
href="/scent-guide"
|
|
className="absolute -left-16 top-0 mt-8 hidden rounded-full border border-line-primary/20 bg-surface-card p-4 text-text-main shadow-sm transition-colors hover:text-brand-primary xl:-left-24 lg:flex"
|
|
>
|
|
<ArrowLeft className="h-5 w-5" />
|
|
</Link>
|
|
|
|
<div className="space-y-6 px-6 py-12 text-center">
|
|
<Link href="/scent-guide" className="mb-4 inline-flex items-center gap-2 text-sm text-muted-text hover:text-brand-primary lg:hidden">
|
|
<ArrowLeft className="h-4 w-4" /> Go Back
|
|
</Link>
|
|
<p className="ui-eyebrow">
|
|
{new Date(article.createdAt).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })}
|
|
</p>
|
|
<h1 className="mx-auto max-w-3xl text-4xl font-bold leading-tight text-text-main md:text-5xl lg:text-6xl">
|
|
{article.title}
|
|
</h1>
|
|
</div>
|
|
|
|
{article.coverImage && (
|
|
<div className="relative mb-16 aspect-[16/7] w-full overflow-hidden bg-surface-card shadow-card md:rounded-[2rem]">
|
|
<Image src={article.coverImage} alt={article.title} fill unoptimized className="h-full w-full object-cover" />
|
|
</div>
|
|
)}
|
|
|
|
<div className="ui-panel mx-auto max-w-3xl px-6 py-10 md:px-12">
|
|
<div
|
|
dangerouslySetInnerHTML={{ __html: article.content }}
|
|
className="space-y-6 leading-relaxed text-text-main/85 [&_a]:font-semibold [&_a]:text-brand-primary [&_h2]:text-3xl [&_h2]:font-bold [&_h2]:text-text-main [&_h3]:text-2xl [&_h3]:font-bold [&_h3]:text-text-main [&_img]:rounded-2xl [&_img]:shadow-card [&_p]:leading-relaxed [&_strong]:text-text-main"
|
|
/>
|
|
</div>
|
|
</article>
|
|
</main>
|
|
</div>
|
|
)
|
|
}
|