Layouts and Blocks
Eta has built-in support for layouts with named content blocks, giving you a powerful way to build page templates with overridable sections.
Layouts
A template file can have one parent layout (though layouts themselves can have parents). To set the parent layout, call the layout() function:
<% layout("./base") %>
<h1>My Page</h1>
<p>This content will be available as `it.body` in the layout.</p>In the layout file (base.eta), render the child content with it.body:
<!DOCTYPE html>
<html>
<head><title><%= it.title %></title></head>
<body>
<%~ it.body %>
</body>
</html>You can also pass extra data to the layout:
<% layout("./base", { title: "My Page" }) %>Blocks
Blocks let you define named content sections that child templates can fill, and layouts can render. This is useful for things like page-specific scripts, styles, or sidebar content.
Defining blocks in a child template
Use the block() helper to define a named block:
<% layout("./base") %>
<% block("title", () => { %>
My Page Title
<% }) %>
<% block("sidebar", () => { %>
<nav>Page-specific sidebar</nav>
<% }) %>
<p>Main content goes in it.body as usual.</p>Rendering blocks in a layout
In the layout, call block() with just the name to render the block's content. You can provide a fallback by passing a function as the second argument:
<!DOCTYPE html>
<html>
<head>
<title><%~ block("title", () => { %>Default Title<% }) %></title>
</head>
<body>
<aside>
<%~ block("sidebar") %>
</aside>
<main>
<%~ it.body %>
</main>
</body>
</html>If the child template defines a "sidebar" block, its content is rendered. If not, the block renders nothing (or the fallback content if one is provided).
Async blocks
For blocks that need to await async operations, use blockAsync():
<% layout("./base") %>
<% blockAsync("data", async () => { %>
<%= await fetchSomeData() %>
<% }) %>In the layout, render with blockAsync():
<%~ await blockAsync("data") %>How blocks work
When a child template calls block("name", fn) and a layout is active, the block content is captured and stored. When the layout later calls block("name"), the stored content is returned.
This means:
- Blocks defined in the child are available to the parent layout
- If no layout is active,
block()renders its content inline (useful for reusable components) - Fallback content in the layout is only used when the child doesn't define that block
Full example
views/page.eta:
<% layout("./layout") %>
<% block("head", () => { %>
<link rel="stylesheet" href="/page.css">
<% }) %>
<% block("scripts", () => { %>
<script src="/page.js"></script>
<% }) %>
<h1><%= it.title %></h1>
<p><%= it.content %></p>views/layout.eta:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<%~ block("head") %>
</head>
<body>
<main><%~ it.body %></main>
<%~ block("scripts", () => { %>
<script src="/default.js"></script>
<% }) %>
</body>
</html>Rendering:
const html = eta.render("./page", {
title: "Hello",
content: "Welcome to my site"
})Output:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="/page.css">
</head>
<body>
<main><h1>Hello</h1>
<p>Welcome to my site</p></main>
<script src="/page.js"></script>
</body>
</html>