# Discourse to Markdown Plugin

**URL:** https://meta.discourse.org/t/discourse-to-markdown-plugin/401199
**Category:** Plugin
**Tags:** markdown, ai
**Created:** [April 22, 2026, 2:33am UTC](https://meta.discourse.org/t/discourse-to-markdown-plugin/401199 "2026-04-22T02:33:17Z")
**Posts on this page:** 6
**Page:** 1

<div class="post-metadata">

### Author: ![benword](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/benword/32/493782_2.png) [@benword](https://meta.discourse.org/u/benword)
#### Post date: [April 22, 2026, 2:33am UTC](https://meta.discourse.org/t/discourse-to-markdown-plugin/401199/1 "2026-04-22T02:33:17Z")

</div>

**[discourse-to-markdown](https://github.com/roots/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](https://discourse.roots.io):

```bash
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](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`:

```yaml
hooks:
  after_code:
    - exec:
        cd: $home/plugins
        cmd:
          - git clone https://github.com/roots/discourse-to-markdown.git

```

Rebuild the container:

```bash
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](https://github.com/roots/discourse-to-markdown)
- **[acceptmarkdown.com](http://acceptmarkdown.com)** — serving Markdown to agents via content negotiation, plus a readiness check for your site
- [RFC 9110 §12.5.1 — Proactive Negotiation](https://www.rfc-editor.org/rfc/rfc9110#name-proactive-negotiation) — the spec this plugin implements
- [RFC 7763](https://www.rfc-editor.org/rfc/rfc7763) — the `text/markdown` media type registration
- [MDN — Content negotiation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Content_negotiation) — approachable intro to the concept
- See [acceptmarkdown.com/reference](https://acceptmarkdown.com/reference) for the full spec + developer docs list

---

<div class="post-metadata">

### Author: ![pmusaraj](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/pmusaraj/32/119489_2.png) [@pmusaraj](https://meta.discourse.org/u/pmusaraj)
#### Post date: [April 22, 2026, 2:31pm UTC](https://meta.discourse.org/t/discourse-to-markdown-plugin/401199/2 "2026-04-22T14:31:22Z")

</div>

This is great, thanks for publishing it!

> [@benword](#):
>
> The plugin converts Discourse’s `cooked` HTML — the rendered representation readers see, with oneboxes expanded, mentions linked, and quotes attributed — not `raw`.

This is a pretty cool approach. It gives a richer Markdown than the raw output, because it benefits from Discourse’s `cooking` infrastructure.

---

<div class="post-metadata">

### Author: ![jrgong](https://avatars.discourse-cdn.com/v4/letter/j/c57346/32.png) [@jrgong](https://meta.discourse.org/u/jrgong)
#### Post date: [April 27, 2026, 11:57am UTC](https://meta.discourse.org/t/discourse-to-markdown-plugin/401199/5 "2026-04-27T11:57:44Z")

</div>

OMG, that’s exactly what I have been looking for, fantastic timing! We use the API or MCP frequently and it always bugged us that the raw content doesn’t have resolved image URLs

Thank you for the work!

Edit: Could that somehow be used with [Discourse MCP](https://meta.discourse.org/t/discourse-mcp-is-here/386983) as well?

---

<div class="post-metadata">

### Author: ![benword](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/benword/32/493782_2.png) [@benword](https://meta.discourse.org/u/benword)
#### Post date: [April 28, 2026, 8:38pm UTC](https://meta.discourse.org/t/discourse-to-markdown-plugin/401199/6 "2026-04-28T20:38:28Z")

</div>

> [@jrgong](#):
>
> OMG, that’s exactly what I have been looking for, fantastic timing! We use the API or MCP frequently and it always bugged us that the raw content doesn’t have resolved image URLs
> 
> Thank you for the work!
> 
> Edit: Could that somehow be used with [Discourse MCP](https://meta.discourse.org/t/discourse-mcp-is-here/386983) as well?

Glad it’s useful!

Discourse MCP could add content negotiation as an optional path. It would not need to require this plugin, it could request `Accept: text/markdown` from the canonical topic/post URL and fall back to the current JSON API behavior if the site does not support Markdown.

This plugin is just one way for a Discourse site to satisfy that request today. Without either this plugin or equivalent support in Discourse core, the `Accept` header alone would not change the JSON API output.

So the ideal MCP integration might be: try `text/markdown` on the content URL first, then fall back to `/t/:id.json?include_raw=true`.

---

<div class="post-metadata">

### Author: ![jrgong](https://avatars.discourse-cdn.com/v4/letter/j/c57346/32.png) [@jrgong](https://meta.discourse.org/u/jrgong)
#### Post date: [April 29, 2026, 9:34am UTC](https://meta.discourse.org/t/discourse-to-markdown-plugin/401199/7 "2026-04-29T09:34:49Z")

</div>

> [@benword](#):
>
> So the ideal MCP integration might be: try `text/markdown` on the content URL first, then fall back to `/t/:id.json?include_raw=true`.

Funny that you say that, as Claude solved it exactly that way…

---

<div class="post-metadata">

### Author: ![pmusaraj](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/pmusaraj/32/119489_2.png) [@pmusaraj](https://meta.discourse.org/u/pmusaraj)
#### Post date: [September 21, 2026, 2:49pm UTC](https://meta.discourse.org/t/discourse-to-markdown-plugin/401199/8 "2026-09-21T14:49:29Z")

</div>

@benword a quick note that we’re in the process of adding this plugin’s functionality to core. Here is the first PR:

> <https://github.com/discourse/discourse/pull/43843>
>
> References: https://meta.discourse.org/t/discourse-to-markdown-plugin/401199 and… https://acceptmarkdown.com/, credit goes to Ben Word for spearheading the plugin and spec. 
> 
> This PR adds support for forums to respond with rich markdown to requests on topic lists (latest, new, categories, tags) and topic views. This is better for LLMs, more efficient than HTML but with better quality content than the existing \`.raw\` output. Images, lists, tables, formatting is respected properly compared to \`.raw\`. 
> 
> This PR doesn't enable the feature by default yet, it's shipped behind a disabled experimental site setting called \`experimental\_markdown\_endpoints\`. This setting will be soon enabled (after testing) and make public.

Initially, this feature in core will be behind a hidden site setting while we test it. But soon enough, it will be default enabled (admins can disable the feature if they wish).

I have not tested this branch with the plugin side-by-side, I suspect the plugin won’t be needed once this lands in core though.
