> ## Documentation Index
> Fetch the complete documentation index at: https://docs.appsignal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Markdown MIME type and renderer in Rails 8.1

> Rails 8.1 adds a Markdown MIME type and renderer, so a controller can respond with text/markdown. It does not convert Markdown to HTML, and this explains what it does instead.

export const YouTube = ({id, title, description, presenter, duration, republished}) => {
  const playlistId = "PLFHQSOKTqHXA";
  const playlistUrl = `https://www.youtube.com/playlist?list=${playlistId}`;
  if (!id || id === "YOUTUBE_ID") {
    return <div style={{
      border: "1px dashed currentColor",
      borderRadius: "0.5rem",
      opacity: 0.7,
      padding: "2rem 1.5rem",
      textAlign: "center",
      fontSize: "0.875rem"
    }}>
        <strong>Video not published yet.</strong>
        <br />
        {title ? `"${title}" has no YouTube id.` : "This tutorial has no YouTube id."}{" "}
        Replace <code>YOUTUBE_ID</code> in this page with the id from the
        video's URL, once it is in the{" "}
        <a href={playlistUrl}>GoRails x AppSignal playlist</a>.
      </div>;
  }
  const prettyDate = republished ? new Date(`${republished}T00:00:00Z`).toLocaleDateString("en-US", {
    year: "numeric",
    month: "long",
    day: "numeric",
    timeZone: "UTC"
  }) : null;
  const schema = {
    "@context": "https://schema.org",
    "@type": "VideoObject",
    name: title,
    embedUrl: `https://www.youtube-nocookie.com/embed/${id}`,
    contentUrl: `https://www.youtube.com/watch?v=${id}`,
    thumbnailUrl: [`https://i.ytimg.com/vi/${id}/maxresdefault.jpg`],
    creator: {
      "@type": "Organization",
      name: "GoRails",
      url: "https://gorails.com"
    },
    publisher: {
      "@type": "Organization",
      name: "AppSignal",
      url: "https://appsignal.com"
    }
  };
  if (description) schema.description = description;
  if (duration) schema.duration = duration;
  if (republished) schema.uploadDate = republished;
  if (presenter) schema.author = {
    "@type": "Person",
    name: presenter
  };
  return <div style={{
    marginBottom: "1.5rem"
  }}>
      <iframe width="100%" height="450" src={`https://www.youtube-nocookie.com/embed/${id}?list=${playlistId}`} title={title} style={{
    borderRadius: "0.5rem",
    border: 0,
    display: "block"
  }} allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen />
      <p style={{
    fontSize: "0.875rem",
    marginTop: "0.5rem"
  }}>
        <a href={`https://www.youtube.com/watch?v=${id}&list=${playlistId}`}>{`Watch "${title}" on YouTube`}</a>
      </p>
      <script type="application/ld+json" dangerouslySetInnerHTML={{
    __html: JSON.stringify(schema)
  }} />
    </div>;
};

<YouTube id="L1ETcZickjA" title="Respond with Markdown in Rails 8.1" description="Rails 8.1 adds a Markdown MIME type and renderer, so a controller can respond with text/markdown. It does not convert Markdown to HTML, and this explains what it does instead." presenter="Chris Oliver" duration="PT5M5S" republished="2026-09-19" />

