Setup
Add the md plugin to your Next.js config, then the route handler that renders and converts pages.
With the Affordances graph, add md() to withAffordances's plugins:
import { withAffordances } from '@affordances/next/config'
import { md } from '@affordances/md/next'
export default withAffordances(nextConfig, { name: 'Store', plugins: [md()] })On its own, wrap the config with withMd:
import { withMd } from '@affordances/md/next'
export default withMd(nextConfig)Then add the route handler. init writes it:
vlx @affordances/md initnpx @affordances/md initpnpm dlx @affordances/md inityarn dlx @affordances/md initbunx @affordances/md initdeno run -A npm:@affordances/md init
It's one line:
export { GET } from '@affordances/md/next'Routing
The config only adds rewrites, so nothing runs per request until an agent asks:
- A page requested with
Accept: text/markdownis rewritten to the handler. API routes,/_next, and URLs with a file extension are left alone. /blog/post.mdand/index.md(for/) are rewritten after Next's own files, so a real.mdfile inpublic/still wins.- The handler's own request for the page carries an
x-affordances-md-renderheader, which the rewrites skip, so it can't loop.
Next matches Accept as a pattern, so a header that lists text/markdown with a low q-value still gets Markdown. Browsers never send it, so in practice only agents do.
Advertise it
mdAlternate(path) returns metadata alternates for a page's Markdown twin:
import { mdAlternate } from '@affordances/md/next'
export const generateMetadata = async ({ params }) => {
const { slug } = await params
return { alternates: { canonical: `/blog/${slug}`, ...mdAlternate(`/blog/${slug}`) } }
}That renders <link rel="alternate" type="text/markdown" href="/blog/post.md">. For /, it points at /index.md.