Skip to content

Markdown versions checker

Enter your domain or your llms.txt URL. We fetch your homepage and up to 25 pages your llms.txt links, and for each one report whether it has a markdown version, how a client finds it, and whether the version says the same thing as the page. We obey your robots.txt for our tool token, LLMSRank-User: a page it disallows is skipped, never fetched, and listed in the results. If it disallows your homepage, no check is made. Your llms.txt and robots.txt are always read.

This is a different sample from the scan: the scan looks at pages chosen by its own rules, this check looks at the pages your llms.txt points to. It is the tool behind the markdown version, llms.txt link choice and negotiation audits. The comparison reads the server HTML only, so a page built by JavaScript can look shorter than it is. Recipes and claims checked 2026-09-29. This page doesn’t claim that any AI product reads markdown versions; it helps you publish them correctly.

Checks the homepage and up to 25 pages your llms.txt links, about 10 to 60 seconds. It counts as one of your five free scans a day. Without JavaScript the form posts to the API and shows its JSON reply, which holds a status link to open; the results table needs JavaScript.

What each column means

  • Page: the HTML status of the URL.
  • Declared alternate: a rel="alternate" type="text/markdown" link in the head or the Link header, and whether that URL answers with a non-HTML file.
  • Guessed .md twin: the page URL plus .md (or /index.md). A 200 that is HTML is reported as “HTML served at .md” and is not a twin.
  • describedby: a rel="describedby" link, usually to your llms.txt.
  • llms.txt links: whether your llms.txt links the page’s markdown URL, its HTML URL or both.
  • Accept: text/markdown: whether the page URL answers markdown when asked for it, and whether the response carries Vary: Accept so caches keep the two answers apart.
  • Parity: the verdict, the shared-word-run share (Jaccard), the word counts and any H2 headings the markdown lacks.

How we compare a page with its markdown version

Method dated 2026-09-29.

A diagnostic, not a score

This check tells you whether your markdown version says the same thing as the web page it stands for. It does not count toward your LLMSRank score and does not change any audit result.

Nothing here says that an AI product reads markdown versions or that matching ones rank better. It only compares two files.

What we compare

From the web page we take the text a visitor would read: we drop scripts, styles, navigation, headers, footers and sidebars, and use the <main> element when the page has one. We read the HTML your server sends and do not run JavaScript, so text that only appears after scripts run is not counted.

From the markdown version we take the text without markup. Both texts are split into overlapping runs of 5 words, and we report the share of runs the two have in common (Jaccard similarity: shared runs divided by all distinct runs). 100% means the same words in the same order.

We also list H2 headings on the page that the markdown version lacks, and links that point somewhere different.

Verdicts

Matches: 70% or more of the word runs are shared.

Partial: 40% up to 70%.

Differs: under 40%.

Stub: the markdown version has fewer than 15% as many words as the page. This is reported instead of the other three.

How we set the thresholds

On 2026-09-29 we compared 39 page and markdown pairs from 30 sites that already publish markdown versions, and read each pair ourselves. Every pair at 70% or more carried the same content. Faithful versions from documentation tools often land between 70% and 80%, because the web page repeats tab labels and button text ("curl Python JavaScript", "Copy page") between blocks. No pair fell between 30% and 53%, so the partial line sits in that gap. The two stubs we found had 2% and 4% of the page's words.

Most of the sample is developer documentation, so the thresholds are best tested there. We will revisit them as more sites publish markdown versions.

When the verdict can mislead

Short or link-heavy pages (indexes, hubs, card grids): a few extra words such as "Read more" or item numbers break many word runs, so a faithful version can read as partial or differs.

Pages without navigation markup: menus and page tools that are not in <nav>, <header> or <aside> count as page text, so a clean markdown version looks shorter than the page.

API reference pages: some documentation tools publish the OpenAPI definition as the markdown version while the page shows code samples. Both describe the same endpoint, but the words differ.

Pages built by JavaScript: when the server sends little text, the markdown version can hold several times more words than the page, and the verdict says differs.

Rewritten or out-of-date versions: a summary written for AI readers, or a version not regenerated after the page changed, is reported as differs. That is the point of the check, but look at the headings and links lists before assuming the version is wrong.

Translated pages: compare a page with the markdown version in the same language. Different languages share almost no word runs.

Add markdown versions on your platform

Each recipe says what the platform documents and where we wrote the code ourselves. “Not tested by us” means we have not yet run it on that platform. Recipes and claims checked 2026-09-29.

Next.js (App Router) Not tested by us

