Using Markdown in your blog with Ruby on Rails

18 December 2018 at 02:20 - 4 minute read

Ever wondered how blogs all over the internet display code blocks and handle other styling? Wonder no more. Many of these use Markdown, which allows you to dictate styles easily.

To get started, we have an existing model called Post with a text field called content.

We'll be using Redcarpet and Rouge to render this field as styled markdown. Add both to your Gemfile and run bundle install.

We'll start by creating a MarkdownAdapter module under app/lib with an associated test using RSpec. Why create an adapter? Because we might want to be able to swap out our rendering engine in the future, and creating an adapter means we 1) will only have to change the adapter to change the rendering engine and 2) can use this adapter to render any other markdown in the app (not just on Posts).

In spec/lib/markdown_adapter_spec.rb:

require "rails_helper"

include MarkdownAdapter

describe MarkdownAdapter do
  describe "#render_content" do
    it "should return html_safe rendered markdown" do
      content = "# wowzers\n\n_testing_\n\n~~does this work~~\n\n*italics*"

      result = render_content(content)

      expect(result).to eq(
        "<h1>wowzers</h1>\n\n<p><u>testing</u></p>\n\n<p>"/
        "<del>does this work</del></p>\n\n<p><em>italics</em></p>\n"
      )
    end
  end
end

Our method to change content to rendered markdown will be called render_content.

From the Rouge and Redcarpet docs, we learn that we can implement this pretty easily. We'll implement most of the extensions.

In lib/markdown_adapter.rb:

require "redcarpet"
require "rouge"
require "rouge/plugins/redcarpet"

module MarkdownAdapter
  class HTML < Redcarpet::Render::HTML
    include Rouge::Plugins::Redcarpet
  end

  def render_content(content)
    options = {
      filter_html: true,
      hard_wrap: true,
      link_attributes: {rel: "nofollow", target: "_blank"}
    }

    extensions = {:strikethrough => true,
                  :superscript => true,
                  :underline => true,
                  :highlight => true,
                  :disable_indented_code_blocks => true,
                  :space_after_headers => true,
                  :fenced_code_blocks => true,
                  :autolink => true,
                  :lax_spacing => true,
                  :no_intra_emphasis => true}

    renderer = HTML.new(options)
    markdown = Redcarpet::Markdown.new(renderer, extensions)

    markdown.render(content).html_safe
  end
end

Potential gotchas: Rails 5 does not autoload files in the lib directory. You might need to add something like config.autoload_paths << Rails.root.join("lib") to your application.rb.

We're mostly done at this point. There are a few ways to make the rendered content display (in this case, we only want markdown to render on the show page). We'll make sure the following is in our app/controllers/posts_controller.rb:

class PostsController < ApplicationController
  ...
  include MarkdownAdapter

  def show
    @post_content = render_content(@post.content)
  end
  ...
end

In the show page, we just use @post_content where we'd have previously used @post.content (and update tests to include markdown).
In app/views/posts/show.html.erb:

<div class="post__content">
  <%= @post_content %>
</div>

We'll also grab one of the styles that ships with Rouge and dump it directly in our assets directory with rougify style monokai.sublime > app/assets/stylesheets/rouge_syntax.scss.

You're done! Now you're rendering markdown.

p.s. there's a good argument that the rendering function should be defined as self.render_content, and calling it with MarkdownAdapter.render_content. I'm not really attached to either method, and went for the one that requires less typing but is perhaps less explicit on later readings of the controller.

Adding slugs to your model in Ruby on Rails →