133 lines
3.8 KiB
TypeScript
133 lines
3.8 KiB
TypeScript
import Link from 'next/link'
|
|
import { notFound } from 'next/navigation'
|
|
import { CustomMDX } from '@/components/mdx'
|
|
import PostHeader from '@/components/blog/PostHeader'
|
|
import ProgressBar from '@/components/blog/ProgressBar'
|
|
import { getAllPosts, getReadingTime, findAdjacentPosts } from '../utils'
|
|
import { baseUrl } from '../../sitemap'
|
|
|
|
export async function generateStaticParams() {
|
|
const posts = await getAllPosts()
|
|
|
|
return posts.map((post) => ({
|
|
slug: post.slug,
|
|
}))
|
|
}
|
|
|
|
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
|
|
const { slug } = await params
|
|
const post = (await getAllPosts()).find((p) => p.slug === slug)
|
|
if (!post) {
|
|
return {}
|
|
}
|
|
|
|
const {
|
|
title,
|
|
publishedAt: publishedTime,
|
|
summary: description,
|
|
image,
|
|
} = post.metadata
|
|
|
|
const ogImage = image
|
|
? image
|
|
: `${baseUrl}/og?title=${encodeURIComponent(title)}`
|
|
|
|
return {
|
|
title,
|
|
description,
|
|
openGraph: {
|
|
title,
|
|
description,
|
|
type: 'article',
|
|
publishedTime,
|
|
url: `${baseUrl}/blog/${post.slug}`,
|
|
images: [{ url: ogImage }],
|
|
},
|
|
twitter: {
|
|
card: 'summary_large_image',
|
|
title,
|
|
description,
|
|
images: [ogImage],
|
|
},
|
|
}
|
|
}
|
|
|
|
export default async function Blog({ params }: { params: Promise<{ slug: string }> }) {
|
|
const { slug } = await params;
|
|
const posts = await getAllPosts()
|
|
const post = posts.find((p) => p.slug === slug)
|
|
|
|
if (!post) {
|
|
notFound()
|
|
}
|
|
|
|
const reading = getReadingTime(post.content)
|
|
const { prev, next } = findAdjacentPosts(posts, slug)
|
|
|
|
return (
|
|
<>
|
|
<ProgressBar />
|
|
<section className="mx-auto max-w-5xl px-4 md:px-6">
|
|
<div className="mb-2">
|
|
<Link
|
|
href="/blog"
|
|
className="inline-flex items-center gap-1 rounded-md px-2 py-1 text-sm text-neutral-700 transition-colors hover:bg-black/[0.04] hover:text-neutral-900 dark:text-neutral-300 dark:hover:bg-white/5 dark:hover:text-neutral-100"
|
|
aria-label="Back to blog"
|
|
>
|
|
← <span className="underline-offset-4 hover:underline">Back to blog</span>
|
|
</Link>
|
|
</div>
|
|
<script
|
|
type="application/ld+json"
|
|
suppressHydrationWarning
|
|
dangerouslySetInnerHTML={{
|
|
__html: JSON.stringify({
|
|
'@context': 'https://schema.org',
|
|
'@type': 'BlogPosting',
|
|
headline: post.metadata.title,
|
|
datePublished: post.metadata.publishedAt,
|
|
dateModified: post.metadata.publishedAt,
|
|
description: post.metadata.summary,
|
|
image: post.metadata.image
|
|
? `${baseUrl}${post.metadata.image}`
|
|
: `/og?title=${encodeURIComponent(post.metadata.title)}`,
|
|
url: `${baseUrl}/blog/${post.slug}`,
|
|
author: {
|
|
'@type': 'Person',
|
|
name: 'My Portfolio',
|
|
},
|
|
}),
|
|
}}
|
|
/>
|
|
<PostHeader
|
|
title={post.metadata.title}
|
|
publishedAt={post.metadata.publishedAt}
|
|
readingTimeText={reading.text}
|
|
tags={post.metadata.tags}
|
|
summary={post.metadata.summary}
|
|
className="mb-6"
|
|
/>
|
|
<article className="prose mx-auto max-w-3xl">
|
|
<CustomMDX source={post.content} />
|
|
</article>
|
|
<nav className="mx-auto mt-10 flex max-w-3xl justify-between text-sm">
|
|
{prev ? (
|
|
<Link href={`/blog/${prev.slug}`} className="underline-offset-4 hover:underline">
|
|
← {prev.metadata.title}
|
|
</Link>
|
|
) : (
|
|
<span />
|
|
)}
|
|
{next ? (
|
|
<Link href={`/blog/${next.slug}`} className="underline-offset-4 hover:underline">
|
|
{next.metadata.title} →
|
|
</Link>
|
|
) : (
|
|
<span />
|
|
)}
|
|
</nav>
|
|
</section>
|
|
</>
|
|
)
|
|
}
|