152 lines
4.5 KiB
TypeScript
152 lines
4.5 KiB
TypeScript
import { postBySlugQuery, postSlugsQuery, urlFor } from '@/lib/sanity'
|
|
import { sanityFetch } from '@/lib/sanityFetch'
|
|
import { PortableTextRenderer } from '@/components/PortableTextRenderer'
|
|
import Image from 'next/image'
|
|
import Link from 'next/link'
|
|
import { notFound } from 'next/navigation'
|
|
import type { Metadata } from 'next'
|
|
|
|
export const revalidate = 60
|
|
|
|
type Params = { slug: string }
|
|
|
|
type Post = {
|
|
_id: string
|
|
title: string
|
|
slug: { current: string }
|
|
publishedAt: string
|
|
excerpt: string | null
|
|
mainImage: { asset: object; alt?: string } | null
|
|
body: unknown[]
|
|
seoTitle: string | null
|
|
seoDescription: string | null
|
|
author: { name: string; image?: object; bio?: string } | null
|
|
}
|
|
|
|
export async function generateStaticParams() {
|
|
try {
|
|
const slugs: { slug: string }[] = await sanityFetch<{ slug: string }[]>(postSlugsQuery)
|
|
return slugs.map((s) => ({ slug: s.slug }))
|
|
} catch (err) {
|
|
console.error('[Guides] generateStaticParams fetch failed:', err)
|
|
return []
|
|
}
|
|
}
|
|
|
|
export async function generateMetadata({ params }: { params: Promise<Params> }): Promise<Metadata> {
|
|
const { slug } = await params
|
|
let post: Post | null = null
|
|
try {
|
|
post = await sanityFetch<Post | null>(postBySlugQuery, { slug })
|
|
} catch (err) {
|
|
console.error('[Guides] generateMetadata fetch failed:', err)
|
|
}
|
|
if (!post) return {}
|
|
|
|
return {
|
|
title: post.seoTitle ?? post.title,
|
|
description: post.seoDescription ?? post.excerpt ?? undefined,
|
|
openGraph: {
|
|
title: post.seoTitle ?? post.title,
|
|
description: post.seoDescription ?? post.excerpt ?? undefined,
|
|
images: post.mainImage?.asset
|
|
? [{ url: urlFor(post.mainImage).width(1200).height(630).url() }]
|
|
: [],
|
|
},
|
|
}
|
|
}
|
|
|
|
function formatDate(iso: string) {
|
|
return new Date(iso).toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
})
|
|
}
|
|
|
|
export default async function PostPage({ params }: { params: Promise<Params> }) {
|
|
const { slug } = await params
|
|
let post: Post | null = null
|
|
try {
|
|
post = await sanityFetch<Post | null>(postBySlugQuery, { slug })
|
|
} catch (err) {
|
|
console.error('[Guides] PostPage fetch failed:', err)
|
|
}
|
|
|
|
if (!post) notFound()
|
|
|
|
return (
|
|
<main className="mx-auto max-w-3xl px-4 py-10">
|
|
{/* Breadcrumb */}
|
|
<nav className="mb-6 text-xs text-zinc-500">
|
|
<Link href="/guides" className="hover:text-zinc-300">
|
|
← Guides
|
|
</Link>
|
|
</nav>
|
|
|
|
{/* Header */}
|
|
<header className="mb-8">
|
|
<div className="mb-3 flex items-center gap-2 text-[0.7rem] text-zinc-500">
|
|
{post.author?.name && <span>{post.author.name}</span>}
|
|
{post.author?.name && <span>·</span>}
|
|
<time dateTime={post.publishedAt}>{formatDate(post.publishedAt)}</time>
|
|
</div>
|
|
|
|
<h1 className="text-3xl font-semibold leading-tight tracking-tight text-zinc-100 md:text-4xl">
|
|
{post.title}
|
|
</h1>
|
|
|
|
{post.excerpt && (
|
|
<p className="mt-4 text-base text-zinc-400 leading-relaxed">
|
|
{post.excerpt}
|
|
</p>
|
|
)}
|
|
</header>
|
|
|
|
{/* Hero image */}
|
|
{post.mainImage?.asset && (
|
|
<div className="relative mb-10 h-64 w-full overflow-hidden rounded-xl md:h-80">
|
|
<Image
|
|
src={urlFor(post.mainImage).width(1200).height(630).url()}
|
|
alt={post.mainImage.alt ?? post.title}
|
|
fill
|
|
priority
|
|
className="object-cover"
|
|
sizes="(max-width: 768px) 100vw, 768px"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Body */}
|
|
{post.body && (
|
|
<article className="border-b border-zinc-800 pb-10">
|
|
<PortableTextRenderer value={post.body} />
|
|
</article>
|
|
)}
|
|
|
|
{/* CTA footer */}
|
|
<div className="mt-10 rounded-xl border border-zinc-800 bg-zinc-950/60 p-6 text-center">
|
|
<p className="text-sm font-medium text-zinc-200">
|
|
Ready to put this into practice?
|
|
</p>
|
|
<p className="mt-1 text-xs text-zinc-500">
|
|
Use the Battl Builder to price out your parts and track your total.
|
|
</p>
|
|
<Link
|
|
href="/builder"
|
|
className="mt-4 inline-block rounded-md bg-amber-400 px-5 py-2.5 text-sm font-semibold text-black hover:bg-amber-300 transition-colors"
|
|
>
|
|
Start Your Build →
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Back link */}
|
|
<div className="mt-8 text-center">
|
|
<Link href="/guides" className="text-xs text-zinc-500 hover:text-zinc-300">
|
|
← Back to all guides
|
|
</Link>
|
|
</div>
|
|
</main>
|
|
)
|
|
}
|