Next.js has no built-in setting for this: PR #98807 (markdownAgents) was still open on 2026-09-29. A Next.js maintainer’s answer (discussion #90579, 2026-02-26) is a rewrites() rule on the accept request header that sends the request to a Route Handler returning Content-Type: text/markdown; charset=utf-8 and Vary: Accept.

  1. List the pages that have a markdown version, and write getMarkdown(page) from the same content your pages render.
  2. Add the rewrites and headers to next.config.mjs: /page.md URLs, the same page URL answering Accept: text/markdown, and a Link header on the HTML page naming its markdown version and the llms.txt.
  3. Add the Route Handler at app/md/[[...slug]]/route.ts. It answers text/markdown; charset=utf-8 with Vary: Accept, and names the HTML page as canonical.
  4. Check with curl, then with our tool.

next.config.mjs

// next.config.mjs
// Pages that have a markdown version. Only these get the Accept rewrite and the Link header,
// so every other page keeps serving HTML and never advertises a 404.
const MARKDOWN_PAGES = ['/', '/pricing', '/docs/getting-started'];
const twinOf = (page) => (page === '/' ? '/index.md' : `${page}.md`);
const WANTS_MARKDOWN = { type: 'header', key: 'accept', value: '(.*)text/markdown(.*)' };

export default {
  async rewrites() {
    return {
      beforeFiles: [
        // /pricing.md (and /index.md) -> the markdown route handler
        { source: '/:path*.md', destination: '/md/:path*' },
        // /pricing with Accept: text/markdown -> the same handler, same URL for the client
        ...MARKDOWN_PAGES.map((page) => ({
          source: page,
          has: [WANTS_MARKDOWN],
          destination: page === '/' ? '/md' : `/md${page}`,
        })),
      ],
    };
  },
  async headers() {
    // HTML responses only (the handler sets its own headers on markdown responses).
    return MARKDOWN_PAGES.map((page) => ({
      source: page,
      missing: [WANTS_MARKDOWN],
      headers: [
        { key: 'Link', value: `<${twinOf(page)}>; rel="alternate"; type="text/markdown", </llms.txt>; rel="describedby"` },
        { key: 'Vary', value: 'Accept' },
      ],
    }));
  },
};

app/md/[[...slug]]/route.ts

// app/md/[[...slug]]/route.ts
import { getMarkdown } from '@/lib/markdown'; // yours: the page's markdown, or null

const MARKDOWN = 'text/markdown; charset=utf-8';

export async function GET(_request: Request, { params }: { params: Promise<{ slug?: string[] }> }) {
  const { slug = [] } = await params;
  const page = slug.length === 0 || slug.join('/') === 'index' ? '/' : `/${slug.join('/')}`;
  const markdown = await getMarkdown(page);
  if (markdown === null) {
    return new Response('# Not found\n', { status: 404, headers: { 'Content-Type': MARKDOWN, Vary: 'Accept' } });
  }
  return new Response(markdown, {
    headers: {
      'Content-Type': MARKDOWN,
      Vary: 'Accept',
      // The llms.txt that covers the page, and the HTML page as canonical.
      Link: `</llms.txt>; rel="describedby", <${page}>; rel="canonical"`,
    },
  });
}

check

curl -sI https://example.com/pricing | grep -iE '^(link|vary):'
curl -s -H 'Accept: text/markdown' https://example.com/pricing | head
curl -sI https://example.com/pricing.md | grep -iE '^(content-type|link):'

Sources for this recipe:

Sources: Next.js discussion #90579: a maintainer’s answer on serving markdown for Accept: text/markdown (rewrite on the accept header to a route handler) (checked 2026-09-29); Next.js PR #98807 “feat: add markdownAgents content negotiation” (open; last activity 2026-09-17) (checked 2026-09-29); Next.js source: setVaryHeader in base-server.ts (appends its own Vary values to page responses; read in next 15.5.20) (checked 2026-09-29); Vercel blog: Making agent-friendly pages with content negotiation (checked 2026-09-29); Vercel Knowledge Base: How to serve documentation for agents (checked 2026-09-29); RFC 7763: The text/markdown Media Type (checked 2026-09-29); The Link and Content-Type headers our own pages and .md versions send (checked 2026-09-29); Markdown page versions and link relations (our guide, incl. “Don’t advertise a 404”) (checked 2026-09-29)
Vercel, any framework (vercel.json) Not tested by us

Vercel has no project setting for markdown versions. Vercel’s own docs answer in markdown at the page URL and at a .md URL, but that page describes Vercel’s docs site, not a customer setting. vercel.json headers and rewrites accept has and missing conditions on request headers.

  1. Build a .md file next to each page that has a markdown version (for example /pricing.md).
  2. Add the headers: text/markdown; charset=utf-8 on .md files, and on each HTML page a Link header naming its markdown version and the llms.txt.
  3. Add a rewrite per page on Accept: text/markdown, if the page is served by a function or framework route (see the caution).
  4. Deploy a preview and check with curl, then with our tool.

