Discourse to Markdown: Serve Your Forum to AI Agents
I built roots/discourse-to-markdown, a Discourse plugin that does content negotiation on every forum route. Send Accept: text/markdown or append .md to any URL and you get Markdown back instead of HTML.
What it does
Every route a reader hits has a Markdown equivalent:
| 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 |
Two ways to ask for it:
# Accept header — for programmatic clients
curl -H "Accept: text/markdown" https://discourse.roots.io/t/welcome/5
# .md URL suffix — shareable, paste-into-chat friendly
curl https://discourse.roots.io/t/welcome/5.md
Content negotiation gotchas
Rails’ Accept handling doesn’t rank text/markdown above */*, so Accept: text/markdown, */* gets you HTML. The plugin ships its own parser that ranks by specificity, the way RFC 9110 says it should.
It also sets Vary: Accept on every response. Without it, a CDN can cache the HTML for a given URL and serve it to an agent asking for Markdown, or vice versa.
Cooked, not raw
Discourse stores two versions of every post: raw (the authoring syntax, with [quote=…] blocks and :smile: shortcodes) and cooked (the rendered HTML, with oneboxes expanded, mentions linked, and quotes attributed). The plugin converts cooked to Markdown so the output matches what readers actually see.
Discourse-specific constructs are rewritten before conversion:
<aside class="quote">→ blockquote with> [@user](post-url):attribution<aside class="onebox">→ blockquote with title, URL, and excerpt<details><summary>→ blockquote with bolded summary + body<a class="mention">@user</a>→@userliteral<div class="lightbox-wrapper">→ image with the full-size URL
Install
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
Then rebuild:
cd /var/discourse
./launcher rebuild app
Flip discourse_to_markdown_enabled on in Admin → Settings → Plugins and every route above starts negotiating on Accept: text/markdown (and exposes a .md suffix as a fallback).
More context at acceptmarkdown.com, including a pass/fail readiness check for your own site. If you’re on WordPress instead of Discourse, post-content-to-markdown does the same job there.