Ruby Logo

ERB Templating

Master Embedded Ruby templating for dynamic content generation. Learn ERB syntax, security, layouts, partials, and template optimization.

Home Ruby ERB Templating

ERB Templating in Ruby

Introduction to ERB

ERB (Embedded Ruby) is a templating system that allows you to embed Ruby code within text files. It's the default templating engine for Ruby on Rails and is widely used for generating HTML, configuration files, and other text-based output.

Basic ERB Syntax

<!-- ERB Tags -->
<%= "Expression tag - outputs the result" %>
<% "Scriptlet tag - executes code without output" %>
<%# "Comment tag - ignored by ERB processor" %>
<%- "Trim tag - removes trailing newline" -%>

<!-- Example -->
<h1><%= "Hello, World!" %></h1>
<% name = "Ruby Developer" %>
<p>Welcome, <%= name %>!</p>

Using ERB in Ruby

require 'erb'

# Simple ERB template as a string
template = "<h1><%= title %></h1><p><%= message %></p>"

# Create ERB object
erb = ERB.new(template)

# Define variables for template
title = "Welcome to ERB"
message = "This is a simple ERB example"

# Render the template
result = erb.result(binding)
puts result
# Output: <h1>Welcome to ERB</h1><p>This is a simple ERB example</p>

Working with ERB Templates

ERB templates are typically stored in separate files and rendered with data from your Ruby application.

Loading Templates from Files

# template.html.erb
<!DOCTYPE html>
<html>
<head>
  <title><%= page_title %></title>
</head>
<body>
  <h1><%= heading %></h1>
  <% if show_navigation %>
    <nav>
      <ul>
        <% navigation_items.each do |item| %>
          <li><a href="<%= item[:url] %>"><%= item[:name] %></a></li>
        <% end %>
      </ul>
    </nav>
  <% end %>

  <main>
    <%= content %>
  </main>

  <footer>
    <p>Generated at <%= Time.now.strftime('%Y-%m-%d %H:%M:%S') %></p>
  </footer>
</body>
</html>

Ruby Code to Render Template

require 'erb'

class TemplateRenderer
  def initialize(template_path)
    @template = File.read(template_path)
    @erb = ERB.new(@template)
  end

  def render(variables = {})
    # Create a new binding for the template
    template_binding = create_binding(variables)
    @erb.result(template_binding)
  end

  private

  def create_binding(variables)
    # Create a new binding context
    template_context = Object.new

    # Define variables in the context
    variables.each do |key, value|
      template_context.instance_variable_set("@#{key}", value)
      template_context.define_singleton_method(key) { instance_variable_get("@#{key}") }
    end

    template_context.instance_eval { binding }
  end
end

# Usage
renderer = TemplateRenderer.new('template.html.erb')

data = {
  page_title: 'My ERB Website',
  heading: 'Welcome to ERB Templating',
  show_navigation: true,
  navigation_items: [
    { name: 'Home', url: '/' },
    { name: 'About', url: '/about' },
    { name: 'Contact', url: '/contact' }
  ],
  content: '<p>This is the main content area.</p>'
}

html_output = renderer.render(data)
puts html_output

Advanced ERB Features

ERB provides several advanced features for more complex templating scenarios.

ERB with Custom Delimiters

# Custom ERB delimiters
template_with_custom_delimiters = "<h1>{{ title }}</h1><p>{%= message %}</p>"

# Create ERB with custom delimiters
erb = ERB.new(template_with_custom_delimiters)
erb.pattern = /\{\{(.*?)\}\}|\{\%(.*?)\%\}/

# Alternative: Use ERB with different syntax
class CustomERB < ERB
  def initialize(str, safe_level = nil, trim_mode = nil, eoutvar = '_erbout')
    # Replace custom delimiters with standard ERB syntax
    str = str.gsub(/\{\{(.*?)\}\}/, '<%=\1%>')
    str = str.gsub(/\{\%(.*?)\%\}/, '<%\1%>')
    super(str, safe_level, trim_mode, eoutvar)
  end
end

title = "Custom Delimiters"
message = "This uses custom ERB delimiters"

custom_erb = CustomERB.new(template_with_custom_delimiters)
result = custom_erb.result(binding)
puts result

ERB Trim Modes

# Different trim modes
template = <<~ERB
  <% items = ['apple', 'banana', 'cherry'] %>
  <ul>
  <% items.each do |item| -%>
    <li><%= item %></li>
  <% end -%>
  </ul>
ERB

# Trim mode options:
# nil    - No trimming
# '-'    - Trim lines ending with -%>
# '<>'   - Trim lines starting with <%- and ending with -%>
# '%'    - Trim lines starting with %
# '%-'   - Combination of '-' and '%'

