Core Concepts

How HTTP content negotiation works and how next-md-negotiate implements it for Next.js applications.

What is content negotiation?

Content negotiation is a mechanism defined in the HTTP specification that allows a client and server to agree on the best representation of a resource. The client sends an Accept header indicating what content types it can handle, and the server responds with the most appropriate format.

This is not new or experimental — it has been part of HTTP since the beginning. When your browser requests a web page, it sends Accept: text/html. When an API client requests JSON, it sends Accept: application/json.

The Accept header

The Accept header tells the server what content types the client understands. next-md-negotiate looks for these markdown types:

  • text/markdown — the standard MIME type
  • application/markdown — alternative registration
  • text/x-markdown — legacy vendor prefix

If the Accept header contains any of these types and the requested URL matches a configured route, the server returns markdown instead of HTML.

Browsers vs LLM agents

The key difference between browsers and LLM agents is the Accept header they send:

ClientAccept HeaderGets
Chrome, Firefox, Safaritext/htmlNormal HTML page
LLM agent / AI crawlertext/markdownMarkdown document
curl (default)*/*Normal HTML page
curl (with header)text/markdownMarkdown document

Browsers never send text/markdown in their Accept header, so they always receive the normal HTML page. Your site looks and works identically for human visitors.

Route patterns

next-md-negotiate supports all standard Next.js route patterns:

Static routes

createMdVersion('/about', async () => { return '# About Us\n...'; });

Dynamic parameters

// [param] → { param: string } createMdVersion('/products/[productId]', async ({ productId }) => { ... } ); // Multiple params createMdVersion('/[org]/[repo]', async ({ org, repo }) => { ... } );

Catch-all routes

// [...slug] captures nested paths createMdVersion('/docs/[...slug]', async ({ slug }) => { // /docs/getting-started → { slug: 'getting-started' } // /docs/api/auth/tokens → { slug: 'api/auth/tokens' } } );

The request flow

Here is exactly what happens when a request arrives at your Next.js app with content negotiation configured:

  1. Request arrives — a client sends a GET request to your URL
  2. Accept header check — the library inspects the Accept header for markdown MIME types
  3. Route matching — the URL is matched against your configured patterns in mdConfig
  4. Rewrite or passthrough — if both checks pass, the request is rewritten to the internal /md-api/... handler; otherwise it passes through unchanged
  5. Handler execution — your handler function runs, receives the extracted parameters, and returns a markdown string
  6. Response — the markdown is returned with Content-Type: text/markdown; charset=utf-8 and status 200
First match wins: Routes in mdConfig are checked in order. The first matching route handles the request. If your patterns could overlap, put more specific routes first.

Single source of truth

All your markdown routes are defined in one place: md.config.ts. This file is used by both the route handler (to match requests and generate markdown) and the routing layer (to generate rewrites or middleware logic). There is no duplication between your page routes and your markdown routes.