Skip to Content
SEO & feeds

SEO & feeds

Every SEO surface lives on the Collection object you got back from defineCollection (the collection API, introduced in v1.2). Site-wide aggregation (sitemap, llms.txt) is done with composeSitemap / composeLlmsTxt.

Per-page metadata

// app/blog/[slug]/page.tsx import { blog } from '@/next-md-blog.config'; export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; const post = await blog.getOne(slug); if (!post) return { title: 'Not found' }; return blog.metadata(post); }

metadata() populates <title> (using defaults.titleTemplate or opts.titleTemplate), description, canonical, openGraph, twitter, robots, hreflang (alternates.languages), and author / keyword metadata.

For app/[lang] routing, pass locale and pre-compute hreflang siblings:

const alternateLanguages = await blog.hreflangMap(slug, LOCALES); return blog.metadata(post, { locale, titleTemplate: 'absolute', alternateLanguages });

JSON-LD

const jsonLd = blog.schemaGraph(post, undefined, { locale, speakable: true }); // → { "@context": ..., "@graph": [Organization, BlogPosting, BreadcrumbList] }
  • schemaGraph(doc, breadcrumbs?, opts?) returns the combined @graph with Organization, the doc schema, and BreadcrumbList.
  • schema(doc, opts?) returns just the doc schema (no Organization or breadcrumbs).
  • breadcrumbsSchema(doc, breadcrumbs?, opts?) returns just the BreadcrumbList.
  • Pass opts.extend to mutate the doc node freely.

FAQPage & HowTo (since v1.3)

If a post’s frontmatter has faq or howto, schemaGraph automatically appends a Schema.org FAQPage and/or HowTo node to the @graph — nothing else to call. See Content & frontmatter for the frontmatter shapes.

// post.frontmatter.faq / .howto set → graph includes FAQPage / HowTo const jsonLd = blog.schemaGraph(post); // → { "@graph": [Organization, BlogPosting, BreadcrumbList, FAQPage, HowTo] }

Inline a sanitized <script type="application/ld+json"> yourself:

<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd).replace(/</g, '\\u003c').replace(/&/g, '\\u0026'), }} />

Sitemap

app/sitemap.ts
import { composeSitemap } from '@next-md-blog/core'; import { blog, glossary, site } from '@/next-md-blog.config'; const LOCALES = [site.defaultLang ?? 'en'] as const; export default async function sitemap() { return composeSitemap({ collections: [blog, glossary], locales: LOCALES, staticEntries: [{ url: `${site.siteUrl}/${LOCALES[0]}`, lastModified: new Date(), priority: 1 }], }); }

Each row includes xhtml:link rel="alternate" hreflang="…" for every locale that has a translation, plus x-default. lastModified uses max(frontmatter.date, frontmatter.updated).

Robots

app/robots.ts
import { getRobots } from '@next-md-blog/core/next'; import { site } from '@/next-md-blog.config'; export default () => getRobots(site);

RSS

app/feed.xml/route.ts
import { blog } from '@/next-md-blog.config'; export async function GET() { return blog.rssResponse(); }

Per-locale feeds at /[lang]/feed.xml:

app/[lang]/feed.xml/route.ts
import { blog, LOCALES } from '@/next-md-blog.config'; export async function generateStaticParams() { return LOCALES.map((lang) => ({ lang })); } export async function GET(_req: Request, { params }: { params: Promise<{ lang: string }> }) { const { lang } = await params; return blog.rssResponse({ locale: lang }); }

rssResponse sets Content-Type and Cache-Control: public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400 by default.

llms.txt + llms-full.txt

app/llms.txt/route.ts
import { composeLlmsTxt } from '@next-md-blog/core'; import { blog, glossary, site, LOCALES } from '@/next-md-blog.config'; export async function GET() { const body = await composeLlmsTxt({ site, collections: [blog, glossary], locales: LOCALES, }); return new Response(body, { headers: { 'content-type': 'text/plain; charset=utf-8' } }); }

The same shape works for composeLlmsFullTxt (concatenated post bodies).

Hreflang

When a post has the same slug across multiple locales, use getInAllLocales (or its convenience wrapper hreflangMap) to build the alternateLanguages map for metadata().

Per-post overrides via frontmatter.alternateLanguages (a Record<hreflang, url>) win over what hreflangMap computes.

In your layout’s metadata:

alternates: { types: { 'application/rss+xml': [ { url: `/${lang}/feed.xml`, title: `${site.siteName} — ${lang}` }, ...OTHER_LOCALES.map((l) => ({ url: `/${l}/feed.xml`, title: `${site.siteName} — ${l}` })), ], }, },
Last updated on