Templates
.erbfiles compiled to Ruby at build time, with their locals declared on line one.
Templates are .erb files in views/. They are compiled to plain Ruby methods at build time — there is no
ERB at runtime and no eval anywhere — so a syntax mistake in a template is a build error naming the .erb
file and line rather than a 500 in production.
views/layout.erb the layout, applied automatically when this file exists
views/display.erb render :display
views/_events.erb partial :events
Declaring locals
The first line of every template declares its locals. A template with no declaration, or one declaring
locals: (), takes none, and passing one is a build error.
<%# locals: (events:, notes:, empty:, compact: false) %>
The declaration becomes the method’s keyword parameters, so events: is required and compact: false has a
default. A missing or misspelled local at a render or partial call site is a build error naming the
template and the line of the call — see “What the build catches” below. A name used in the template body
that was never declared is still a NameError when the template runs.
render
get "/notes" do
render :notes, notes: db.query("SELECT id, text FROM notes"), heading: "All notes"
end
render(name, layout: true, **locals) -> String. name is a Symbol matching views/<name>.erb. It returns
the rendered HTML as a String, which the route returns as the response body. layout: false skips the
layout. A name with no file is a build error, no template views/<name>.erb. Always write a literal Symbol;
a computed one defeats the whole design, and skips the check — it raises the same message at runtime.
Layout
If views/layout.erb exists it wraps every render that does not pass layout: false. The layout is an
ordinary template; it must declare content: and emit it unescaped:
<%# locals: (content:) %>
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title><%= app_title %></title>
<link rel="stylesheet" href="/app.css"></head>
<body><%== content %></body>
</html>
With no views/layout.erb, there is no layout and layout: false is a no-op.
Locals the layout declares
A layout usually wants a per-page title or meta description. Declare it as a local of the layout and
render forwards it there, alongside content::
<%# locals: (content:, title: "at.here", description: nil) %>
<!doctype html>
<title><%= title %></title>
<% if description %><meta name="description" content="<%= description %>"><% end %>
<body><%== content %></body>
render :card, handle: h, title: "#{h} on at.here", description: bio
handle: goes to the page, description: to the layout, and title: to both if the page declares it too.
- A keyword goes to whichever template declares it, and to both when both do. One neither declares is a build error naming both files.
- A layout local with a default may be left out; one the layout requires has to be passed by every
renderthat keeps the layout, or the build fails. content:is the layout’s own local — it is the rendered page — so a call site never passes it.layout: falseskips the layout, and with it the forwarding.- When both declare the name and both give it a default, the layout’s default is the one that reaches
the page through
render; the page’s own applies only to a directViews.<name>(...)call. name,layout,localsandbodyare what the generatedrendercalls its own parameters, so a layout cannot declare them. A layout local’s default must be a literal (a String, Symbol, number,true/false/nil, or an Array or Hash of those), becauserenderevaluates it where the layout’s other locals do not exist. Either is a build error naming the layout.
Partials
<%= partial :events, events: today, notes: today_notes, empty: "Nothing on." %>
partial(name, **locals) -> String renders views/_<name>.erb. The leading underscore is in the filename
only, never in the call. Partials declare locals the same way and never take a layout.
Inside a template you may also write <%= render "events", events: today %> with a String name; the
compiler rewrites it to a direct call. Both spellings work; prefer partial :events in templates and
render :page in routes so the two roles stay distinguishable.
Tags
| Tag | Meaning |
|---|---|
<%= expr %> | evaluate and output, HTML-escaped |
<%== expr %> | evaluate and output raw, no escaping |
<% code %> | Ruby statement: if, each, end — no output |
<%# comment %> | dropped (this is also where locals: lives) |
-%> | trims the newline that follows the tag |
Escaping is on for <%= %> and escapes & < > " '. The three helpers that return HTML on purpose —
render, partial and csrf_field — are recognised and not escaped. Everything else is. Use <%== %>
only for HTML you built yourself; never for anything derived from params, the database or an HTTP
response.
What you can call in a template
Anything top level: app_title, csrf_field, h, params, session, request, setting, and any method
you defined in app.rb or lib/. Ruby core is available too — Time.at(...), strftime, String,
Array, Hash.
<%# locals: (feeds:) %>
<% feeds.each do |f| -%>
<tr>
<td><span class="dot" style="background:<%= f["colour"] %>"></span> <%= f["name"] %></td>
<td><%= f["fetched_at"] ? Time.at(f["fetched_at"].to_i).strftime("%-d %b %H:%M") : "never" %></td>
</tr>
<% end -%>
Database rows are Hash{String => value}: index them with f["colour"], never f[:colour].
Every form that changes something needs the CSRF field:
<form method="post" action="/notes">
<%= csrf_field %>
<input type="text" name="text" required>
<button>Add</button>
</form>
What the build catches
The build step syntax-checks each template on its own, so an unbalanced if or each is a build error
naming the file and the .erb line:
views/broken.erb:2: unexpected end-of-input, assuming it is closing the parent top level context
It then reads every render and partial call in app.rb, lib/**/*.rb and the templates themselves, and
checks it against the template it names:
app.rb:30: no template views/logon.erb
app.rb:30: views/login.erb does not declare a local `errro`
app.rb:30: views/login.erb requires `error`
views/display.erb:22: views/_events.erb requires `empty`
app.rb:30: neither views/login.erb nor views/layout.erb declares a local `titel`
app.rb:30: views/layout.erb requires `title`
views/layout.erb:1: the layout requires `title`, which no render passes
The layout is checked too: it must declare content, and a local it requires that no render passes is a
build error against the layout rather than a 500 on every page.
A call is skipped when there is nothing to check it against: a template name that is not a literal Symbol,
or locals passed as **hash. Those raise at runtime instead, as does a name used in a template body that
was never declared. Load every page under melee dev before pushing. One caveat for a skipped call: a
local the layout declares has a value in render whatever the call site did, so where an unchecked call
used to raise missing keyword it now renders the layout’s default instead — another reason to keep
template names literal.
What is not here
No content_for/yield, no nested layouts, no template inheritance, no helper modules to include, no
.erb.html or other extensions, no HAML/Slim, no runtime template lookup by a computed name, no capture.
Templates cannot be added or changed without a rebuild — they are compiled into the binary.