roots/post-content-to-markdown: The WordPress Plugin That Speaks Markdown on Demand

One URL. Two formats. A feed for machines. This plugin turns old-school WordPress posts into clean Markdown for LLMs, static sites, and any workflow that prefers text over HTML.

6 to 7 min read • View on GitHub • More from roots

A single WordPress post is shown as one object with two faces. One side reads like a browser page, while the other side collapses into neat Markdown lines under an Accept: text/markdown header. The image explains that the same canonical URL can serve different representations without creating a second content system.
The plugin treats representation as a request-time decision, not a separate export job.
Key Takeaways

The Same Post Can Be Two Things

The useful surprise here is that a WordPress post does not need a new endpoint to become machine-readable. If the request says Accept: text/markdown, or if the URL carries ?format=markdown, the plugin serves Markdown instead of HTML from the same post page.

That is a cleaner move than bolting on a parallel API just to satisfy scripts and LLMs. The content stays canonical. The representation changes at the edge.

A WordPress plugin that returns post content in Markdown format when requested with an Accept header set to text/markdown or a ?format=markdown query parameter.

roots/post-content-to-markdown README, Project Documentation · roots/post-content-to-markdown

The plugin routes representations, it does not duplicate content. Cache and invalidation keep the expensive path practical.

Why Content Negotiation Still Matters

Content negotiation is old web muscle that most CMS workflows forget. Instead of inventing a different resource for every format, the server can vary the response based on what the client asks for.

ApproachHow content is exposedOperational overheadBest forWeakness
Traditional WordPress pageHTML only at the canonical URLLowHuman browsingMachine consumers need extra parsing or glue
Custom API or client-side conversionSeparate endpoint or front-end transformHighHighly custom integrationsParallel system, more maintenance, more failure points
roots/post-content-to-markdownSame URL, negotiated HTML or MarkdownModerateLLMs, static generation, text-first workflowsDepends on server-side conversion and careful caching

How Roots Wires It Into WordPress

The implementation is intentionally narrow. The main plugin file hooks into template_redirect, checks whether the request wants Markdown, and intercepts output before WordPress renders the normal template.

That keeps the logic close to the request lifecycle. It also makes the behavior easy to reason about: detect, convert, cache, emit.

A hedcut-style portrait of Ben Word, known as retlehs. The image identifies the project’s main contributor and shows the person behind the plugin’s opinionated implementation choices.

Under the hood, the plugin leans on league/html-to-markdown for conversion and exposes filters so other code can change which post types qualify or how conversion behaves. The output path is also guarded by caching, because repeated conversion on every request would be wasteful.

add_filter('post_content_to_markdown/post_types', function ($types) {
    return ['post', 'page'];
});

add_filter('post_content_to_markdown/converter_options', function ($options) {
    $options['strip_tags'] = true;
    return $options;
});

The Markdown Feed Is the Real Distribution Layer

The second clever move is the dedicated /feed/markdown/ endpoint. That changes the plugin from a per-post formatter into a publishing surface that automated tools can discover and consume.

A postal sorting room with one incoming WordPress request split onto two conveyor belts. One belt goes to a conventional HTML output tray, while the other is stamped with an Accept header, passes through a Markdown conversion press, and lands in a tidy /feed/markdown/ bundle beside a cache locker. The scene explains that the feed is a distribution layer, not just a formatting option.
The feed turns Markdown into a first-class publishing channel, not an afterthought.

Roots also adds autodiscovery to the standard RSS flow, so consumers can find the Markdown version without guessing. That is the sort of small detail that turns a neat feature into infrastructure.

SurfaceDiscoveryWhat it returnsWhy it matters
HTML post pageNormal browser navigationRendered HTMLDefault human experience
Negotiated post pageAccept header or query parameterMarkdown from the same URLOne canonical URL, two representations
Markdown feedFeed autodiscovery plus /feed/markdown/Multiple posts in MarkdownEasy machine consumption and site-wide ingestion

Why the Plugin Feels Pro-Grade

The codebase is small, but the discipline is obvious. PHP 8.1+, Pest, php-scoper, WordPress Playground, CI, and transient caching say this is a maintained tool, not a weekend experiment.

That matters because this kind of plugin lives or dies on trust. If it breaks content rendering or pollutes a site’s dependency graph, nobody will let it near production.

SignalWhat it suggestsWhy it matters
Scoped dependenciesVendor isolationFewer plugin conflicts
Pest testsBehavior is verifiedSafer changes to request handling
WordPress PlaygroundInstant reproducibilityEasier onboarding and debugging
Transient cacheConversion cost is managedBetter performance under repeat access

What This Replaces

This plugin is most compelling as a replacement for glue code. The usual stack is some mix of REST extraction, client-side conversion, custom scripts, or manual exports. Each piece works, but the maintenance burden creeps up fast.

Roots removes a layer. It lets WordPress keep its identity as the source of truth while becoming legible to tools that do not want HTML.

Why This Matters Beyond WordPress

The broader idea is bigger than one plugin. Canonical content is more valuable when it can present itself in the format the consumer actually needs. Sometimes that is HTML. Sometimes it is Markdown. Sometimes it is a feed that a crawler or agent can ingest without ceremony.

That is why this project lands. It uses a very old web trick to solve a very current problem. Instead of creating another content silo, it teaches WordPress to speak in the language of the request.