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

2. Routes and templates

A route that returns a page, a layout that wraps it, a partial it reuses, and why <%= and <%== are different.

Replace app.rb with a notes board. The notes are hardcoded for now; step 3 puts them in a database.

# frozen_string_literal: true
title "Notes"

NOTES = ["Buy milk & bread", "Book the <dentist>", "Water the ferns"].freeze

get "/" do
  render :index, notes: NOTES
end

title names the app; app_title reads it back, and the log records it. The route block takes no arguments, and whatever it returns is the response — a String is HTML with status 200, and render returns a String.

Templates

Templates are .erb files in views/. render :index means views/index.erb.

<%# locals: (notes:) %>
<ul class="notes">
  <% notes.each do |note| %>
    <%== partial :note, note: note %>
  <% end %>
</ul>
<p class="count"><%= notes.size %> open</p>

The first line is not a comment you can skip. Every template declares its locals, and that declaration becomes the method signature the template is compiled into. It is what lets the build step check every render call in your app against the template it names — a missing or misspelled local is an error at build time, with the line in app.rb that got it wrong, rather than a NameError in front of a visitor.

(notes:) is required. (notes:, error: nil) makes error optional with a default.

A template can only see its declared locals plus the top-level helpers (params, session, h, csrf_field, app_title and the rest). There is no implicit access to instance variables, because there is no instance.

Partials

views/_note.erb — the underscore marks it as a partial:

<%# locals: (note:) %>
<li><%= note %></li>

Rendered with partial :note, note: note. A partial has its own declared locals and gets nothing it was not handed.

Layout

views/layout.erb wraps every render unless you pass layout: false:

<%# locals: (content:) %>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><%= app_title %></title>
<link rel="stylesheet" href="/app.css">
</head>
<body>
<h1><%= app_title %></h1>
<%== content %>
</body>
</html>

<%= escapes, <%== does not

This is the one piece of ERB syntax that differs from what you may be used to, and it matters.

<%= value %>HTML-escapes the value. Use it for everything that came from a person.
<%== value %>Inserts it raw. Use it only for HTML you produced — content in the layout, partial output, csrf_field.
<% ... %>Runs Ruby, prints nothing.

Escaping is the default, so the dangerous thing is the one you have to type an extra character for. Load the page and look at the source:

<li>Buy milk &amp; bread</li>
<li>Book the &lt;dentist&gt;</li>

Both notes went through <%= note %>, so both are safe text rather than markup.

Routes

get "/x"     post "/x"     put "/x"     patch "/x"     delete "/x"

get "/notes/:id" do            # params.fetch(:id)
get "/files/*path" do          # params.fetch(:path) — the rest of the path

Two more that are worth knowing now:

not_found do "No such page" end
error do |e| "Something broke: #{e.message}" end

Static files need no route at all: anything in public/ is served straight from disk, so public/app.css answers at /app.css.

Restart and look

melee dev has no reload, so Ctrl-C and start it again:

melee dev: prepared 3 templates, 1 migrations
melee dev: http://127.0.0.1:4567 (Ctrl-C to stop)
GET / -> 200 (0.1 ms)

Next

A database — real notes, in SQLite.

Full detail: The app and Templates.