blog.hangerthem.com/app/(withRecent)/categories/page.tsx
HangerThem fd96e4b7fc feat(seo): sitemap, robots, canonical URLs, OG images and JSON-LD
- 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>
2026-10-19 14:22:40 +02:00

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>
)
}