# Discourse

The [Roots](https://roots.io/) discourse-to-markdown plugin serves topics, categories, tags, and topic lists as Markdown when requested via the `Accept` header, with proper `Vary`, `406`, and q-value handling.

The cleanest way to content-negotiate a Discourse forum is the
[Roots](https://roots.io/) **[discourse-to-markdown](https://github.com/roots/discourse-to-markdown)**
plugin. It handles the conversion and the Accept-header routing
without touching your theme or any Rails code.

## What the plugin does

- Parses the incoming `Accept` header with a spec-correct
  [RFC 9110 §12.5.1](/reference) parser, so specificity wins over the
  wildcard tie that would otherwise make Rails favour HTML.
- If Markdown is preferred, converts Discourse's rendered `cooked` HTML
  to Markdown on the fly and serves it with
  `Content-Type: text/markdown; charset=utf-8` and `Vary: Accept`.
- Returns `406 Not Acceptable` when the client explicitly rejects every
  representation the plugin can serve (toggle via
  `discourse_to_markdown_strict_accept`).
- Covers topics, single posts, categories, tags, and the `/latest`,
  `/top`, `/hot`, and `/u/:username/activity` topic lists — plus their
  RSS feeds, which carry an `<atom:link>` pointing at the Markdown
  equivalent.
- Accepts a `.md` URL suffix on any supported route as a first-class,
  shareable alternative to the `Accept` header. `.md` responses carry
  `X-Robots-Tag: noindex, nofollow` so search engines don't index the
  Markdown alias alongside the canonical HTML page.
- Advertises the Markdown sibling on every HTML response — both as an
  HTTP `Link` header and as an HTML `<link rel="alternate">` in the
  `<head>` — so [RFC 8288](/reference)-aware crawlers and HTML-parsing
  clients can discover it without sending `Accept: text/markdown`. See
  [advertising `.md` siblings](/guides/accept-text-markdown#advertising-md-siblings-via-link--relalternate).
- Otherwise, Discourse renders the forum as normal.

## Install

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
```

Then rebuild:

```bash
cd /var/discourse
./launcher rebuild app
```

## Configure

All settings live under **Admin → Settings → Plugins**. The master
switch ships off — flip `discourse_to_markdown_enabled` on to activate
negotiation.

| 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` when the client's `Accept` 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. Disable if your reverse proxy manages `Vary` itself |
| `discourse_to_markdown_include_post_metadata` | `true` | Include URL, category, tags, author, and timestamps in the Markdown |

Converted Markdown is cached per post in `Discourse.cache` (Redis in
production) keyed on `post.id` + `post.updated_at`, so edits produce a
fresh key automatically — no explicit invalidation hook needed.

## Verify

```bash
curl -sI -H "Accept: text/markdown" https://yourforum.com/t/welcome/5
# Content-Type: text/markdown; charset=utf-8
# Vary: Accept

curl -sI https://yourforum.com/latest.md
# Content-Type: text/markdown; charset=utf-8
# Vary: Accept
# X-Robots-Tag: noindex, nofollow
```

---

_Markdown content negotiation: serve clean Markdown to AI agents from the same URL that serves HTML to browsers._ — [acceptmarkdown.com](https://acceptmarkdown.com/)