Presented by Chris Oliver for [GoRails](https://gorails.com). Republished on AppSignal September 19, 2026.

Rails 8.1 adds a Markdown MIME type and a Markdown renderer. This was widely misread when it landed, so it is worth stating plainly what it is not: it does not convert Markdown into HTML. No Markdown parser was added to Rails.

What it does is let a controller respond with `text/markdown`, the same way it can already respond with JSON.

<h2 id="what-this-covers">
  What this covers
</h2>

* Responding to a `.md` request with `text/markdown`.
* The `to_markdown` method the renderer expects on your object.
* Rendering Markdown as HTML, which is a separate job for a separate gem.

<h2 id="requirements">
  Requirements
</h2>

| What  | Value                                        |
| ----- | -------------------------------------------- |
| Rails | 8.1 or later, for the MIME type and renderer |
| Gem   | `commonmarker`, only if you also want HTML   |

<h2 id="respond-with-markdown">
  Respond with Markdown
</h2>

Add a `md` format to the action's `respond_to` block:

```ruby theme={null}
def show
  respond_to do |format|
    format.html
    format.json { render json: @post }
    format.md { render markdown: @post }
  end
end
```

`format.md` matches a request for the `.md` extension. `render markdown:` is the new renderer.

<h2 id="define-to_markdown">
  Define to\_markdown
</h2>

The renderer calls `to_markdown` on whatever you hand it. Without that method the request fails with `undefined method 'to_markdown'`.

Where the Markdown is already in an attribute, delegate to it:

```ruby theme={null}
class Post < ApplicationRecord
  delegate :to_markdown, to: :body
end
```

Requesting `/posts/3.md` now returns the raw Markdown, with a `Content-Type` of `text/markdown; charset=utf-8`.

That header is the point of the feature. Any consumer that content-negotiates now gets Markdown from your application as a first-class representation, rather than HTML it has to strip.

<h2 id="rendering-markdown-as-html">
  Rendering Markdown as HTML
</h2>

For displaying Markdown to a person in a browser, you still need a Markdown parser. `commonmarker` is a reasonable default, and supports GitHub-flavored Markdown.

```ruby theme={null}
# Gemfile
gem "commonmarker"
```

```erb theme={null}
<%= Commonmarker.to_html(@post.body).html_safe %>
```

<Warning>
  `html_safe` disables escaping for that string. Only call it on Markdown you trust, or configure the parser to sanitize untrusted input. User-submitted Markdown rendered with `html_safe` and no sanitization is a cross-site scripting vulnerability.
</Warning>

<h2 id="why-this-shipped">
  Why this shipped
</h2>

The motivation is language models. Markdown is what documents get converted to before being passed to one as context. An application that serves `text/markdown` directly saves every consumer from parsing HTML back into something usable.

The same reasoning is why AppSignal's documentation is available as Markdown. Every page here is served as plain Markdown at its URL with a `.md` extension, and indexed in [`llms.txt`](https://docs.appsignal.com/llms.txt). An agent reading these docs gets the text rather than the page furniture. Adding `format.md` to your own application makes it legible to agents in the same way.

To give an agent access to your own monitoring data, see [AppSignal for agents](/agents).

<h2 id="monitoring-markdown-responses">
  Monitoring Markdown responses
</h2>

Two things are worth watching once an endpoint serves Markdown.

* **Rendering cost.** Converting Markdown to HTML on every request is real work, and it happens inside the view. AppSignal's [performance traces](/performance-tracing) break a request into events, so a slow render appears as its own segment rather than as unexplained time. If parsing turns out to be significant, cache the HTML rather than the Markdown.
* **Traffic from agents.** Markdown endpoints are usually requested by tools rather than by browsers, and their traffic pattern is different: bursty, and often highly repetitive. Giving the format its own [action name](/guides/actions) or [tag](/guides/tagging) keeps that traffic separable from your HTML requests, so neither distorts the other's response times.

<h2 id="transcript">
  Transcript
</h2>

Transcribed from the video and lightly edited: the automatic captions misheard a number of product and API names, and those have been corrected.

<Accordion title="Read the transcript">
  **0:02** Hey guys, this episode, we're talking about Rails 8.1's new Markdown MIME type and renderer, and how it works and how to use it in your Rails apps. So I've got a really simple Rails app here with a scaffold for posts. They have a title, they have a body. We're gonna say something like Hello World, and we're gonna use Markdown here and say like GoRails, https, gorails.com, is awesome. And we'll write a little Markdown in there, and we have this rendered out.

  **0:35** But it's just a string, Markdown is just text, and we need to be able to render this out in the UI with proper HTML. Now this new feature of Rails 8.1 is not going to be for rendering into HTML. It's actually for rendering Markdown as a MIME type, and response type in your views. So in order to render this out properly, we're gonna use a gem called Commonmarker, which can take basically your Markdown text, convert it to HTML, and render it out. So I've already got this gem installed, but you can add it to your gem file, and run bundle to install it.

  **1:14** And then we can hop into our view, and go to where the post body is rendered, and we'll say Commonmarker to HTML, give it that content, and we can then mark it as HTML safe afterwards. Now there's a whole bunch of options you can also add in here as well, which you can check out the Commonmarker docs, but we're gonna use the defaults here, and we'll refresh our page, and we now get GoRails is awesome, GoRails is the link with the Markdown, and awesome is italicized, because the Markdown has converted this over to an anchor tag and an EM tag. So that's awesome. That works as we would expect it to. Now the Rails 8.1 feature of Markdown as a MIME type, and a renderer, it actually refers to the renderer in your controllers.

  **2:02** So when you have a view like this where we can respond to HTML, or the JSON version of this, we can now say format.md. That's going to look for the .md file extension in the URL, and then we can say render Markdown, which is the Markdown renderer. So this .md is the MIME type in the shortcut for that. Then we have a render Markdown, and we can give it the post. And this, if we try to do this in the browser and visit 3.md, it's gonna give us an internal server error, and it's gonna be undefined method call for an instance of post.

  **2:41** What's going on here is basically that the Markdown renderer will attempt to call two Markdown on our model, and we can then delegate this to the body attribute where our Markdown text is. And if we refresh in our browser, we get the raw Markdown content as we would expect here. And if we open the network browser and we refresh, we can see this request, and the content type is going to be text/markdown character set of UTF-8. So that's the MIME type being rendered out for the content type of our response. And this is something useful in the day of AI, as DHH mentions, Markdown is what we use to give to our LLMs in order to read this and use this as context in your request.

  **3:32** So this is super useful for that, but it doesn't actually do rendering of Markdown to HTML. And that's where something like Commonmarker is gonna come into play. This is what I've switched to using for pretty much everything these days. It has a bunch of options, lots of the same GitHub flavored Markdown features as well, and it works really great. And that's what we can use to actually render it out as HTML in the browser.

  **3:59** But now we have the format.md, MIME type registered, as well as the Markdown renderer. And all we have to do is define that to Markdown method. And we are good to go. So we can have any of our views now respond with Markdown as a content type. So that's pretty awesome.

  **4:20** That's really all there is to it. This is not a big feature. I know a lot of people were super excited about it. I think a lot of people assumed it was something like this. Commonmarker being added to Rails, but it's not, it's actually just the content type, MIME type, and the renderer, and that's the confusing part, I think.

  **4:37** The renderer just refers to having the render helper understand that yes, this MIME type should go and return this content. So that's all there is to it really. There's not a whole bunch to talk about here, but that is all you need to do in your models in order to make them render out as Markdown. So that's pretty awesome.
</Accordion>

<h2 id="related-tutorials">
  Related tutorials
</h2>

* [Local CI](/tutorials/ruby/local-ci)
* [params.expect](/tutorials/ruby/params-expect)

<h2 id="about-this-tutorial">
  About this tutorial
</h2>

This tutorial summarizes a GoRails screencast on the Rails 8.1 Markdown MIME type and renderer, by Chris Oliver. The screencast is the original work. GoRails publishes it, and the rest of the series, at [gorails.com](https://gorails.com).
