A new "Site URL" setting (validated, trailing-slash-normalized, with SITE_URL env fallback) anchors every absolute URL. On top of it: - /feed.xml — RSS 2.0 with the 20 newest published posts: excerpt description, full sanitized HTML in content:encoded (relative image and link URLs rewritten to absolute, since readers resolve nothing), categories from tags, dc:creator, and a self atom:link. Autodiscovery <link> on every public page and a footer link. - /sitemap.xml — home, post list, every published post and page (lastmod from updatedAt), and publicly visible tags. Rendered per request like the rest of the site so it never goes stale; drafts never appear. - /robots.txt — allow all, disallow /admin/ and /api/, sitemap pointer. Admin pages also carry noindex meta as a second layer. - Page metadata: metadataBase + canonical URLs everywhere, Open Graph (article type with published/modified times, author, and tags on posts; og:image + summary_large_image card when there's a featured image), and BlogPosting JSON-LD on post pages. Gotcha encoded in lib/seo.ts: Next merges metadata shallowly, so pages setting alternates.canonical alone would wipe the layout's RSS autodiscovery entry — pageAlternates() always sets both. Backups gain settings.siteUrl (export v4; older files still import). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
27 lines
947 B
TypeScript
27 lines
947 B
TypeScript
import type { Metadata } from "next";
|
|
import { notFound } from "next/navigation";
|
|
import { PageArticle } from "@/components/public/PageArticle";
|
|
import { pageAlternates } from "@/lib/seo";
|
|
import { getPublishedPageBySlug } from "@/lib/services/pages";
|
|
|
|
type Props = { params: Promise<{ slug: string }> };
|
|
|
|
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
const { slug } = await params;
|
|
const page = await getPublishedPageBySlug(slug);
|
|
if (!page) return {};
|
|
return {
|
|
title: page.title,
|
|
alternates: pageAlternates(`/pages/${page.slug}`),
|
|
openGraph: { title: page.title, url: `/pages/${page.slug}` },
|
|
};
|
|
}
|
|
|
|
export default async function StaticPage({ params }: Props) {
|
|
const { slug } = await params;
|
|
// Draft pages are filtered inside the query — they 404 like unknown slugs.
|
|
const page = await getPublishedPageBySlug(slug);
|
|
if (!page) notFound();
|
|
return <PageArticle page={page} />;
|
|
}
|