vercel.json

{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "headers": [
    {
      "source": "/(.*)\\.md",
      "headers": [
        { "key": "Content-Type", "value": "text/markdown; charset=utf-8" },
        { "key": "Link", "value": "</llms.txt>; rel=\"describedby\"" }
      ]
    },
    {
      "source": "/pricing",
      "headers": [{ "key": "Vary", "value": "Accept" }]
    },
    {
      "source": "/pricing",
      "missing": [{ "type": "header", "key": "accept", "value": "(.*)text/markdown(.*)" }],
      "headers": [
        { "key": "Link", "value": "</pricing.md>; rel=\"alternate\"; type=\"text/markdown\", </llms.txt>; rel=\"describedby\"" }
      ]
    }
  ],
  "rewrites": [
    {
      "source": "/pricing",
      "has": [{ "type": "header", "key": "accept", "value": "(.*)text/markdown(.*)" }],
      "destination": "/pricing.md"
    }
  ]
}

Caution. Vercel’s docs say a rewrite’s source “should NOT be a file because precedence is given to the filesystem prior to rewrites being applied”. On a static site, where /pricing is a file, the Accept rewrite won’t run; the .md URLs and Link headers still work. Sources: Vercel docs: Static Configuration with vercel.json (headers, rewrites, has and missing) (checked 2026-09-29)

Caution. Vercel’s docs say “Using has does not yet work locally while using vercel dev, but does work when deployed.” Test on a preview deployment. Sources: Vercel docs: Static Configuration with vercel.json (headers, rewrites, has and missing) (checked 2026-09-29)

Sources for this recipe:

Sources: Vercel docs: Static Configuration with vercel.json (headers, rewrites, has and missing) (checked 2026-09-29); Vercel docs: Markdown and Agent Discovery (how Vercel’s own docs serve markdown) (checked 2026-09-29)
Cloudflare Not tested by us

Markdown for Agents converts a page’s HTML to markdown at Cloudflare when the request’s Accept header asks for text/markdown. Cloudflare’s reference lists Pro, Business and Enterprise plans and SSL for SaaS customers; the Free plan is not listed. Turn it on in the dashboard under AI Crawl Control, or with the zone setting content_converter.

  1. Dashboard: AI Crawl Control → Markdown for Agents → on. Or use the API call below.
  2. Set your own content-signal header at your origin if you don’t want the default (see the caution).
  3. Check a page with Accept: text/markdown.
  4. For .md URLs, add the Worker below. It is our recipe: Cloudflare doesn’t document .md URLs.

turn on and check

curl -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/settings/content_converter" \
  -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"value":"on"}'

# Check: the same URL, asking for markdown
curl -sI -H 'Accept: text/markdown' https://example.com/pricing | grep -iE '^(content-type|vary|content-signal|x-markdown-tokens):'

worker.js — Our recipe, not Cloudflare’s. Cloudflare doesn’t document whether a Worker’s request to its own zone is converted; not verified. If it isn’t, the Worker answers 404 rather than serve HTML as markdown.

// worker.js: .md URLs on a zone with Markdown for Agents on. Our recipe; Cloudflare doesn't
// document .md URLs. /pricing.md asks the zone for /pricing with Accept: text/markdown and serves
// the answer only if it came back as markdown, so HTML is never labelled as markdown.
// Leave ADVERTISE off until you have checked that /pricing.md answers 200 on your zone.
const ADVERTISE = false;

const pageOf = (path) => (path === '/index.md' ? '/' : path.slice(0, -'.md'.length));
const twinOf = (path) => (path === '/' ? '/index.md' : `${path.replace(/\/$/, '')}.md`);

export default {
  async fetch(request) {
    const url = new URL(request.url);
    const isMd = url.pathname.endsWith('.md');
    if (!isMd || (request.method !== 'GET' && request.method !== 'HEAD')) {
      const response = await fetch(request);
      const type = response.headers.get('content-type') || '';
      if (!ADVERTISE || isMd || response.status !== 200 || !type.startsWith('text/html')) return response;
      const withLink = new Response(response.body, response);
      withLink.headers.append('Link', `<${twinOf(url.pathname)}>; rel="alternate"; type="text/markdown"`);
      withLink.headers.append('Vary', 'Accept');
      return withLink;
    }
    const page = new URL(url);
    page.pathname = pageOf(url.pathname);
    const upstream = await fetch(page, { method: request.method, headers: { Accept: 'text/markdown' } });
    const type = upstream.headers.get('content-type') || '';
    if (upstream.status !== 200 || !type.startsWith('text/markdown')) {
      return new Response('Not found\n', { status: 404, headers: { 'Content-Type': 'text/plain; charset=utf-8' } });
    }
    const headers = new Headers({
      'Content-Type': 'text/markdown; charset=utf-8',
      Link: `<${page.pathname}>; rel="canonical"`,
    });
    for (const name of ['content-signal', 'x-markdown-tokens', 'cache-control']) {
      const value = upstream.headers.get(name);
      if (value) headers.set(name, value);
    }
    return new Response(upstream.body, { status: 200, headers });
  },
};

