ArticlesRuby programmingEngineering

ERBye

Replacing ERB with Eclair to write better HTML in code

I'm an accidental expert in ERB. I wrote a whole component system on top of ERB when I was building Stripe's internal email API. This was more than three years ago.

That component system is quite complicated and includes the capability to type-check ERBs using Sorbet. We do this by compiling each ERB file into Ruby code, slap some code-generated sig's on the template function, and run the type-checker. This is fairly straightforward but won't land inside Sorbet due to much higher priorities (rightfully so).

That is the worst thing about ERB: no one cares enough. It's akin to writing code in Java. No one writing in ERB is trying their best because they know the medium itself isn't one that is attune to doing their best work. In this sense, writing ERB is an uphill battle to start. No one considers it real code.

ERB does itself no favor by also being quite insecure by default. A key aspect of the component system was eliminating XSS vulnerabilities. Because stock ERB is HTML-unaware, it has no syntax for safely escaping untrusted user input.

For example, if user_name were set to "<script>evil()</script>" in this template:

<p>Hello, <%= user_name %></p>

The output is:

<p>
  Hello,
  <script>
    evil()
  </script>
</p>

This gap led to a proliferation of ERB-likes that offer additional safeguards. Before we aligned on the component system there were competing ERB syntaxes and libraries in the codebase. This made detection of potential XSS vulnerabilities basically impossible. Fixing this library problem was one of my more interesting security side-quests.

The component system built atop ERB has gone on to see great adoption. With over 400 components and 2000 distinct email types it's become a staple developer experience. However, at the end of the day despite all the effort I put into it, it's still subpar.

I knew what the better solution was three years ago but was worried the approach would be too radical to get consensus. And a migration from the old wild-west ERBs to a saner ERB system was a lighter move in a better direction. I don't look back with regret, just with curiosity about what could have been.

The better authoring experience is React's JSX code-based component system. There's a lot of flashy, complex stuff in React but JSX itself is great for sure. ERB templates are a second-class citizen, but JSX is code. Developers grok this and write much better code accordingly. It's generating HTML just like ERB but somehow it feels more enduring and reasonable to take the time to author good code.

This past week I wrote a gem to capture this better authoring experience for Ruby. The gem is named Eclair. I chose this name because eclair is a dessert like sorbet, eclair starts with E like ERB, and "eclair" sounds like "declare" as in the declarative nature of JSX.

The above example but using Eclair:

html = Eclair.render(
   Eclair::Html.p({}, ["Hello, ", user_name])
)

The XSS issues are accounted for. If you wanted to allow raw HTML into Eclair you would have to write it with much more intent:

html = Eclair.render(
   Eclair::Html.p(
      {},
      [
         "Hello, ",
         Eclair::Element::DangerousUnescapedHtml.new(
            html: user_name
         )
      ]
   )
)

That one will be caught in code review or by linters with ease.

I'm also happy with how boilerplate-less Eclair is thanks to Ruby's built-in yield_self method which removes the namespace verbosity:

def make_footer
   Eclair::Html.yield_self do |h|
      h.footer({}, [
         h.ul({}, [
            h.li({}, [h.a({href: '/'}, ['Home'])]),
            h.li({}, [h.a({href: '/about'}, ['About'])]),
         ])
      ])
   end
end

html = Eclair.render(make_footer)
#=> '<footer><ul><li><a href="/">Home</a>…'

It's a small library with Sorbet types, but addresses all the developer experience shortcomings of ERB. So much so that it's time for me to say goodbye to ERB, at least where I can.

Try Eclair for yourself and thanks for reading!