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@graphwithOrganization, the doc schema, andBreadcrumbList.schema(doc, opts?)returns just the doc schema (no Organization or breadcrumbs).breadcrumbsSchema(doc, breadcrumbs?, opts?)returns just theBreadcrumbList.- Pass
opts.extendto 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
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
import { getRobots } from '@next-md-blog/core/next';
import { site } from '@/next-md-blog.config';
export default () => getRobots(site);RSS
import { blog } from '@/next-md-blog.config';
export async function GET() {
return blog.rssResponse();
}Per-locale feeds at /[lang]/feed.xml:
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
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.
Per-locale RSS link tags
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}` })),
],
},
},