Have you been scratching your head wondering how to run ERB blocks on static pages using Markdown in your Ruby on Rails app?
There’s always a need for a couple of static pages in a Rails app (terms and conditions, privacy policies and the likes). The High Voltage gem is here to save you from building a custom controller from scratch in each instance. As it runs right inside your app, it’s a perfect solution for documenting examples of helpers, partials or view_components. The same code rendered within the app will run in the docs, right alongside the explanations for it.
However, writing long-form content directly in HTML is pretty tedious (even for a front-end developer 😉). Markdown makes a much nicer authoring experience. So let’s see how we can get the best of each world:
- static pages without spinning a custom controller or a custom method for each page
.html.md
Markdown files for easy addition of the content- ERB blocks inside the files being executed to provide sample renders of helper methods, partials or view_components
For #1, we’ll assume a default installation of High Voltage (for example, using bundle add high_voltage
), where the pages sit in app/views/pages
. I’ll let you dig into High Voltage documentation for all the customisation options around where the static pages sit, possible authentication and all. So we can focus on the parts specific to our scenario: rendering markdown templates and executing the ERB blocks.
Adding a new template handler to Rails
To get some Markdown to render, we’ll have to dive a bit into how Rails renders templates. When rendering a view.html.erb
file, the .erb
part tells Rails which template handler to use for compiling the file and using it for rendering. Rails comes with a few handlers of the box, but nothing for .html.md
files.
Thankfully, you can add new handlers. Handlers provide Rails with a call
method. Rails will pass the source from the template file. In turn, the handler will return a string of Ruby code for Rails to evaluate. It’s how the haml
gem gets Rails to handle .html.haml
templates, for example.
So we’ll roll our own template handler, powered by Kramdown for turning the markdown into HTML. It’s much less scary than it sounds, especially when the burden of the actual rendering is delegated to another library. The handler will pretty much just be passing data around.
After installing Kramdown in our project with bundle add kramdown
, let’s create a first iteration of our Markdown template handler in the lib/markdown_handler.rb
file:
require 'kramdown' class MarkdownHandler class << self # Just like Rails' handlers, and I guess most handlers, # we'll only really care about thesource
of the file # rather than the whole Template object def call(template, source) html = render(source) # Cool trick from Rails's ActionView::Template::Handlers::Raw #inspect
automatically wraps the generated HTML # in quotes and escapes those within the string "#{html.inspect}.html_safe;" end def render(text) Kramdown::Document.new(text).to_html end end end
Before we can render our first Markdown page, there are two last things to do:
- Registering our template handler with the
.md
extension, so Rails knows what to do with our.html.md
file. For that, we’ll add a newconfig/initializers/markdown_handler.rb
file:Rails.application.reloader.to_prepare do ActionView::Template.register_template_handler :md, MarkdownHandler end
- Let Rails know that it can autoload classes from the
lib
folder. Without that, it won’t know where to find ourMarkdownHandler
class. In theconfig/application.rb
file, we’ll need to add:config.autoload_paths << Rails.root.join('lib')
After (re)starting the server, we can now test our markdown setup by creating a app/views/pages/in-markdown.html.md
file with some markdown content. It should get rendered correctly when visiting http://localhost:3000/pages/in-markdown.
That’s one step closer. It’s now time to deal with processing the ERB tags that would lie inside our Markdown.
Processing the template with ERB
Order is crucial here. A lot of examples online process the ERB before the Markdown. With the aim of documenting views, we’ll likely have ERB code samples in our Markdown. If ERB runs before the Markdown library escapes them, we’ll end up with the results and not code samples. Whoops! 😬
As Rails already has a handler for ERB, we can reuse it to compile our template after the Markdown has been processed. Giving the (pretty much) final form to our handler:
class MarkdownHandler
class << self
def call(template, source)
html = render(source)
erb.call(template, html)
end
def render(text)
Kramdown::Document.new(text).to_html
end
private
def erb
# Grab (and store) a reference of the template handler currently used for .erb
files
@erb ||= ActionView::Template.registered_template_handler(:erb)
end
end
end
And voila! Let’s add some ERB tags to the app/views/pages/in-markdown.html.md
file and… and… They end up displayed on the page, not executed 😢. Kramdown escaped <
into <
so there’ll no longer be ERB tags by the time they get to be processed. That would be why most examples process the ERB before the Markdown, I’d guess. But we do need the ERB processing to happen after, so let’s get Kramdown to leave those <%
blocks alone.
Extending Kramdown to keep ERB intact
Kramdown is pretty extensible, both for parsing the Markdown files and generating the HTML output. Here we’ll want to make its parser understand that <%
blocks are not text, but raw content we want to keep as is. The library author had thought about this further, as they provide a quick sample to get in the right direction in their API docs. It had to be tuned a little,, but it was a good place to start our lib/kramdown/parser/kramdown_with_erb.rb
.
require 'kramdown' # If you prefer Github Flavoured Markdown, # you can instead extend Kramdown::Parser::GFM # You'll need to install the kramdown-parser-gfm gem, # with bundle add kramdown-parser-gfm for example class Kramdown::Parser::KramdownWithErb < Kramdown::Parser::Kramdown def initialize(source, options) super # Parse both inline and block occurences of ERB tags @block_parsers.unshift(:erb_tags) @span_parsers.unshift(:erb_tags) # Nesting ERB blocks will lead to quite a lot of indentation # which correspond to code blocks in Markdown. To avoid that # we'll disable the parsing for that part of the syntax @block_parsers = @block_parsers.without(:codeblock) end # Kramdown's parser scans the Markdown string, looking # for specific patterns using Regular Expressions. It then turns # each relevant piece of the string detected by the scanner # into a tree of ruby objects representing the structure of the file # This pattern will let it pick up ERB blocks # Allow any level of intenting before an ERB block ERB_TAGS_START = /s*<%.*?%>/ def parse_erb_tags # Once a block is found, we can move the scanner past it @src.pos += @src.matched_size # And add a :raw node to the tree, # which will be left as is when converted to HTML @tree.children << Element.new(:raw, @src.matched) end # Rails code reloading seems to make the parser # get re-registered while already defined # so putting a little check ahead of the registration unless has_parser?(:erb_tags) define_parser(:erb_tags, ERB_TAGS_START, '<%') end end
All that’s left is telling our handler to use that specific parser, updating its render
method to:
# If you're extending the GFM parser, # you'll also want to pass the hard_wrap: false option # This will prevent Kramdown to insert <br> # for each line break inside ERB blocks, # which would result in outputing content where there should not be def render(text) Kramdown::Document.new(text, input: 'KramdownWithErb').to_html end
With that last bit in, Kramdown should leave our ERB tags alone, allowing them to be processed by the ERB handler. Most of them, at least. One final caveat is that ERB blocks that would sit inside HTML tags will still remain escaped, for example:
<div> <%= "Hello from ERB" %> </div>
Sorting that out would require some heavier changes to the parser. It’d need to scan any text content for one or several ERB tags. This may come in a future article. In the meantime this can be worked around by using Rails’ tag.XYZ helpers.
<%= tag.div do %> <%= "Hello from ERB" %> <% end %>
Together the custom template handler and Markdown parser provide a good base to write static pages in Markdown with hints of ERB. Beyond the technical setup for solving this problem, I hope this article will encourage you to dig inside the internals of the libraries you use. Whether it’s Rails, Kramdown or anything else really, it pays off to look into how things are made inside. There’s plenty to discover to help you use them to their full extent or just pick up a nifty trick or two (like the one for getting a quoted and escaped string using #inspect
).