ruby / templates
The web framework renders HTML
with Haml templates, layouts, partials, and content_for.
Typed view data with Data.define
Passing raw hashes to templates is error-prone.
Instead, define Data.define structs in the handler
that specify the exact contract with the template:
module CompaniesHandler
class Index < Framework::Handler
PageData = Data.define(:summary, :rows)
Row = Data.define(:name, :status, :edit_url)
def handle
companies = Companies::All.new(db).call
count = companies.count { |co| co["active"] }
data = PageData.new(
summary: "#{count} active",
rows: companies.map { |co|
Row.new(
name: co["name"].to_s,
status: co["status"].to_s,
edit_url: "/companies/edit?id=#{co["id"]}"
)
}
)
render "companies/index", data: data
end
end
end
The template receives a data struct and renders HTML.
It accesses data.field and row.field but does not
call formatters, access hash keys, or transform data:
= content_for :title, "Companies"
%h1
= data.summary
%table
- data.rows.each do |row|
%tr
%td
= row.name
%td
= row.status
%td
%a{href: row.edit_url}
Edit
This provides:
- Explicit contracts:
Data.definedocuments what the template needs - Security: sensitive fields from queries never reach templates
- Linear debugging: errors originate in handler or template, not both
- Testable formatting: unit test handler output without rendering HTML
Dumb templates
Templates should render pre-formatted data into HTML, not run logic. I restrict Haml to a "dumb" subset:
Allowed:
- Tags:
%tag,.class,#id,{ key: value }attributes - Escaped output:
= field(field access only) - Unescaped output:
!= field(for pre-escaped HTML from handler) - Conditionals:
- if field/- else - Loops:
- items.each do |item| - Partials:
= render "name", key: value - Static text and comments
Banned:
- Ruby method calls:
.map,.join,.any?,.to_s,.size - Module/class references in templates
- String interpolation with logic
- Hash access:
company["name"] - Variable assignment:
- x = expr
= always HTML-escapes. != outputs raw.
Handlers must pre-escape or pre-sanitize anything that needs !=.
Why Haml
Haml is structure-aware. Indentation maps to HTML nesting, so the parser builds an AST rather than concatenating strings. This prevents entire classes of XSS bugs that string template systems create by interpolating untrusted data into raw HTML strings.
Rendering
The template engine wraps Haml with layout support,
partials, and content_for:
module Framework
class Template
CACHE = Concurrent::Map.new
VIEWS_PATH = File.expand_path("../ui/views", __dir__)
# Pre-load all templates at boot
def self.cache_all(views_path = VIEWS_PATH)
Dir.glob(File.join(views_path, "**/*.haml")).each do |path|
CACHE.compute_if_absent(path) {
Haml::Template.new(path, default_encoding: "UTF-8")
}
end
end
def self.render(name, locals = {}, layout: "layouts/application")
context = Context.new(locals)
content = render_template(name, context, locals)
if layout
context.content_blocks[:_main] = content
render_template(layout, context, locals) { |name = nil|
if name.nil?
content
else
context.content_blocks[name].to_s
end
}
else
content
end
end
def self.render_template(name, context, locals, &block)
path = File.join(VIEWS_PATH, "#{name}.haml")
template = CACHE.compute_if_absent(path) {
Haml::Template.new(path, default_encoding: "UTF-8")
}
template.render(context, locals, &block)
end
end
end
Templates are pre-loaded into a Concurrent::Map at boot
to catch missing files early and avoid disk reads per-request.