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.
<!-- 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>
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>
ERB templates are typically stored in separate files and rendered with data from your Ruby application.
# 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>
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
ERB provides several advanced features for more complex templating scenarios.
# 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
# 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)
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
Let's create a more sophisticated template engine that includes layouts, partials, and helper methods.
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 is commonly used in web frameworks. Here's how to integrate ERB with a simple 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 can be optimized through caching, precompilation, and efficient template design.
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}"
Watch and learn erb templating