- Add sitemap.ts and robots.ts - pageMetadata() helper: consistent title, canonical URL, Open Graph, Twitter card and feed alternate on every page - Generated OG images: a site card, and per post either the cover image or a branded title card (pre-rendered at build) - BlogPosting JSON-LD on post pages - Fail fast with a clear error when SITE_URL is missing Content loading: - Validate post frontmatter at build time and normalize dates to ISO (fixes invalid <time datetime> values) - Add draft: true support (drafts only render in development) - Cache markdown rendering so metadata and page share one render - Replace the three copy-pasted about/projects/setup pages with a shared MarkdownPage component Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
70 lines
2.6 KiB
TypeScript
70 lines
2.6 KiB
TypeScript
import { getAllCategories } from '@/lib/categories'
|
|
import { getAllPostsMeta } from '@/lib/posts'
|
|
import { slugify } from '@/utils/slug'
|
|
import type { Metadata } from 'next'
|
|
import Link from 'next/link'
|
|
import { pageMetadata } from '@/app/metadata'
|
|
import { dateFormatter } from '@/utils/time'
|
|
|
|
export async function generateMetadata(): Promise<Metadata> {
|
|
return pageMetadata({
|
|
title: 'Categories',
|
|
description: 'All post categories on the blog',
|
|
path: '/categories',
|
|
})
|
|
}
|
|
|
|
export default function CategoriesPage() {
|
|
const categories = getAllCategories()
|
|
const posts = getAllPostsMeta()
|
|
const categoryNames = Object.keys(categories).toSorted((a, b) => a.localeCompare(b))
|
|
|
|
return (
|
|
<main className="max-w-3xl 2xl:max-w-4xl mx-auto px-4 py-8 mb-8 lg:py-16 w-full">
|
|
<div className="font-mono text-sm text-fg-dim mb-2">~/categories</div>
|
|
|
|
<ul className="border-l border-border ml-1">
|
|
{categoryNames.map((category) => {
|
|
const categoryPosts = posts.filter((post) => post.category === category)
|
|
|
|
return (
|
|
<li key={category} className="pl-5">
|
|
<Link
|
|
href={`/categories/${slugify(category)}`}
|
|
className="group flex items-baseline gap-2 py-2 px-2 -ml-2 hover:bg-accent/10 transition-colors"
|
|
>
|
|
<span className="font-bold group-hover:text-accent transition-colors">
|
|
{category}
|
|
</span>
|
|
<span className="text-xs text-fg">({categoryPosts.length})</span>
|
|
</Link>
|
|
|
|
<ul className="border-l border-border ml-3 mb-3">
|
|
{categoryPosts.map((post) => (
|
|
<li key={post.slug} className="pl-5">
|
|
<Link
|
|
href={`/posts/${post.slug}`}
|
|
className="group flex items-baseline justify-between gap-3 py-1.5 px-2 -ml-2 border-l-2 border-transparent hover:bg-accent/10 hover:border-accent transition-colors"
|
|
>
|
|
<span className="group-hover:text-accent transition-colors truncate">
|
|
{post.title}
|
|
</span>
|
|
{post.date && (
|
|
<time
|
|
dateTime={new Date(post.date).toISOString()}
|
|
className="text-sm whitespace-nowrap"
|
|
>
|
|
{dateFormatter.format(new Date(post.date))}
|
|
</time>
|
|
)}
|
|
</Link>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</li>
|
|
)
|
|
})}
|
|
</ul>
|
|
</main>
|
|
)
|
|
}
|