Discourse to Markdown Plugin

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:

  1. Accept: text/markdown header (ideal for LLMs)
  2. .md URL suffix
  3. 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