rubyguides

ERB Templates Outside of Rails

ERB (Embedded RuBy) ships as part of Ruby’s standard library. You do not need Rails, Bundler, or any external gem to start using it. ERB templates let you embed Ruby expressions and control flow directly inside text templates, which makes them a practical fit for code generation, email mailers, static site builders, and configuration file rendering.

Key Takeaways

  • ERB templates are built into Ruby, so you can render text without adding a framework
  • <%= %> inserts output, <% %> runs Ruby silently, and <%# %> leaves a comment in the template only
  • result_with_hash is often easier than managing a custom binding by hand
  • Trim mode helps when you generate code, HTML, or any other structured text that should not contain extra blank lines
  • Small templates are easiest to test when you pass only the values they need

If you are already comfortable with Ruby methods and blocks, ERB is a small step up in complexity for a lot of practical payoff. It gives you a way to keep repetitive text in a template while still reusing Ruby for variables, conditionals, and loops.

What ERB templates are

ERB processes templates that contain three kinds of tags:

  • <% %> — runs Ruby code silently (no output)
  • <%= %> — runs Ruby code and inserts the result into the template
  • <%# %> — a comment, completely ignored

Everything else in the template passes through as literal text. This makes ERB useful for any task where you need to mix static text with dynamic values.

That simple model is what makes ERB templates easy to reason about. If you can read the template as plain text, you can usually understand the output without mentally executing a whole framework. The Ruby code only appears where you need dynamic values or control flow.

Basic Usage

Create an ERB object from a template string and call #result to get the rendered output:

require "erb"

template = ERB.new(<<~TEMPLATE)
  Hello, <%= name %>! You have <%= count %> messages.
TEMPLATE

name  = "Alice"
count = 3
puts template.result
# => Hello, Alice! You have 3 messages.

Calling #result without arguments uses the current binding, the set of local variables visible at the call site. That is convenient for quick scripts where the template and the variables live in the same context, but it also means the template can see more than it needs to, which is worth keeping in mind as the template grows. When you want tighter control over what the template can access, passing an explicit binding or a hash of locals gives you a cleaner boundary.

Rendering with a binding

Sometimes you want to render a template in a different context. Pass a binding explicitly using binding or by building a custom binding:

def render_user_report(user)
  template = ERB.new(<<~TEMPLATE)
    User: <%= user[:name] %>
    Email: <%= user[:email] %>
    Role: <%= user[:role] %>
  TEMPLATE
  template.result(binding)
end

user = { name: "Bob", email: "bob@example.com", role: "admin" }
puts render_user_report(user)
# => User: Bob
# => Email: bob@example.com
# => Role: admin

You can also build a binding explicitly using Binding.new (available in Ruby 3.0+) or pass the local bindings hash. The first approach is convenient when the template needs access to methods or instance variables from the surrounding object. The second approach, using a hash of locals, is often cleaner because you can see exactly which values the template receives without inspecting the entire object graph.

def render_with_locals(template_str, locals)
  b = binding
  locals.each { |k, v| b.local_variable_set(k, v) }
  ERB.new(template_str).result(b)
end

puts render_with_locals("Welcome, <%= name %>!", { name: "Carol" })
# => Welcome, Carol!

The first approach is convenient when the template needs access to methods or instance variables from the surrounding object. The second is often better when you want a smaller and more explicit interface, because you can see exactly which values the template receives.

When you pass local values directly, you also make the template easier to test. The template does not need to know about the rest of the object graph, which makes it simpler to reuse in scripts, mailers, or code generators. That is often the cleaner choice when you want a template that stays small and predictable.

Trim mode

By default, ERB preserves the whitespace around tags. The <% and %> trim mode flag changes this behaviour, which matters when generating source code or structured text where you do not want blank lines from template directives.

Trim ModeBehaviour
% (no dash)Trims leading whitespace before the tag and the newline after
-%>Trims the newline after the closing tag (trailing newline removed from the output line)
<%-Trims leading whitespace before the opening tag
<% combined with -%>Trims on both sides
# Default — preserves surrounding whitespace
template = ERB.new("Line 1\n<%= 1 + 1 %>\nLine 3")
puts template.result
# => Line 1
# => 2
# => Line 3

# Trim mode: removes blank line caused by the tag
template = ERB.new("Line 1\n<% 1 + 1 %>\nLine 3", trim: "%")
puts template.result
# => Line 1
# => Line 3

