discourse-to-markdown is a new plugin that returns forum content as Markdown when the client sends Accept: text/markdown or appends .md to any content URL.
We’re running it on our own forum at https://discourse.roots.io:
curl -H "Accept: text/markdown" https://discourse.roots.io/latest
curl https://discourse.roots.io/t/serve-your-wordpress-posts-as-markdown/30321.md
HTML is expensive to feed to an LLM, and serving Markdown that just contains the content often cuts token usage by 3–5x. This means cheaper API calls, faster responses, and more headroom in the context window for the model to reason over. See https://acceptmarkdown.com for the longer pitch and a readiness check for any site.
How clients request Markdown
Three entry points:
Accept: text/markdownheader (ideal for LLMs).mdURL suffix- Discovery (every HTML response advertises its Markdown sibling via
Link: <...>; rel="alternate"; type="text/markdown"and a<link rel="alternate">tag in<head>, RSS feeds carry an<atom:link>pointing at the Markdown equivalent)
Supported routes
| Route | HTML | Markdown |
|---|---|---|
| Topic | /t/:slug/:id |
/t/:slug/:id.md |
| Single post | /t/:slug/:id/:post_number |
/t/:slug/:id/:post_number.md |
| Category | /c/:slug/:id |
/c/:slug/:id.md |
| Tag | /tag/:tag |
/tag/:tag.md |
| Latest | /latest |
/latest.md |
| Top | /top |
/top.md |
| Hot | /hot |
/hot.md |
| User activity | /u/:username/activity |
/u/:username/activity.md |
Installation
Add the plugin to your app.yml:
hooks:
after_code:
- exec:
cd: $home/plugins
cmd:
- git clone https://github.com/roots/discourse-to-markdown.git
Rebuild the container:
cd /var/discourse
./launcher rebuild app
Then enable it in Admin → Settings → Plugins → Markdown output.
Notes on the conversion
The plugin converts Discourse’s cooked HTML — the rendered representation readers see, with oneboxes expanded, mentions linked, and quotes attributed — not raw. This preserves what readers actually see and keeps the output portable across any GFM-compatible renderer. Discourse-specific constructs (quotes, oneboxes, details, mentions, hashtags, emoji, lightboxes, polls) are rewritten sensibly before conversion.
Converted Markdown is cached in Redis per post keyed on post.id + post.updated_at and edits invalidate automatically.
Settings
| Setting | Default | Purpose |
|---|---|---|
discourse_to_markdown_enabled |
false |
Master switch for the plugin |
discourse_to_markdown_md_urls_enabled |
true |
Accept .md URL suffixes as a sibling to the HTML route |
discourse_to_markdown_strict_accept |
false |
Return 406 Not Acceptable when the client’s Accept header excludes both text/html and text/markdown |
discourse_to_markdown_emit_vary |
true |
Emit Vary: Accept on Markdown and 406 responses so caches don’t cross-serve representations |
discourse_to_markdown_include_post_metadata |
true |
Include URL, category, tags, author, timestamps in the Markdown representation |
Resources
- Source/issues: GitHub - roots/discourse-to-markdown: A Discourse plugin that serves post content as Markdown via Accept headers and .md URLs · GitHub
- acceptmarkdown.com — serving Markdown to agents via content negotiation, plus a readiness check for your site
- RFC 9110 §12.5.1 — Proactive Negotiation — the spec this plugin implements
- RFC 7763 — the
text/markdownmedia type registration - MDN — Content negotiation — approachable intro to the concept
- See acceptmarkdown.com/reference for the full spec + developer docs list