Rails 8.1 Adds Native Markdown Rendering Support

Introduction

Markdown has become the lingua franca of AI. Large language models output markdown by default, documentation lives in markdown files, and developers think in markdown.

Why AI “Speaks” Markdown

Markdown’s rise in the age of AI comes down to a few key factors:

  • Simplicity and Structure: Its minimalist, plain text syntax (# for headings, * for lists) creates a clear, predictable structure that AI models are trained on and can easily interpret.

  • Enhanced Comprehension: AI struggles with complex formats like PDFs or Word documents due to visual clutter and metadata. Markdown strips this away, letting AI focus on meaningful content.

  • Improved Prompting and Output: Markdown in prompts provides a clear roadmap for AI, reducing ambiguity. AI systems generate responses in Markdown to ensure clear presentation with lists, code blocks, and tables.

  • Efficiency: Markdown’s conciseness saves tokens, allowing more information within an AI’s context window and reducing processing overhead.

  • Interoperability: As plain text, Markdown integrates easily across platforms—from GitHub to Notion to Obsidian—making it a universal format for digital content.

Rails 8.1 embraces this reality by adding native markdown rendering support. Controllers can now respond to markdown requests directly, just like HTML, JSON, or XML.

Before

Before Rails 8.1, serving markdown content required workarounds.

class PagesController < ApplicationController
  def show
    @page = Page.find(params[:id])

    respond_to do |format|
      format.html
      format.text { render plain: @page.body }
    end
  end
end

If we wanted to serve markdown specifically, we had to register a custom MIME type and handle the rendering manually:

# config/initializers/mime_types.rb

Mime::Type.register "text/markdown", :md

# In the controller

format.md { render plain: @page.body, content_type: "text/markdown" }

This worked but felt clunky. There was no convention for markdown responses, and objects couldn’t define their own markdown representation.

After

Rails 8.1 adds first class markdown support. The format.md option is now available in respond_to blocks, and objects can implement to_markdown for automatic rendering.

class Page < ApplicationRecord
  def to_markdown
    body
  end
end

class PagesController < ApplicationController
  def show
    @page = Page.find(params[:id])

    respond_to do |format|
      format.html
      format.md { render markdown: @page }
    end
  end
end

When a client requests Accept: text/markdown or appends .md to the URL, Rails calls to_markdown on the object and returns the content with the correct MIME type.

The to_markdown Convention

Similar to to_json and to_xml, Rails 8.1 introduces to_markdown as a convention for objects to define their markdown representation.

class Article < ApplicationRecord
  def to_markdown
    <<~MARKDOWN

      # #{title}

      *Published on #{published_at.strftime("%B %d, %Y")}*

      #{body}

      ---
      Tags: #{tags.pluck(:name).join(", ")}
    MARKDOWN

  end
end

This keeps the markdown generation logic encapsulated within the model, following Rails conventions.

Rendering Options

The render markdown: method accepts several options:

class DocumentsController < ApplicationController
  def show
    @document = Document.find(params[:id])

    respond_to do |format|
      format.md { render markdown: @document }
    end
  end

  def raw
    @document = Document.find(params[:id])

    respond_to do |format|
      format.md { render markdown: @document.raw_content }
    end
  end
end

We can pass an object that responds to to_markdown or a string directly.

Use Cases

AI Generated Content

With AI assistants becoming common in applications, markdown rendering is essential. AI responses are typically formatted in markdown:

class ChatController < ApplicationController
  def response
    @message = AIService.generate_response(params[:prompt])

    respond_to do |format|
      format.html { render :response }
      format.md { render markdown: @message.content }
      format.json { render json: @message }
    end
  end
end

Clients can request markdown directly and render it on their end, or request HTML for server side rendering.

Documentation APIs

For applications that serve documentation, markdown is the natural format:

class DocsController < ApplicationController
  def show
    @doc = Documentation.find_by!(slug: params[:slug])

    respond_to do |format|
      format.html { render :show }
      format.md { render markdown: @doc }
    end
  end
end

Documentation tools and static site generators can fetch content in markdown format and process it as needed.

Export Functionality

Users often want to export content in markdown for use in other tools:

class NotesController < ApplicationController
  def export
    @note = current_user.notes.find(params[:id])

    respond_to do |format|
      format.md do
        send_data @note.to_markdown,
                  filename: "#{@note.title.parameterize}.md",
                  type: "text/markdown"
      end
    end
  end
end

Content Negotiation

Rails handles content negotiation automatically. Clients can request markdown in several ways:

Using Accept Header

curl -H "Accept: text/markdown" https://example.com/pages/1

Using Format Extension

curl https://example.com/pages/1.md

Using Format Parameter

curl https://example.com/pages/1?format=md

All three approaches work out of the box with the standard respond_to block.

Combining with Views

We can also use markdown templates directly. Create a view with the .md.erb extension:

<%# app/views/articles/show.md.erb %>
# <%= @article.title %>

*By <%= @article.author.name %>*

<%= @article.body %>

## Comments

<% @article.comments.each do |comment| %>
- **<%= comment.author %>**: <%= comment.body %>
<% end %>

Rails will render the ERB first, then serve the result as markdown.

Testing

Testing markdown responses follows the same patterns as other formats:

class PagesControllerTest < ActionDispatch::IntegrationTest
  test "returns markdown format" do
    page = pages(:welcome)

    get page_path(page), headers: { "Accept" => "text/markdown" }

    assert_response :success
    assert_equal "text/markdown", response.media_type
    assert_includes response.body, "# #{page.title}"
  end

  test "returns markdown via extension" do
    page = pages(:welcome)

    get page_path(page, format: :md)

    assert_response :success
    assert_equal "text/markdown", response.media_type
  end
end

Conclusion

Native markdown rendering in Rails 8.1 reflects how developers work today. With AI tools generating markdown content and documentation living in markdown files, first class support makes sense.

The implementation follows Rails conventions: to_markdown mirrors to_json, format.md works like any other format, and content negotiation handles the rest.

For applications serving AI generated content, documentation, or exportable notes, this feature removes boilerplate and provides a clean, conventional approach.

References

Need expert help with Rails?

Saeloun is a Rails Foundation Contributing Member helping teams modernize, upgrade, scale, and maintain production Rails applications.

Our Expertise

  • Rails contributors
  • 500+ Technical Articles
  • Production Rails consulting
  • Performance Optimization

Services

  • Rails application development
  • Code Audits
  • Rails upgrades
  • Team Augmentation

Need help on your Ruby on Rails or React project?

Join Our Newsletter