Caution. When your origin doesn’t send a content-signal header, Markdown for Agents adds content-signal: ai-train=yes, search=yes, ai-input=yes to converted responses, which says the content may be used for AI training. If you disagree, send your own content-signal header from your origin; Cloudflare keeps the origin’s value. Sources: Cloudflare docs: Markdown for Agents (reference) (checked 2026-09-29); Cloudflare changelog: Introducing Markdown for Agents (checked 2026-09-29)

Sources for this recipe:

Sources: Cloudflare docs: Markdown for Agents (reference) (checked 2026-09-29); Cloudflare changelog: Introducing Markdown for Agents (checked 2026-09-29)
WordPress Not tested by us

We found no WordPress core setting for markdown versions. The plugins below add them. They are listed alphabetically; we don’t rank or endorse them. After installing one, check the result with our tool.

  1. Markdown Alternate (GitHub only, not in the WordPress.org directory; by Joost de Valk, ProgressPlanner): .md URLs, ?format=markdown, and a <link rel="alternate" type="text/markdown"> in each page’s <head>. A request with Accept: text/markdown gets a 303 redirect to the .md URL. Markdown responses send text/markdown; charset=UTF-8, Vary: Accept, a canonical Link and X-Markdown-Tokens.
  2. Post Content to Markdown (GitHub only, not in the WordPress.org directory; by Roots): Accept: text/markdown on the post’s own URL returns markdown, plus .md URLs, ?format=markdown and a /feed/markdown/ feed. It sends Vary: Accept, and advertises the markdown version with a Link header and a <link> element (rel="alternate"; type="text/markdown"). The charset in its Content-Type is the site’s own charset setting.
  3. Serve Markdown (serve-md, WordPress.org directory, by Azey): per its directory page, Accept: text/markdown, .md URLs and a <link rel="alternate" type="text/markdown"> in the page head, with YAML front matter and per-post opt-out. We haven’t read its source.
  4. Yoast SEO’s llms.txt feature: its help page describes generating an llms.txt file. It doesn’t describe markdown versions of pages.

Sources for this recipe:

Sources: Markdown Alternate (ProgressPlanner/markdown-alternate on GitHub), version 1.1.0; source read at the commit of 2026-06-15 (checked 2026-09-29); Joost de Valk: My WordPress take on Markdown for Agents (checked 2026-09-29); Post Content to Markdown (roots/post-content-to-markdown on GitHub), version 1.7.1; source read at the commit of 2026-05-07 (checked 2026-09-29); Serve Markdown (serve-md) in the WordPress.org plugin directory, version 1.0 (checked 2026-09-29); WordPress.org plugin directory API (slugs markdown-alternate and post-content-to-markdown: “Plugin not found”) (checked 2026-09-29); Yoast help: How to enable llms.txt (checked 2026-09-29)
Mintlify Not tested by us

Built in. Mintlify serves a markdown version at the page URL plus .md, and at the page URL for Accept: text/markdown or Accept: text/plain. It generates llms.txt and llms-full.txt; a file of your own with the same name replaces the generated one. It sends Link: </llms.txt>; rel="llms-txt" and X-Llms-Txt headers.

  1. Nothing to add for markdown versions.
  2. If you publish your own llms.txt, link the .md forms of your pages.
  3. Check with our tool.

Sources for this recipe:

Sources: Mintlify docs: Markdown export (checked 2026-09-29); Mintlify docs: llms.txt (checked 2026-09-29); Mintlify blog: content negotiation and Link / X-Llms-Txt headers for agents (checked 2026-09-29)
Netlify Not tested by us

Netlify publishes a prompt template for an AI coding assistant: it builds an Edge Function that converts HTML to markdown with Turndown when a request sends Accept: text/markdown.

  1. Read Netlify’s template.
  • The template’s header list includes Content-Signal: ai-train=yes, search=yes, ai-input=yes, which allows AI training; change it if you disagree. It doesn’t mention Vary: Accept; add it. Sources: Netlify docs: Markdown for Agents prompt template (checked 2026-09-29)

Sources for this recipe:

Sources: Netlify docs: Markdown for Agents prompt template (checked 2026-09-29)

What we know and don’t