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)
Liquid Go templates
<!-- 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
Liquid Go templates
<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:

  1. Markdown rendering – Goldmark parses .md files into HTML. Template tags ({{ }}, {% %}) are preserved through Markdown via a custom goldmark extension.
  2. Template rendering – The configured engine evaluates Liquid or Go template syntax in the rendered output and in layouts.
  3. 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:

  1. Backticks. If the tag can be code-formatted, put it in an inline code span. Always literal, most common case.

  2. {% 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{{ "}}" }}.

  3. templateTags: false for 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)