Layouts

Layouts wrap your page content in shared HTML structure. Every content page is rendered into a layout, which injects the page body via {{ content }}.

Liquid Go templates
<!-- layouts/default.liquid -->
<!DOCTYPE html>
<html lang="{{ site.language }}">
<head>
  <meta charset="utf-8">
  <title>{{ page.title }} - {{ site.title }}</title>
</head>
<body>
  {% include "partials/header" %}
  <main>{{ content }}</main>
  {% include "partials/footer" %}
</body>
</html>
<!-- layouts/default.html -->
<!DOCTYPE html>
<html lang="{{ .site.language }}">
<head>
  <meta charset="utf-8">
  <title>{{ .page.title }} - {{ .site.title }}</title>
</head>
<body>
  <main>{{ .content }}</main>
</body>
</html>

The {{ content }} variable holds the fully rendered body of the current page. For Markdown files, this is already converted to HTML before the layout is applied.

Layout resolution order

Alloy resolves layouts through a predictable lookup chain. The configured engine determines the extension checked at each step: the Liquid engine tries .liquid first, then falls back to the bare extension (e.g., .html); the Go template engine checks .html only.

Blog-like sections

Sections with date-based permalink patterns (containing :year, :month, or :day tokens in _data.yaml) use special resolution.

Index file (content/blog/index.md):

  1. layout: from front matter or _data.yaml cascade
  2. layouts/blog.liquidlayouts/blog.html (section name)
  3. layouts/default.liquidlayouts/default.html (fallback)
  4. Build error

Child file (content/blog/my-post.md):

  1. layout: from front matter or _data.yaml cascade
  2. layouts/post.liquidlayouts/post.html (child of date-based section)
  3. layouts/default.liquidlayouts/default.html (fallback)
  4. Build error

Regular sections and standalone pages

Pages in sections without date-based permalinks resolve through a shorter chain.

Any file (content/docs/getting-started.md):

  1. layout: from front matter or _data.yaml cascade
  2. layouts/default.liquidlayouts/default.html (fallback)
  3. Build error

Layouts do not match by filename. If you relied on layouts/getting-started.liquid being picked up automatically, add layout: "getting-started" to the page’s front matter or _data.yaml.

Taxonomy pages

Auto-generated taxonomy pages check their own path first:

  1. layouts/taxonomies/<name>.liquidlayouts/taxonomies/<name>.html (e.g., layouts/taxonomies/tags.liquid)
  2. layouts/<name>.liquidlayouts/<name>.html

Specifying a layout

Set the layout explicitly in front matter:

---
title: &#34;About Us&#34;
layout: &#34;page&#34;
---

Or apply a layout to an entire directory via _data.yaml:

# content/docs/_data.yaml
layout: &#34;doc&#34;

All pages in content/docs/ and subdirectories inherit this layout unless they override it in their own front matter. Front matter always takes priority over the cascade.

Extension-bearing layout names

A layout name that includes a recognized extension (.liquid, .html, .xml, .json, .txt) is used as a literal filename – no engine extension is appended:

---
layout: &#34;base.html&#34;   # resolves to layouts/base.html exactly
---

Bare names (without an extension) get the engine-specific lookup: baselayouts/base.liquidlayouts/base.html for the Liquid engine.

Extension-bearing layout names cannot be used with output formatslayout: "article.liquid" with outputs: ["html", "json"] produces a build error with a fix-it message suggesting layout: "article" instead.

Disabling layout wrapping

Set layout: false to output the page body without any layout wrapper:

---
title: &#34;Raw HTML Page&#34;
layout: false
---

This is useful for pages that are complete HTML documents on their own, or for data-only pages consumed by other templates.

Layout chaining

Layouts can reference a parent layout via a layout: directive in their front matter. The pipeline renders inside-out: page content flows into the innermost layout, which flows into the parent, and so on up the chain.