# Trim both leading whitespace and trailing newline
template = ERB.new("  <%- 1 + 1 %>\nLine 3", trim: "-%")
puts template.result
# => Line 3

The trim mode is especially useful when generating Ruby code, HTML, or any structured format where extra blank lines would cause problems.

If the output is intended for humans to read, trim mode can still help by making the generated file more consistent. In generated code, even a few unnecessary blank lines can make diffs noisier and make the file harder to scan.

Security: avoiding injection

Raw output inside an ERB template can open you up to injection attacks. When the template produces HTML and user input flows into it unescaped, an attacker can inject arbitrary markup or scripts.

Ruby’s standard library provides ERB::Util.html_escape (aliased as h) to escape HTML-sensitive characters:

require "erb"
require "erb/util"

template = ERB.new(<<~TEMPLATE)
  <p>Welcome, <%= ERB::Util.html_escape(username) %></p>
TEMPLATE

username = "<script>alert('xss')</script>"
puts template.result
# => <p>Welcome, &lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;</p>

If you are building plain text (not HTML), use CGI.escape or URI.encode_www_form_component as appropriate. The rule is simple: always escape output when the template produces a format where special characters have meaning.

That guideline also applies to generated configuration files and email bodies. The output format matters more than the input source, so think about what the receiving system expects before deciding how to escape a value.

Real-world use cases

Email mailers

ERB templates work well for composing transactional emails without pulling in a framework:

require "erb"

class UserMailer
  def welcome_email(user)
    template = ERB.new(File.read("templates/welcome.txt.erb"))
    template.result_with_hash(
      name:  user[:name],
      url:   user[:verification_url]
    )
  end
end

mailer = UserMailer.new
body = mailer.welcome_email(name: "Dana", verification_url: "https://example.com/verify/abc")
puts body

#result_with_hash accepts a hash and makes those keys available as local variables inside the template, a cleaner interface than manually managing bindings.

This is a good example of where ERB templates shine outside Rails. The template stays easy to edit, while the Ruby code handles the data fetching and orchestration. If the message needs to change later, you usually only touch the template instead of the code that renders it. That makes ERB templates a nice middle ground between hard-coded strings and a heavier rendering framework.

Static site generator

ERB can power a simple static site. Store templates in a directory and render Markdown or HTML files with interpolated frontmatter:

require "erb"
require "yaml"

Dir.glob("content/**/*.md").each do |file|
  raw  = File.read(file)
  parts = raw.split(/^---$/, 3)
  meta = YAML.safe_load(parts[1])
  body = ERB.new(parts[2]).result_with_hash(meta)

output = File.join("public", file.sub("content/", ""))
File.write(output.sub(".md", ".html"), body)
end

The same approach works for documentation sites, small product landing pages, or any project where you want to keep content in files but still reuse a little Ruby logic. In practice, the template layer stays small, and most of the work happens in file discovery and metadata parsing.

Code generation

Generating Ruby source code from ERB templates is a clean way to build repetitive boilerplate. Using the -%> trim mode keeps the generated code tidy:

require "erb"

def generate_accessor(name)
  template = ERB.new(<<~RUBY, trim: "-%")
    def <%= name %>
      @<%= name %>
    end

    def <%= name %>=(value)
      @<%= name %> = value
    end
  RUBY
  template.result
end

puts generate_accessor(:title)
# => def title
# =>   @title
# => end
# =>
# => def title=(value)
# =>   @title = value
# => end

You can extend this pattern to generate model classes, database migrations, or configuration files based on schemas. In practice, ERB templates are most useful when the output is repetitive but still needs to stay readable to humans who review the generated file.

When you generate source code, the best ERB templates are usually the boring ones. Keep the template readable, keep the Ruby helpers small, and make the output predictable. That makes it much easier to regenerate files safely when the input schema changes.

Common questions about templates

When should I use ERB templates instead of string concatenation?

Use ERB templates when the output has more than a couple of moving parts. String interpolation is fine for one-line output, but templates are easier to scan once you have branches, loops, or repeated blocks of text.

Can I use ERB templates without Rails?

Yes. ERB is part of Ruby’s standard library, so plain Ruby scripts can render templates without adding Rails or another framework. That is one of the main reasons ERB is still useful for lightweight automation.

What should I test first?

Start with the template output, not the rendering helper. A good test usually renders the template with fixed inputs and checks the final text, which tells you whether the template logic and the surrounding Ruby code are working together.

See Also