Templates Overview
Alloy uses the Liquid template language by default. Templates live in the layouts/ directory, are parsed once at startup, and rendered per page with full access to the site’s data cascade.
# alloy.config.yaml
templates:
engine: "liquid" # default; also supports "gotemplate" (Go html/template)
<!-- layouts/default.liquid -->
<!DOCTYPE html>
<html>
<head><title>{{ page.title }}</title></head>
<body>
{% include "partials/header" %}
{{ content }}
{% include "partials/footer" %}
</body>
</html>
<!-- layouts/default.html -->
<!DOCTYPE html>
<html>
<head><title>{{ .page.title }}</title></head>
<body>
{{ partial "partials/header" }}
{{ .content }}
{{ partial "partials/footer" }}
</body>
</html>
Every content page is rendered through its resolved layout. The layout receives the page’s rendered body as {{ content }} and can access all front matter fields via the page object.
Template engines
Alloy supports two built-in (Tier 1) template engines. The engine is a global, project-wide setting – one engine is active per build.
| Engine | Config value | File extension | Syntax |
|---|---|---|---|
| Liquid | "liquid" (default) |
.liquid |
{{ var }}, {% tag %} |
| Go templates | "gotemplate" |
.html |
{{ .var }}, {{ range }} |
Both engines receive the same map[string]any context from the data cascade. All built-in filters are registered in both engines at startup – in Go templates, filters are called as functions ({{ upcase .page.title }}) rather than with pipe-and-colon syntax. See Filter syntax by engine.
The config value must be exactly "gotemplate" – any other value falls back to Liquid.
Plugin-provided template engines (Nunjucks, EJS, Pug via the Node bridge) are an experimental design concept and are not implemented – today the engine setting accepts only "liquid" and "gotemplate", and any other value falls back to Liquid.
Template context
Every template receives these top-level variables:
| Variable | Description |
|---|---|
page |
Current page data: page.title, page.url, page.date, page.summary, page.toc, plus all front matter fields |
content |
The rendered body of the current page (Markdown already converted to HTML) |
site |
Site-wide data: site.title, site.baseURL, site.language, site.data.*, site.pages |
collections |
Collections from date-based sections and config-declared collections: collections.blog, collections.releases, etc. |
taxonomies |
Taxonomy groups: taxonomies.tags.javascript, taxonomies.categories.tutorials, etc. |
pagination |
Pagination context (only on paginated pages): pagination.pageNumber, pagination.totalPages, pagination.nextPage, pagination.previousPage |
<h1>{{ page.title }}</h1>
<time>{{ page.date | date: "%B %d, %Y" }}</time>
{{ content }}
<h2>Recent posts</h2>
{% for post in collections.blog limit: 5 %}
<a href="{{ post.url }}">{{ post.title }}</a>
{% endfor %}
<h1>{{ .page.title }}</h1>
<time>{{ date .page.date "%B %d, %Y" }}</time>
{{ .content }}
<h2>Recent posts</h2>
{{ range limit .collections.blog 5 }}
<a href="{{ .url }}">{{ .title }}</a>
{{ end }}
Template resolution
The .liquid extension marks a file as a Liquid template. The extension before .liquid determines the output format:
layouts/default.liquid --> HTML output (default)
layouts/feed.xml.liquid --> XML output
layouts/api.json.liquid --> JSON output
The configured engine determines the layout extension: .liquid for Liquid, .html for Go templates. There is no cross-engine fallback – with the Liquid engine, only .liquid layout files are resolved; with the Go engine, only .html files.
layouts/
├── default.liquid <-- used when engine: "liquid"
├── default.html <-- used when engine: "gotemplate"
├── feed.xml.liquid <-- XML format layout (Liquid)
└── feed.xml.html <-- XML format layout (Go templates)
If a file contains syntax for the wrong engine, the build fails with a parse error. Alloy does not inspect file contents to determine the engine.
Rendering pipeline
Alloy renders content in a strict order:
- Markdown rendering – Goldmark parses
.mdfiles into HTML. Template tags ({{ }},{% %}) are preserved through Markdown via a custom goldmark extension. - Template rendering – The configured engine evaluates Liquid or Go template syntax in the rendered output and in layouts.
- Layout wrapping – The page body is injected into its resolved layout via
{{ content }}. Layouts can chain to parent layouts.
Template tags inside <code> blocks in Markdown files are automatically escaped so they display as literal text rather than being evaluated. See the next section for the full rules.
Literal template syntax
Whether {{ }} and {% %} are evaluated depends on where they appear in a Markdown file:
| Where the tags appear | Default | templateTags: false |
|---|---|---|
| Prose / body text | Evaluated by the template engine | Shown as typed |
| Inline code and fenced code blocks | Shown as typed | Shown as typed |
Code is always safe – write `{{ site.title }}` in backticks or a fenced block and it displays exactly as typed, no escaping needed.
Prose is evaluated by default. This line in the page source:
You are reading {{ page.title }}.
renders as:
You are reading Templates Overview.
That quote is live – the tag was evaluated against this page’s context at build time, which is what you want when composing pages. But the same rule applies when a tag is meant as an example:
The syntax {{ user.name }} inserts the author's name.
The syntax inserts the author’s name.
Also live – and user.name is empty in this page’s context, so the rendered sentence has a hole where the example should be. When you want prose tags shown as typed, escalate through three tools:
-
Backticks. If the tag can be code-formatted, put it in an inline code span. Always literal, most common case.
-
{% raw %}for a one-off. Wrap any run of prose in{% raw %}...{% endraw %}and the template engine outputs it verbatim:{% raw %}Handlebars uses {{name}} for interpolation.{% endraw %}{% raw %}is a Liquid tag. Under the Go template engine, emit literal braces with a quoted expression instead:{{ "{{" }}name{{ "}}" }}. -
templateTags: falsefor the whole site. If your content writes about template syntax everywhere and never uses tags in prose, turn prose evaluation off globally in Markdown configuration:content: markdown: goldmark: templateTags: false
Directory structure
layouts/
├── default.liquid # Fallback layout for all pages
├── blog.liquid # Blog index layout (matches section name)
├── post.liquid # Blog post layout (child of date-based section)
└── partials/ # Reusable template fragments
├── header.liquid
└── footer.liquid
The layouts directory is configurable:
# alloy.config.yaml
structure:
layouts: "./docs/layouts/" # default: "layouts"
Next steps
- Layouts – layout chaining, resolution order, partials
- Render Hooks – customize Markdown element rendering
- Filters – built-in and custom filter reference
- Shortcodes – reusable content snippets with parameters
- Output Formats – multi-format rendering (HTML, JSON, XML)