SEO metadata & sitemap
Every Article carries the fields Next needs for generateMetadata and app/sitemap.ts. There’s no SEO helper to learn — you map the fields you already have. For the full schema-markup story (JSON-LD from author.kind), see SEO & JSON-LD.
generateMetadata
Section titled “generateMetadata”seoTitle is always a string (it falls back to title server-side). seoDescription and canonicalUrl are genuinely optional, so coalesce the nulls — and only set alternates.canonical when a canonical URL is present:
import type { Metadata } from "next";import { notFound } from "next/navigation";import { gw } from "@/lib/ghostwritr";
export async function generateMetadata({ params,}: { params: Promise<{ slug: string }>;}): Promise<Metadata> { const article = await gw.getArticle((await params).slug); if (!article) return {}; return { title: article.seoTitle, // always a string description: article.seoDescription ?? undefined, // null → omit alternates: article.canonicalUrl ? { canonical: article.canonicalUrl } // only when set : undefined, };}You can enrich further from the same object — article.image for Open Graph (url, alt, width, height), article.inLanguage for <html lang>, article.tags for keywords:
return { title: article.seoTitle, description: article.seoDescription ?? undefined, openGraph: article.image ? { images: [{ url: article.image.url, alt: article.image.alt }] } : undefined,};app/sitemap.ts
Section titled “app/sitemap.ts”There’s no sitemap helper by design — build it from getArticles(). Map each article to a { url, lastModified } entry, using canonicalUrl when present and falling back to your own route, and updatedAt for lastModified:
import type { MetadataRoute } from "next";import { gw } from "@/lib/ghostwritr";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> { const articles = await gw.getArticles(); return articles.map((a) => ({ url: a.canonicalUrl ?? `https://yoursite.com/blog/${a.slug}`, lastModified: a.updatedAt, // ISO-8601 string }));}getArticles() returns newest-first, and updatedAt is an ISO-8601 string that Next accepts directly for lastModified.
Keep the sitemap fresh
Section titled “Keep the sitemap fresh”app/sitemap.ts reads the feed, so it caches with your rendering mode like any other route. If you use instant revalidation, add the sitemap path so it refreshes on publish alongside your articles:
import { createRevalidateHandler } from "@ghostwritr/next/revalidate";
export const POST = createRevalidateHandler({ secret: process.env.GHOSTWRITR_FEED_SECRET!, tags: ["ghostwritr"], paths: ["/blog", "/sitemap.xml"],});What to reach for next
Section titled “What to reach for next”- SEO & JSON-LD — emit
Article+Person/Organizationstructured data fromauthor.kind. - The article shape — every field, and which are nullable.
- Instant revalidation — bust the sitemap on publish.