vibecheck0.3.0
AI readinessaeomarkdown-negotiation-missinginfo

No markdown content negotiation in Next.js

When an agent requests your page with Accept: text/markdown and gets HTML back, it has to parse the full DOM — navigation, scripts, styling — to recover the content. Offering a markdown representation of the same URL hands agents clean, structured text directly. It’s an advanced, optional signal, but a strong one for agent-first sites and docs.

Symptoms

  • Requesting the page with Accept: text/markdown returns HTML
  • Agents parse noisy DOM instead of clean content
  • No lightweight text representation for programmatic consumers

How VibeCheck catches it

In your widget · Problems

infoaeoNo markdown content negotiation

To your coding agent · MCP

agent › get_detected_issues
{ detector: "aeo", issue: "No markdown content negotiation", threshold: "A request with Accept: text/markdown still returns an HTML content type" }

The same string in your widget and in your agent’s context — no screenshot, no copy-paste.

Root causes

  • The server ignores the Accept header and always returns HTML
  • No markdown source or renderer exposed for content routes

The fix for Next.js

Branch on the Accept header inside a Route Handler (or Middleware) and return the markdown source with the right content type and Vary header.

app/blog/[slug]/route.ts (or middleware)ts
export async function GET(req: Request, { params }) {
  const post = await getPost((await params).slug)
  if (req.headers.get('accept')?.includes('text/markdown')) {
    return new Response(post.markdown, {
      headers: { 'Content-Type': 'text/markdown; charset=utf-8', Vary: 'Accept' },
    })
  }
  return new Response(renderHtml(post), { headers: { 'Content-Type': 'text/html' } })
}

Steps

  1. Detect Accept: text/markdown on your content routes
  2. Return the markdown representation with a text/markdown content type
  3. Send Vary: Accept so caches don’t mix HTML and markdown responses

See the general, framework-agnostic fix →

FAQ

Is markdown negotiation required?
No — it’s an optional, advanced AEO signal, most valuable for docs and agent-facing sites. VibeCheck reports it as info, not an error. Skip it if your content isn’t markdown-backed.
Why send Vary: Accept?
So shared caches and CDNs store the HTML and markdown responses separately per Accept header, instead of serving one to a client that asked for the other.
How is this different from llms.txt?
llms.txt is one static index of your key content for LLMs; markdown negotiation serves a clean markdown version of any URL on request. They complement each other — llms.txt points agents at the content, negotiation delivers it without the DOM noise.