erb_no_trim = ERB.new(template, trim_mode: nil)
erb_with_trim = ERB.new(template, trim_mode: '-')

puts "Without trimming:"
puts erb_no_trim.result(binding)

puts "\nWith trimming:"
puts erb_with_trim.result(binding)

Safe ERB with Security Considerations

require 'erb'

class SafeERB
  def initialize(template)
    @template = template
    # Use safe level for untrusted templates
    @erb = ERB.new(@template, nil, '-', '@output')
  end

  def render(context = {})
    # Create a restricted binding
    safe_binding = create_safe_binding(context)
    @erb.result(safe_binding)
  end

  private

  def create_safe_binding(context)
    # Create a clean environment
    safe_env = Object.new

    # Only allow specific methods and variables
    allowed_methods = %w[to_s to_i length size empty? nil?]

    context.each do |key, value|
      # Validate and sanitize values
      sanitized_value = sanitize_value(value)
      safe_env.instance_variable_set("@#{key}", sanitized_value)
      safe_env.define_singleton_method(key) { instance_variable_get("@#{key}") }
    end

    # Add helper methods
    safe_env.define_singleton_method(:h) { |text| ERB::Util.html_escape(text) }
    safe_env.define_singleton_method(:u) { |text| ERB::Util.url_encode(text) }

    safe_env.instance_eval { binding }
  end

  def sanitize_value(value)
    case value
    when String
      # HTML escape by default for strings
      ERB::Util.html_escape(value)
    when Numeric, TrueClass, FalseClass, NilClass
      value
    when Array
      value.map { |item| sanitize_value(item) }
    when Hash
      value.transform_values { |v| sanitize_value(v) }
    else
      # Convert unknown types to safe strings
      ERB::Util.html_escape(value.to_s)
    end
  end
end

# Usage with potentially unsafe data
template = "<h1><%= title %></h1><p><%= message %></p><p>Raw: <%= raw_message %></p>"

unsafe_data = {
  title: "Safe Title",
  message: "<script>alert('XSS')</script>",
  raw_message: "This is safe content"
}

safe_erb = SafeERB.new(template)
result = safe_erb.render(unsafe_data)
puts result
# Script tags will be escaped automatically

Building a Template Engine

Let's create a more sophisticated template engine that includes layouts, partials, and helper methods.

Advanced Template Engine

require 'erb'
require 'pathname'

class AdvancedTemplateEngine
  attr_reader :template_dir, :layout_dir, :partial_dir

  def initialize(template_dir = 'templates')
    @template_dir = Pathname.new(template_dir)
    @layout_dir = @template_dir.join('layouts')
    @partial_dir = @template_dir.join('partials')
    @helpers = {}
    @cache = {}

    setup_default_helpers
  end

  def register_helper(name, &block)
    @helpers[name] = block
  end

  def render(template_name, variables = {}, layout: nil)
    template_path = find_template(template_name)

    # Create template context
    context = TemplateContext.new(self, variables)

    # Render the template
    content = render_template(template_path, context)

    # Wrap in layout if specified
    if layout
      layout_path = find_layout(layout)
      context.instance_variable_set(:@content, content)
      context.define_singleton_method(:content) { @content }
      content = render_template(layout_path, context)
    end

    content
  end

  def render_partial(partial_name, variables = {})
    partial_path = find_partial(partial_name)
    context = TemplateContext.new(self, variables)
    render_template(partial_path, context)
  end

  private

  def find_template(name)
    path = @template_dir.join("#{name}.erb")
    raise "Template not found: #{path}" unless path.exist?
    path
  end

  def find_layout(name)
    path = @layout_dir.join("#{name}.erb")
    raise "Layout not found: #{path}" unless path.exist?
    path
  end

  def find_partial(name)
    # Partials start with underscore by convention
    filename = name.start_with?('_') ? "#{name}.erb" : "_#{name}.erb"
    path = @partial_dir.join(filename)
    raise "Partial not found: #{path}" unless path.exist?
    path
  end

  def render_template(template_path, context)
    # Use caching for performance
    erb = @cache[template_path] ||= ERB.new(File.read(template_path), trim_mode: '-')
    erb.result(context.get_binding)
  end

  def setup_default_helpers
    # HTML escaping helper
    register_helper(:h) { |text| ERB::Util.html_escape(text.to_s) }

    # URL encoding helper
    register_helper(:u) { |text| ERB::Util.url_encode(text.to_s) }

    # Truncate helper
    register_helper(:truncate) do |text, length = 50|
      text.to_s.length > length ? "#{text[0...length]}..." : text.to_s
    end

    # Pluralize helper
    register_helper(:pluralize) do |count, singular, plural = nil|
      plural ||= "#{singular}s"
      count == 1 ? "1 #{singular}" : "#{count} #{plural}"
    end

    # Date formatting helper
    register_helper(:format_date) do |date, format = '%Y-%m-%d'|
      date.respond_to?(:strftime) ? date.strftime(format) : date.to_s
    end
  end
