LLMs consume Markdown better than HTML. More and more often I find myself wanting the same URL to serve a proper HTML page to a browser and a clean Markdown document to a crawler or an agent. Rails already has everything for that: respond_to, template variants, format-specific views. Or so I thought.

The obvious setup

class ArticlesController < ApplicationController
  def show
    @article = Article.find(params[:id])
    respond_to do |format|
      format.html
      format.md
    end
  end
end

With show.html.erb and show.md.erb sitting next to each other, a client sending Accept: text/markdown should get the Markdown view. In practice, it does not. It gets the HTML one.

Why Rails picks HTML

Rails iterates over request.formats in order and renders the first template it finds. When a client sends Accept: text/markdown, text/html;q=0.9, request.formats is built from that header: [:md, :html]. Good. But many clients: browsers, curl without an explicit Accept, agents that forward */*: end up with :html first, and Rails never looks for the Markdown template even when one exists.

I opened rails/rails#56632 to make the lookup smarter, so that a registered Markdown template wins over HTML when the client explicitly asked for Markdown, regardless of the rest of the Accept list.

The workaround until it lands

Until that change ships you can get the same behaviour with a five-line before_action in ApplicationController:

class ApplicationController < ActionController::Base
  before_action :prioritize_markdown_format

  private

  def prioritize_markdown_format
    return unless request.accepts.first&.to_s == "text/markdown"

    request.formats = %i[md html]
  end
end

If the client’s most-preferred type is text/markdown, we rewrite request.formats so Rails looks for a .md template first and falls back to .html when there isn’t one. That fallback matters: it means you can opt individual actions into Markdown by just dropping a show.md.erb next to show.html.erb, without touching the controller.

What the Markdown view looks like

Nothing fancy. It’s just ERB that happens to emit Markdown:

# <%= @article.title %>

<%= @article.body %>

---
[All articles](<%= articles_path %>)