# Serve Your WordPress Posts as Markdown

LLMs and agents work better with Markdown than rendered HTML. Fewer tokens, no theme chrome to parse around. [`roots/post-content-to-markdown`](https://github.com/roots/post-content-to-markdown) adds a Markdown representation to any WordPress site without a separate API or route.

We've written before about [SEO plugins that advertise Markdown for AI but ignore the `Accept` header entirely,](https://roots.io/some-seo-plugins-claim-markdown-for-ai-but-ignore-the-accept-header/) returning HTML to anything that asks. Post Content to Markdown does it properly: real content negotiation, block conversion, and `Link` headers plus `<link>` tags so LLMs that don't send `Accept: text/markdown` can still discover the Markdown version.

> **Is your WordPress site AI-ready?**
> Run any URL through the readiness checker at [acceptmarkdown.com](https://acceptmarkdown.com/) — it'll tell you what's missing for agents and LLMs, with links to relevant guides. See [acceptmarkdown.com/status](https://acceptmarkdown.com/status) for how the major agents stack up today.

## What it does

The plugin gives every post and feed a Markdown representation that clients can request three ways:

- An `Accept: text/markdown` request header, for agents and scripts
- A `?format=markdown` query parameter
- A `.md` URL suffix, e.g. `/hello-world.md`. The HTML version of the post advertises this URL back via a `Link: rel="alternate"; type="text/markdown"` response header and a matching `<link>` in `<head>`, so agents that [don't yet support the `text/markdown` Accept header](https://acceptmarkdown.com/status) can follow the link instead.

The same `Hello world!` post, two ways:

```
$ curl https://example.com/hello-world/
<html lang="en-US">…2,400 lines of theme markup…</html>

$ curl -H "Accept: text/markdown" https://example.com/hello-world/
# Hello world!

Welcome to WordPress. This is your first post. Edit or delete it, then start writing!
```

## Conversion that doesn't lose your content

- Renders Gutenberg blocks first, then converts, so dynamic blocks, embeds, and tables come through correctly
- Strips syntax-highlighter wrapper markup (Prism, highlight.js, etc) so code blocks aren't full of `<span class="token …">` noise
- Caches the conversion at the object-cache layer (Redis/Memcached) keyed on a content hash, so repeat hits skip block rendering, shortcode expansion, and the HTML → Markdown pass entirely

## Feeds and comments

The same content negotiation works on feeds:

- `/feed/markdown/` is a dedicated Markdown feed with site metadata, post titles, authors, dates, categories, tags, and full content
- `/feed/` with `Accept: text/markdown` returns the main feed as Markdown
- `/post-slug/feed/` with `Accept: text/markdown` returns the post plus all of its comments

The Markdown feed is also advertised in the RSS feed via an `<atom:link rel="alternate" type="text/markdown">`, so feed readers and agents can discover it on their own.

## Filters

Filters cover the common cases:

- `post_content_to_markdown/post_types` opts pages or custom post types in (default: `['post']`)
- `post_content_to_markdown/post_allowed` is a per-post allowlist that runs after the post type check
- `post_content_to_markdown/converter_options` controls header style, hard breaks, and removed nodes
- `post_content_to_markdown/conversion_cache_duration` shortens the TTL if you have request-aware blocks
- `post_content_to_markdown/markdown_output` is a final pass over the converted Markdown that runs on every request, outside the cache, so per-request customization works without invalidating cached entries

The README has the [full list](https://github.com/roots/post-content-to-markdown#filters).

## Standards-compliant content negotiation

A lot of "Markdown for AI" plugins skip the HTTP details. Post Content to Markdown follows [RFC 9110 §12.5.1](https://www.rfc-editor.org/rfc/rfc9110#name-proactive-negotiation):

- q-values are parsed, so `Accept: text/html;q=0.9, text/markdown;q=1.0` correctly prefers Markdown
- `Vary: Accept` is emitted on every front-end response, so browsers, proxies, and CDNs key on the Accept header and don't cross-serve an HTML body to a Markdown client
- A `406 Not Acceptable` is returned when the client's `Accept` header rules out every representation the site can serve, instead of silently falling back to HTML
- `X-Markdown-Source: accept | md-url | query` is set on Markdown responses so you can see in your access logs how clients are asking
- `.md` URL responses include `X-Robots-Tag: noindex, nofollow` so search engines don't index the Markdown alias next to the canonical HTML page

## Recent releases

Some of the recent changes to the plugin:

- **v1.3** ([\#5](https://github.com/roots/post-content-to-markdown/pull/5)) renders Gutenberg blocks before conversion and converts tables properly
- **v1.5** ([\#8](https://github.com/roots/post-content-to-markdown/pull/8), [\#9](https://github.com/roots/post-content-to-markdown/pull/9)) makes content negotiation spec-correct: q-value parsing, `Vary: Accept`, and `406 Not Acceptable`
- **v1.6** ([\#11](https://github.com/roots/post-content-to-markdown/pull/11)) adds the `.md` URL suffix with `Link` and `<link>` advertising
- **v1.7** ([\#12](https://github.com/roots/post-content-to-markdown/pull/12)) adds object-cache memoization, the `<atom:link>` autodiscovery in RSS, and the `X-Markdown-Source` response header

The full history lives on [GitHub](https://github.com/roots/post-content-to-markdown/releases).

## Install it

```
composer require roots/post-content-to-markdown
```

Activate the plugin and existing posts immediately have a Markdown representation. The source is on [GitHub](https://github.com/roots/post-content-to-markdown), and issues and PRs welcome.