<!-- layouts/has-toc.liquid -->
---
layout: &#34;base&#34;
---
<div class=&#34;with-toc&#34;>
  <aside>{% include &#34;partials/toc&#34; %}</aside>
  <main>{{ content }}</main>
</div>
<!-- layouts/base.liquid -->
<!DOCTYPE html>
<html>
<head><title>{{ page.title }}</title></head>
<body>
  {% include &#34;partials/header&#34; %}
  {{ content }}
  {% include &#34;partials/footer&#34; %}
</body>
</html>

A page using layout: "has-toc" renders as: page body -> has-toc -> base. Each level injects {{ content }} from the level below.

Layout front matter is stripped before rendering – only the layout: directive is used. Other front matter keys in layout files are ignored.

Circular layout detection

Alloy scans all layout files at build start and fails the build if a cycle exists (e.g., a -> b -> a). Layout chains are capped at 10 levels to prevent infinite loops from malformed configurations.

Accessing page data

All front matter fields are available inside layouts via the page object:

Liquid Go templates
<article class="post">
  <h1>{{ page.title }}</h1>
  <time datetime="{{ page.date | date: '%Y-%m-%d' }}">
    {{ page.date | date: "%B %d, %Y" }}
  </time>
  {% if page.summary %}
    <p class="summary">{{ page.summary }}</p>
  {% endif %}
  {{ content }}
  {% if page.tags %}
    <ul class="tags">
      {% for tag in page.tags %}
        <li><a href="/tags/{{ tag | slugify }}/">{{ tag }}</a></li>
      {% endfor %}
    </ul>
  {% endif %}
</article>
<article class="post">
  <h1>{{ .page.title }}</h1>
  <time datetime="{{ date .page.date "%Y-%m-%d" }}">
    {{ date .page.date "%B %d, %Y" }}
  </time>
  {{ if .page.summary }}
    <p class="summary">{{ .page.summary }}</p>
  {{ end }}
  {{ .content }}
  {{ if .page.tags }}
    <ul class="tags">
      {{ range .page.tags }}
        <li><a href="/tags/{{ slugify . }}/">{{ . }}</a></li>
      {{ end }}
    </ul>
  {{ end }}
</article>

Custom front matter fields work the same way. If your content defines author: "Alice" in front matter, the layout accesses it as {{ page.author }}.

Partials and includes

Partials are reusable template fragments stored in layouts/partials/ (by convention – any path under layouts/ works). Both engines resolve partials from the layouts directory.

Liquid Go templates
{% include "partials/header" %}
{% include "partials/footer" %}
{% render "partials/social-links" %}
Both tags resolve paths relative to the `layouts/` directory, trying `name.liquid`, then `name.html`, then the bare name. The difference: `{% include %}` shares the parent template's variable scope, while `{% render %}` creates an isolated scope (variables from the parent are not accessible unless explicitly passed).
{{ include "partials/header" }}
{{ include "partials/footer" }}
{{ include "partials/social-links" }}
The `include` function resolves paths relative to the `layouts/` directory, trying `name.html`. Context is optional -- with no argument, the include inherits the current template context (like Liquid's `{% include %}`). Pass an explicit context to narrow scope:
{{ include "partials/card" (dict "item" . "compact" true) }}
Unlike Go's built-in `{{ template }}` action, `include` is a function -- its output can be captured in variables and used in pipelines:
{{ $nav := include "partials/nav" }}
{{ if $nav }}<div class="has-nav">{{ $nav }}</div>{{ end }}

Plugin-registered filters work inside partials in both engines – the same filter dispatch mechanism applies to all template files. Partials can include other partials (nesting is capped at 100 levels).

Go template engine

With the Go template engine (engine: "gotemplate" or "go"), layouts are .html files using Go syntax. The same context is available with a leading dot: {{ .page.title }}, {{ .site.title }}, {{ .content }}:

<!-- layouts/default.html -->
<!DOCTYPE html>
<html>
<head><title>{{ .page.title }} - {{ .site.title }}</title></head>
<body>
  {{ .content }}
</body>
</html>

Layout chaining works identically in both engines – a layout: directive in the layout’s front matter names the parent (see Layout chaining). Cross-file includes use the {{ include }} function (see Partials and includes). {% render %} and {% inline %} are Liquid-only tags.

Two helper functions are registered for working with ordered map data (JSON data files preserve key order via an ordered map type that Go’s index syntax cannot address):

{{ oget .site.data.config &#34;title&#34; }}          <!-- ordered-map lookup -->
{{ range orange .site.data.nav }}              <!-- ordered-map iteration -->
  <a href=&#34;{{ oget .Value &#34;url&#34; }}&#34;>{{ .Key }}</a>
{{ end }}

Content-relative file inlining

The {% inline %} tag reads a file relative to the current content file and inserts its raw contents. No template processing occurs – the file is inserted verbatim.

<!-- content/about/index.md -->
# About

{% inline &#34;./about-diagram.svg&#34; %}

This is useful for SVGs that need to respond to CSS custom properties and cannot be loaded as <img> tags.

Rules:

  • Paths must start with ./ or ../ – bare paths like {% inline "diagram.svg" %} are rejected
  • The resolved path must stay within the content root directory
  • Only text file types are accepted: .svg, .html, .htm, .txt, .css, .js, .json, .xml, .toml, .yaml, .yml, .md. Binary types like .png produce a build error suggesting <img> instead
  • {% inline %} is a Liquid tag; it is not available in the Go template engine

Symlinks inside the layouts/ directory are followed. A symlink at layouts/partials/shared.liquid pointing to a file outside the project root is valid – {% include %} and {{ include }} read the symlink target. The path traversal check applies to the logical path within the layouts tree, not the symlink’s physical target.

Table of contents

Alloy extracts heading structure from Markdown pages and exposes it as page.toc. Build a TOC partial to render navigation:

<!-- layouts/partials/toc.liquid -->
<nav class=&#34;toc&#34;>
  {% for item in page.toc %}
    <a href=&#34;#{{ item.id }}&#34;>{{ item.text }}</a>
    {% if item.children.size > 0 %}
      <ul>
        {% for child in item.children %}
          <li><a href=&#34;#{{ child.id }}&#34;>{{ child.text }}</a></li>
        {% endfor %}
      </ul>
    {% endif %}
  {% endfor %}
</nav>

Each TOC entry has id (heading anchor), text (plain text), level (2-6), and children (nested headings). TOC extraction is controlled by content.markdown.toc (default: true). Heading anchor IDs are controlled separately by content.markdown.goldmark.autoHeadingID – disabling TOC does not disable heading IDs (see Content).