Everything about md.config.ts, route patterns, handler functions, and integration strategies.
The configuration file is the single source of truth for all your markdown routes. It exports an array of route handlers created with createMdVersion:
import { createMdVersion } from 'next-md-negotiate';
export const mdConfig = [
createMdVersion('/', async () => {
return '# Home\n\nWelcome to our site.';
}),
createMdVersion('/products/[productId]',
async ({ productId }) => {
const p = await db.products.find(productId);
return `# ${p.name}\n\n**Price:** $${p.price}`;
}),
createMdVersion('/blog/[...slug]',
async ({ slug }) => {
const post = await getPost(slug);
return post.markdown;
}),
];The main function for defining markdown routes. It takes a pattern, a handler, and optional configuration:
createMdVersion(pattern, handler, options?)| Param | Type | Description |
|---|---|---|
pattern | string | Next.js-style route pattern (/path, /path/[param], /path/[...slug]) |
handler | (params) => Promise<string> | Async function that receives extracted params and returns markdown |
options | object | Optional configuration (see below) |
| Option | Type | Default | Description |
|---|---|---|---|
hintText | string | default message | Custom LlmHint message for this route |
skipHint | boolean | false | Skip LlmHint injection for this route |
Patterns follow Next.js App Router conventions:
// Static
'/about' → matches /about
// Dynamic
'/products/[id]' → matches /products/42
'/[org]/[repo]' → matches /vercel/next.js
// Catch-all
'/docs/[...slug]' → matches /docs/a/b/c
// Root
'/' → matches /TypeScript automatically infers the correct parameter types from your route pattern using the ExtractParams utility type:
// TypeScript knows the exact params
'/products/[productId]'
→ { productId: string }
'/[org]/[repo]'
→ { org: string; repo: string }
'/docs/[...slug]'
→ { slug: string }
'/'
→ {}There are two ways to connect content negotiation to your Next.js routing: rewrites and middleware.
Uses Next.js native rewrite rules with header conditions. Zero runtime overhead — rewrites are evaluated by the Next.js router before your code runs.
// next.config.ts
import { createRewritesFromConfig } from 'next-md-negotiate';
import { mdConfig } from './md.config';
export default {
async rewrites() {
return {
beforeFiles: createRewritesFromConfig(mdConfig),
};
},
}Uses Next.js middleware to intercept requests. Gives you more control but adds slight runtime overhead.
// middleware.ts
import { createNegotiatorFromConfig } from 'next-md-negotiate';
import { mdConfig } from './md.config';
const negotiate = createNegotiatorFromConfig(mdConfig);
export function middleware(request) {
const response = negotiate(request);
if (response) return response;
}| Aspect | Rewrites | Middleware |
|---|---|---|
| Performance | Zero overhead | Slight overhead |
| Flexibility | Limited | Full control |
| Setup | next.config.ts | middleware.ts |
| Best for | Most projects | Custom logic needed |
Both strategies route matched requests to an internal handler. For App Router, this lives at app/md-api/[[...path]]/route.ts:
import { createMdHandler } from 'next-md-negotiate';
import { mdConfig } from '@/md.config';
export const GET = createMdHandler(mdConfig);createMdApiHandler instead, which returns a Next.js API route handler compatible with NextApiRequest / NextApiResponse.