{"title":"Views","description":"Render Amber V2 HTML with Crystal ECR templates and explicit response formats","section":"guides","version":"v2","path":"guides/views","canonical_url":"https://amberframework.org/docs/v2/guides/views","markdown_url":"https://amberframework.org/docs/v2/guides/views.md","inherited":false,"content_markdown":"# Views\n\nAmber V2's supported web path renders HTML with Crystal ECR templates. The\nconvention is deliberately small:\n\n- controllers load resources and declare response formats;\n- ECR templates own HTML;\n- the application layout owns the document shell and local assets;\n- `public/` owns files the browser requests directly.\n\n## Negotiate HTML and JSON in one action\n\nUse `respond_with` when one resource has more than one representation. The\naction loads the resource once and makes each public format explicit:\n\n**File: `src/controllers/articles_controller.cr` — add this action inside\n`ArticlesController`.**\n\n```crystal\nclass ArticlesController < ApplicationController\n  def show\n    article = ArticleCatalog.fetch(params[\"slug\"])\n\n    respond_with do\n      html { render(\"show.ecr\") }\n      json { article.to_json }\n    end\n  end\nend\n```\n\nA request with `Accept: text/html` renders the ECR template and layout. A\nrequest with `Accept: application/json` runs only the JSON block. Amber also\nrecognizes supported path extensions when the route accepts that path. If the\nrequest asks for no available representation, Amber returns `406 Not\nAcceptable`.\n\nKeep representation selection in the controller. Do not duplicate resource\nloading in separate HTML and JSON actions unless the application behavior is\nactually different. Register the matching route in `config/routes.cr`; see\n[Routes](../routing/routes/) for the complete route boundary.\n\n## Generated view structure\n\nA clean web application starts with:\n\n**Generated files:**\n\n```text\nsrc/views/\n├── home/index.ecr\n└── layouts/application.ecr\n```\n\nAs the application grows, group templates by controller and name reusable\npartials with a leading underscore:\n\n**Reference structure:**\n\n```text\nsrc/views/\n├── articles/\n│   ├── _meta.ecr\n│   ├── index.ecr\n│   └── show.ecr\n└── layouts/\n    └── application.ecr\n```\n\n**File: `src/controllers/application_controller.cr` — keep this constant inside\nthe generated base controller.**\n\n```crystal\nclass ApplicationController < Amber::Controller::Base\n  LAYOUT = \"application.ecr\"\nend\n```\n\n## Render ECR safely\n\nLocal variables in the controller action are available to the rendered ECR.\nECR does not automatically escape interpolation, so escape values that can\ncontain user or external data.\n\n**File: `src/views/articles/show.ecr` — create this template for the controller's\n`render(\"show.ecr\")` branch.**\n\n```ecr\n<article class=\"article-shell\">\n  <p class=\"eyebrow\">Field note</p>\n  <h1><%= escape_html(article[:title]) %></h1>\n  <p><%= escape_html(article[:summary]) %></p>\n\n  <%= render(partial: \"articles/_meta.ecr\") %>\n</article>\n```\n\nThe layout receives the completed action template as `content`. That value is\nframework-rendered HTML, so it is intentionally inserted without escaping.\n\n**File: `src/views/layouts/application.ecr` — this is a complete minimal layout;\nmerge the asset tags into an existing branded layout instead of discarding its\nnavigation and metadata.**\n\n```ecr\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <%= stylesheet_link_tag(\"stylesheets/app.css\") %>\n  </head>\n  <body>\n    <%= content %>\n    <%= javascript_importmap_tag(\n      {\"app\" => \"javascript/app.js\"},\n      preload: [\"javascript/app.js\"]\n    ) %>\n    <script type=\"module\">import \"app\";</script>\n  </body>\n</html>\n```\n\nThe common rendering forms belong inside controller actions.\n\n**File: a controller under `src/controllers/`, such as\n`src/controllers/articles_controller.cr` — choose the form that matches the\nview file you created.**\n\n```crystal\nrender(\"show.ecr\")\nrender(partial: \"articles/_meta.ecr\")\nrender(\"card.ecr\", layout: false)\nrender(\"admin/show.ecr\", layout: \"admin.ecr\")\n```\n\n## Front-end boundary\n\nThe layout above uses a browser-native import map. The `app` name resolves to a\nlocal ES module served by Amber's static pipeline. The generated baseline needs\nno Node.js dependency, package manager, bundler, UI framework, or CDN.\n\nRead [Import maps](../assets/import-maps/) for the complete local-module pattern\nand [Web template](../web-template/) for the exact generated project structure.\n\nThe V2 CLI generator emits ECR only. Slang, Kilt, Mustache, and Temel examples\non the V1 site remain maintenance references for old applications, not choices\nin the supported V2 web template."}