Templates use tempura syntax, a Handlebars-like syntax. Layouts, partials, .hbs content, and output files are all templates.
Syntax
These are the tempura forms that these docs use:
| Syntax | Meaning |
|---|---|
{{ value }} |
Print a value, escaped for HTML. |
{{{ value }}} |
Print a value as it is. Use it for HTML, such as $page.content. |
{{! comment }} |
A comment. It prints nothing. |
{{#if test}} … {{#elif other}} … {{#else}} … {{/if}} |
Print the first block whose test is true. |
{{#each list as item}} … {{/each}} |
Print the block once for each item. as (item, i) also names the index. |
{{#var name = value}} |
Declare a variable. See Your own variables. |
{{#expect a, b}} |
Declare the arguments that a partial reads. |
{{#partial src="name"}} |
Call a partial. See Partials. |
Values are JavaScript expressions, so {{ $pages.length }} and {{#each $section.filter(post => post.params.date) as post}} work. See the tempura docs for the full syntax.
tempura removes the line break just before each {{ }} tag and at the end of the file. Put a tag at the end of its line to keep the output tidy.
Layouts
A layout is a .hbs file in layouts/. It wraps the content of a page. {{{ $page.content }}} prints the page body.
<!doctype html>
<html lang="{{ $site.lang }}">
<head>
<meta charset="utf-8"/>
<title>{{ $page.title }} — {{ $site.title }}</title>
<meta name="description" content="{{ $page.summary }}"/>
</head>
<body>
{{{ $page.content }}}
</body>
</html>Find the layout
For each page, bongo uses the first layout that exists in a fixed order. It looks in the folder of layouts/ that matches the page's folder first, then in each parent folder, then in layouts/ itself. For content/blog/2025/post.md, the order is:
layouts/blog/2025/post.hbslayouts/blog/2025/single.hbslayouts/blog/2025/index.hbslayouts/blog/2025/html.hbslayouts/blog/post.hbslayouts/blog/single.hbslayouts/blog/index.hbslayouts/blog/html.hbslayouts/post.hbslayouts/single.hbslayouts/index.hbslayouts/html.hbs
A page uses the index.hbs of its folder when there is no single.hbs, so one index.hbs can serve a whole folder.
For an index page, bongo looks for index.hbs in place of the file name and single.hbs. For content/blog/index.md, the order is:
layouts/blog/index.hbslayouts/blog/html.hbslayouts/index.hbslayouts/html.hbs
A page in the root of content/ uses only the root of layouts/. For content/about.md, the order is layouts/about.hbs, layouts/single.hbs, layouts/index.hbs, then layouts/html.hbs.
Layout chain
When the first layout is not an html.hbs, bongo renders the page through it, then through the nearest html.hbs. The output of the first layout becomes $page.content in the html.hbs. Thus a page renders through one or two layouts.
The starter site uses this chain for its posts. layouts/blog/single.hbs wraps each post, and layouts/html.hbs wraps the result:
<article class="post">
<header>
<h1>{{ $page.title }}</h1>
{{#partial src="postmeta" date=$page.params.date tags=$page.tags}}
</header>
<div class="post-body">
{{{ $page.content }}}
</div>
</article>When bongo finds no html.hbs for a page, the build fails with Missing "html.hbs" template.
Variables
Templates read the build through reserved variables: $site, $page, $pages, $section, $tags, $data, $utils, and $assets. {{ $page.title }} prints the title of the current page, and {{#each $section as post}} loops over the pages of its folder. See Variables for what each one holds, with examples.
Your own variables
{{#var name = value}} declares a variable. The value is a JavaScript expression. The variable holds the value from that tag to the end of the file. Use a variable to calculate a value once and use it more than once, or to give a long expression a short name:
{{#var date = $page.params.date}}
{{#var related = $section.filter(post => post.path !== $page.path).slice(0, 3)}}
<article>
<h1>{{ $page.title }}</h1>
{{#if date}}
<time datetime="{{ date }}">{{ $utils.date.format(date, { dateStyle: 'long' }) }}</time>
{{/if}}
{{{ $page.content }}}
{{#if related.length}}
<h2>More posts</h2>
<ul>
{{#each related as (post, i)}}
<li>{{ i + 1 }}. <a href="{{ post.path }}">{{ post.title }}</a></li>
{{/each}}
</ul>
{{/if}}
</article>- To change a variable, declare it again:
{{#var count = count + 1}}. {{#each list as post}}declarespostfor each item.{{#each list as (post, i)}}also declares the index,i, which starts at 0.- Each template has its own variables. A partial does not see the variables of the layout that calls it. Give them to the partial as arguments, such as
{{#partial src="byline" date=date}}, and declare them in the partial with{{#expect date}}. See Partials.
Your own values can also come from:
- the frontmatter of a page, as
$page.params. See Frontmatter. - the files in
data/, as$data. See Data. - your own helper functions, as
$utils. See Utilities.
Special elements
bongo has its own elements for the scripts, styles, and other files of assets/. Put them in a layout or a partial. After the layouts render, bongo builds each file, gives it a content-hashed name, and replaces the element with the HTML that loads the file:
<!doctype html>
<html lang="{{ $site.lang }}">
<head>
<meta charset="utf-8"/>
<title>{{ $page.title }}</title>
<bongo:asset preload src="fonts/inter.woff2" />
<link rel="icon" href="<bongo:asset src='img/favicon.svg' />"/>
<bongo:style src="app.css" />
</head>
<body>
{{{ $page.content }}}
<bongo:client transitions />
<bongo:script src="main.ts" />
</body>
</html>| Element | Use it to |
|---|---|
<bongo:style> |
build a stylesheet with LightningCSS, as a <link> or an inline <style>. See CSS. |
<bongo:script> |
bundle a script with Rolldown, as a module <script> or an inline script. |
<bongo:asset> |
publish any file, such as a font or an icon, as a URL or a <link rel="preload">. |
<bongo:client> |
add the client runtime, for client-side navigation and view transitions. |
<bongo:image> |
resize and convert an image, as a <picture> with a srcset. See Images. |
<bongo:alternate> |
link another version of the page, such as a Markdown copy that bongo writes. See Alternates. |
bongo publishes only the files that these elements and your CSS use. A src that does not exist fails the build. See Special elements for each attribute and the HTML that each element becomes, and TypeScript for type checks of these elements.