end

class TemplateContext
  def initialize(engine, variables = {})
    @engine = engine
    @variables = variables

    # Set instance variables for template access
    variables.each do |key, value|
      instance_variable_set("@#{key}", value)
      define_singleton_method(key) { instance_variable_get("@#{key}") }
    end

    # Add helper methods
    @engine.instance_variable_get(:@helpers).each do |name, block|
      define_singleton_method(name, &block)
    end
  end

  def render_partial(partial_name, local_vars = {})
    @engine.render_partial(partial_name, @variables.merge(local_vars))
  end

  def get_binding
    binding
  end
end

# Example usage:

# Create template files:
# templates/user_profile.erb
user_profile_template = <<~ERB
  <div class="user-profile">
    <h2><%= h(user[:name]) %></h2>
    <p>Email: <%= h(user[:email]) %></p>
    <p>Joined: <%= format_date(user[:created_at], '%B %d, %Y') %></p>

    <% if user[:bio] %>
      <div class="bio">
        <h3>Bio</h3>
        <p><%= h(user[:bio]) %></p>
      </div>
    <% end %>

    <div class="stats">
      <%= render_partial('user_stats', stats: user[:stats]) %>
    </div>
  </div>
ERB

# templates/layouts/application.erb
layout_template = <<~ERB
  <!DOCTYPE html>
  <html>
  <head>
    <title><%= page_title %> - My App</title>
    <meta charset="utf-8">
    <style>
      body { font-family: Arial, sans-serif; margin: 40px; }
      .user-profile { border: 1px solid #ddd; padding: 20px; border-radius: 5px; }
      .stats { margin-top: 20px; padding: 10px; background: #f5f5f5; }
    </style>
  </head>
  <body>
    <header>
      <h1><%= page_title %></h1>
    </header>

    <main>
      <%= content %>
    </main>

    <footer>
      <p>Generated at <%= Time.now.strftime('%Y-%m-%d %H:%M:%S') %></p>
    </footer>
  </body>
  </html>
ERB

# templates/partials/_user_stats.erb
partial_template = <<~ERB
  <h4>User Statistics</h4>
  <ul>
    <li><%= pluralize(stats[:posts], 'post') %></li>
    <li><%= pluralize(stats[:comments], 'comment') %></li>
    <li><%= pluralize(stats[:likes], 'like') %></li>
  </ul>
ERB

# Simulate saving templates to files (you would normally save these to actual files)
require 'fileutils'

FileUtils.mkdir_p('templates/layouts')
FileUtils.mkdir_p('templates/partials')

File.write('templates/user_profile.erb', user_profile_template)
File.write('templates/layouts/application.erb', layout_template)
File.write('templates/partials/_user_stats.erb', partial_template)

# Use the template engine
engine = AdvancedTemplateEngine.new('templates')

# Add custom helper
engine.register_helper(:currency) do |amount|
  "$#{sprintf('%.2f', amount)}"
end

# Data to render
user_data = {
  page_title: 'User Profile',
  user: {
    name: 'John Doe',
    email: 'john@example.com',
    bio: 'Ruby developer and ERB enthusiast',
    created_at: Date.new(2020, 6, 15),
    stats: {
      posts: 42,
      comments: 156,
      likes: 89
    }
  }
}

# Render template with layout
result = engine.render('user_profile', user_data, layout: 'application')
puts result

ERB in Web Applications

ERB is commonly used in web frameworks. Here's how to integrate ERB with a simple Rack application.

ERB with Rack Application

require 'rack'
require 'erb'
require 'json'

class ERBWebApp
  def initialize
    @template_engine = AdvancedTemplateEngine.new('views')
    setup_routes
  end

  def call(env)
    request = Rack::Request.new(env)

    route_handler = find_route(request.request_method, request.path_info)

    if route_handler
      response = route_handler.call(request)
      format_response(response)
    else
      not_found_response
    end
  end

  private

  def setup_routes
    @routes = {
      ['GET', '/'] => method(:home),
      ['GET', '/users'] => method(:users_index),
      ['GET', %r{^/users/(\d+)$}] => method(:users_show),
      ['POST', '/users'] => method(:users_create)
    }
  end

  def find_route(method, path)
    @routes.each do |(route_method, route_path), handler|
      if route_method == method
        if route_path.is_a?(Regexp)
          if match = route_path.match(path)
            return ->(request) { handler.call(request, *match.captures) }
          end
        elsif route_path == path
          return handler
        end
      end
    end
    nil
  end

  def home(request)
    render_template('home', {
      page_title: 'Welcome',
      message: 'Welcome to our ERB-powered web application!',
      current_time: Time.now
    })
  end

  def users_index(request)
    users = [
      { id: 1, name: 'Alice', email: 'alice@example.com' },
      { id: 2, name: 'Bob', email: 'bob@example.com' },
      { id: 3, name: 'Charlie', email: 'charlie@example.com' }
    ]

    render_template('users/index', {
      page_title: 'All Users',
      users: users
    })
  end

  def users_show(request, user_id)
    # Simulate finding user
    user = {
      id: user_id.to_i,
      name: "User #{user_id}",
      email: "user#{user_id}@example.com",
      bio: "This is user number #{user_id}",
      created_at: Date.today - rand(365)
    }

    render_template('users/show', {
      page_title: "User #{user[:name]}",
      user: user
    })
  end

  def users_create(request)
    # Parse form data
    name = request.params['name']
    email = request.params['email']

    if name && email && !name.empty? && !email.empty?
      # Simulate user creation
      new_user = {
        id: rand(1000),
        name: name,
        email: email,
        created_at: Date.today
      }

      # Redirect to user page
      {
        status: 302,
        headers: { 'Location' => "/users/#{new_user[:id]}" },
        body: ''
      }
    else
      render_template('users/new', {
        page_title: 'Create User',
        error: 'Name and email are required',
        name: name,
        email: email
      })
    end
  end

  def render_template(template_name, variables = {})
    html = @template_engine.render(template_name, variables, layout: 'application')
    {
      status: 200,
      headers: { 'Content-Type' => 'text/html' },
      body: html
    }
  end

  def format_response(response)
    [
      response[:status],
      response[:headers],
      [response[:body]]
    ]
  end

  def not_found_response
    [404, { 'Content-Type' => 'text/html' }, ['<h1>404 Not Found</h1>']]
  end
end

# Create views templates for the web app:

# views/home.erb
home_template = <<~ERB
  <div class="hero">
    <h2><%= h(message) %></h2>
    <p>Current server time: <%= format_date(current_time, '%Y-%m-%d %H:%M:%S %Z') %></p>
    <a href="/users" class="button">View All Users</a>
  </div>
ERB

# views/users/index.erb
users_index_template = <<~ERB
  <h2>All Users</h2>
  <table>
    <thead>
      <tr>
        <th>ID</th>
        <th>Name</th>
        <th>Email</th>
        <th>Actions</th>
      </tr>
    </thead>
    <tbody>
      <% users.each do |user| %>
        <tr>
          <td><%= user[:id] %></td>
          <td><%= h(user[:name]) %></td>
          <td><%= h(user[:email]) %></td>
          <td><a href="/users/<%= user[:id] %>">View</a></td>
        </tr>
      <% end %>
    </tbody>
  </table>
ERB

# views/users/show.erb
users_show_template = <<~ERB
  <div class="user-detail">
    <h2><%= h(user[:name]) %></h2>
    <p><strong>Email:</strong> <%= h(user[:email]) %></p>
    <p><strong>ID:</strong> <%= user[:id] %></p>
    <p><strong>Joined:</strong> <%= format_date(user[:created_at], '%B %d, %Y') %></p>

    <% if user[:bio] %>
      <div class="bio">
        <h3>Bio</h3>
        <p><%= h(user[:bio]) %></p>
      </div>
    <% end %>

    <a href="/users">← Back to All Users</a>
  </div>
ERB

# views/layouts/application.erb (similar to previous example)
app_layout_template = <<~ERB
  <!DOCTYPE html>
  <html>
  <head>
    <title><%= h(page_title) %> - ERB Web App</title>
    <meta charset="utf-8">
    <style>
      body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
      .hero { text-align: center; padding: 40px; background: #f8f9fa; border-radius: 8px; }
      .button { display: inline-block; padding: 10px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 4px; }
      table { width: 100%; border-collapse: collapse; margin: 20px 0; }
      th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
      th { background: #f8f9fa; }
      .user-detail { max-width: 600px; margin: 20px 0; }
    </style>
  </head>
  <body>
    <header>
      <h1><a href="/" style="text-decoration: none; color: inherit;">ERB Web App</a></h1>
    </header>

    <main>
      <%= content %>
    </main>

    <footer>
      <hr>
      <p><small>Powered by ERB and Rack</small></p>
    </footer>
  </body>
  </html>
ERB

# Save templates
FileUtils.mkdir_p('views/layouts')
FileUtils.mkdir_p('views/users')

File.write('views/home.erb', home_template)
File.write('views/users/index.erb', users_index_template)
File.write('views/users/show.erb', users_show_template)
File.write('views/layouts/application.erb', app_layout_template)

# config.ru for running the app
# run ERBWebApp.new

ERB Performance and Optimization

ERB performance can be optimized through caching, precompilation, and efficient template design.

ERB Caching and Optimization

class OptimizedERB
  def initialize
    @compiled_templates = {}
    @template_cache = {}
    @cache_enabled = true
  end

  def enable_cache
    @cache_enabled = true
  end

  def disable_cache
    @cache_enabled = false
    clear_cache
  end

  def clear_cache
    @compiled_templates.clear
    @template_cache.clear
  end

  def render(template_content, variables = {}, cache_key: nil)
    if @cache_enabled && cache_key && @template_cache[cache_key]
      return @template_cache[cache_key]
    end

    # Compile template (with caching)
    erb = get_compiled_template(template_content)

    # Create binding with variables
    template_binding = create_binding(variables)

    # Render
    result = erb.result(template_binding)

    # Cache result if cache key provided
    if @cache_enabled && cache_key
      @template_cache[cache_key] = result
    end

    result
  end

  def precompile_template(template_content)
    # Pre-compile template for better performance
    erb = ERB.new(template_content, trim_mode: '-')
    template_hash = template_content.hash
    @compiled_templates[template_hash] = erb
    template_hash
  end

  def render_with_benchmark(template_content, variables = {})
    start_time = Time.now
    result = render(template_content, variables)
    end_time = Time.now

    {
      result: result,
      render_time: ((end_time - start_time) * 1000).round(2) # milliseconds
    }
  end

  private

  def get_compiled_template(template_content)
    template_hash = template_content.hash

    @compiled_templates[template_hash] ||= begin
      ERB.new(template_content, trim_mode: '-')
    end
  end

  def create_binding(variables)
    context = Object.new

    variables.each do |key, value|
      context.instance_variable_set("@#{key}", value)
      context.define_singleton_method(key) { instance_variable_get("@#{key}") }
    end

    # Add helper methods
    context.define_singleton_method(:h) { |text| ERB::Util.html_escape(text.to_s) }

    context.instance_eval { binding }
  end
end

# Performance testing
erb_engine = OptimizedERB.new

template = <<~ERB
  <h1><%= h(title) %></h1>
  <ul>
  <% items.each do |item| -%>
    <li><%= h(item[:name]) %> - $<%= '%.2f' % item[:price] %></li>
  <% end -%>
  </ul>
  <p>Total: $<%= '%.2f' % items.sum { |item| item[:price] } %></p>
ERB

data = {
  title: 'Shopping Cart',
  items: [
    { name: 'Apple', price: 1.50 },
    { name: 'Banana', price: 0.75 },
    { name: 'Orange', price: 2.00 }
  ]
}

# Test without caching
result1 = erb_engine.render_with_benchmark(template, data)
puts "First render (no cache): #{result1[:render_time]}ms"

# Test with caching
result2 = erb_engine.render_with_benchmark(template, data.merge(cache_key: 'shopping_cart'))
puts "Second render (cached): #{result2[:render_time]}ms"

# Test cache hit
result3 = erb_engine.render_with_benchmark(template, data.merge(cache_key: 'shopping_cart'))
puts "Third render (cache hit): #{result3[:render_time]}ms"

# Test precompilation
template_id = erb_engine.precompile_template(template)
puts "Template precompiled with ID: #{template_id}"

ERB Best Practices

  • Always escape user input using ERB::Util.html_escape or helper methods
  • Use meaningful variable names in your templates
  • Keep complex logic out of templates - use helper methods instead
  • Use layouts and partials to avoid code duplication
  • Cache compiled templates for better performance
  • Use trim mode (-) to avoid unnecessary whitespace in output
  • Validate template data before rendering to avoid errors
  • Consider using safe mode for untrusted templates
  • Use descriptive file naming conventions (e.g., _partial.erb for partials)
  • Test your templates thoroughly with various data scenarios

Quick Navigation

Related Topics

Video Tutorial

Watch and learn erb templating

Pro Tip: After reading through the content above, watch this video to reinforce your understanding and see the concepts in action!