Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Templates

.erb files 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 render that 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: false skips 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 direct Views.<name>(...) call.
  • name, layout, locals and body are what the generated render calls 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), because render evaluates 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

TagMeaning
<%= 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.