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>
44 lines
1.5 KiB
TypeScript
44 lines
1.5 KiB
TypeScript
import type { Settings } from "@/db/schema";
|
|
|
|
export const FEED_PATH = "/feed.xml";
|
|
|
|
/**
|
|
* Canonical + RSS-autodiscovery alternates for a public page. Next.js
|
|
* merges metadata SHALLOWLY per key, so a page that sets only
|
|
* `alternates.canonical` would wipe the RSS link inherited from the
|
|
* root layout — always set both through this helper.
|
|
*/
|
|
export function pageAlternates(canonical: string) {
|
|
return {
|
|
canonical,
|
|
types: { "application/rss+xml": FEED_PATH },
|
|
};
|
|
}
|
|
|
|
/**
|
|
* The site's public origin for canonical URLs, feeds, and the sitemap.
|
|
* Preference order: the admin-configured setting, the SITE_URL env var,
|
|
* then localhost so development still produces valid (if wrong) URLs.
|
|
* Always returned without a trailing slash.
|
|
*/
|
|
export function resolveSiteUrl(settings: Pick<Settings, "siteUrl">): string {
|
|
const configured = settings.siteUrl || process.env.SITE_URL || "http://localhost:3000";
|
|
return configured.replace(/\/+$/, "");
|
|
}
|
|
|
|
export function absoluteUrl(siteUrl: string, path: string): string {
|
|
return `${siteUrl}${path.startsWith("/") ? path : `/${path}`}`;
|
|
}
|
|
|
|
/**
|
|
* Rewrites site-relative src/href attributes ("/uploads/…", "/posts/…")
|
|
* to absolute URLs. Feed readers resolve nothing relative to the feed,
|
|
* so stored HTML must be absolutified before it goes into RSS.
|
|
*/
|
|
export function absolutifyHtml(html: string, siteUrl: string): string {
|
|
return html.replace(
|
|
/(src|href)="(\/(?!\/)[^"]*)"/g,
|
|
(_m, attr: string, path: string) => `${attr}="${siteUrl}${path}"`,
|
|
);
|
|
}
|