Documentation

Views

Updated
Browse documentation

Published 2026-08-13. V2 is a prerelease; the web core is release-gated and other previews are named separately. What beta means.

Read this page as HTML, Markdown, or structured JSON—or open the published Markdown with an AI assistant. Gemini receives the prompt through your clipboard because its signed-out page does not reliably prefill URL text; paste when the new tab opens. External assistants need the public site URL.

Views

Amber V2's supported web path renders HTML with Crystal ECR templates. The convention is deliberately small:

  • controllers load resources and declare response formats;
  • ECR templates own HTML;
  • the application layout owns the document shell and local assets;
  • public/ owns files the browser requests directly.

Negotiate HTML and JSON in one action

Use respond_with when one resource has more than one representation. The action loads the resource once and makes each public format explicit:

File: src/controllers/articles_controller.cr — add this action inside ArticlesController.

Crystal
class ArticlesController < ApplicationController
  def show
    article = ArticleCatalog.fetch(params["slug"])

    respond_with do
      html { render("show.ecr") }
      json { article.to_json }
    end
  end
end

A request with Accept: text/html renders the ECR template and layout. A request with Accept: application/json runs only the JSON block. Amber also recognizes supported path extensions when the route accepts that path. If the request asks for no available representation, Amber returns 406 Not Acceptable.

Keep representation selection in the controller. Do not duplicate resource loading in separate HTML and JSON actions unless the application behavior is actually different. Register the matching route in config/routes.cr; see Routes for the complete route boundary.

Generated view structure

A clean web application starts with:

Generated files:

File tree
src/views/
├── home/index.ecr
└── layouts/application.ecr

As the application grows, group templates by controller and name reusable partials with a leading underscore:

Reference structure:

File tree
src/views/
├── articles/
│   ├── _meta.ecr
│   ├── index.ecr
│   └── show.ecr
└── layouts/
    └── application.ecr

File: src/controllers/application_controller.cr — keep this constant inside the generated base controller.

Crystal
class ApplicationController < Amber::Controller::Base
  LAYOUT = "application.ecr"
end

Render ECR safely

Local variables in the controller action are available to the rendered ECR. ECR does not automatically escape interpolation, so escape values that can contain user or external data.

File: src/views/articles/show.ecr — create this template for the controller's render("show.ecr") branch.

ECR template
<article class="article-shell">
  <p class="eyebrow">Field note</p>
  <h1><%= escape_html(article[:title]) %></h1>
  <p><%= escape_html(article[:summary]) %></p>

  <%= render(partial: "articles/_meta.ecr") %>
</article>

The layout receives the completed action template as content. That value is framework-rendered HTML, so it is intentionally inserted without escaping.

File: src/views/layouts/application.ecr — this is a complete minimal layout; merge the asset tags into an existing branded layout instead of discarding its navigation and metadata.

ECR template
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <%= stylesheet_link_tag("stylesheets/app.css") %>
  </head>
  <body>
    <%= content %>
    <%= javascript_importmap_tag(
      {"app" => "javascript/app.js"},
      preload: ["javascript/app.js"]
    ) %>
    <script type="module">import "app";</script>
  </body>
</html>

The common rendering forms belong inside controller actions.

File: a controller under src/controllers/, such as src/controllers/articles_controller.cr — choose the form that matches the view file you created.

Crystal
render("show.ecr")
render(partial: "articles/_meta.ecr")
render("card.ecr", layout: false)
render("admin/show.ecr", layout: "admin.ecr")

Front-end boundary

The layout above uses a browser-native import map. The app name resolves to a local ES module served by Amber's static pipeline. The generated baseline needs no Node.js dependency, package manager, bundler, UI framework, or CDN.

Read Import maps for the complete local-module pattern and Web template for the exact generated project structure.

The V2 CLI generator emits ECR only. Slang, Kilt, Mustache, and Temel examples on the V1 site remain maintenance references for old applications, not choices in the supported V2 web template.