Layouts and Partials

Layouts are the HTML frames that Braised renders each page into. A layout receives a PageContext object and is responsible for the full document structure — <html>, <head>, navigation, content area, and footer.

Full layout replacement

Place a .html file in layouts/ (site root) or {themeDir}/layouts/ (theme) to replace a built-in layout by name:

File Replaces
layouts/page.html Default documentation page
layouts/blank.html Pages with layout: blank frontmatter
layouts/reference.html Pages with layout: reference frontmatter
layouts/my-layout.html New layout, used by pages with layout: my-layout

A minimal custom layout:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>{{if .Page.Title}}{{.Page.Title}} — {{end}}{{.Site.Title}}</title>
  <link rel="stylesheet" href="/assets/braised.css">
  <script defer src="/assets/blocks.js"></script>
</head>
<body>
  <nav>
    {{if .Nav}}
    <ul>
      {{template "navTree" .Nav.Roots}}
    </ul>
    {{end}}
  </nav>
  <main>{{.Page.Content}}</main>
</body>
</html>

{{define "navTree"}}
{{range .}}
<li>
  {{if .Children}}
    <span>{{.Label}}</span>
    <ul>{{template "navTree" .Children}}</ul>
  {{else}}
    <a href="{{.URL}}"{{if .Active}} class="active"{{end}}>{{.Label}}</a>
  {{end}}
</li>
{{end}}
{{end}}

The navTree helper must be defined in your layout file (or in a partial) if you want to render sidebar navigation. It receives a []*nav.NavNode slice and recurses to produce nested <ul> elements.

Partial overrides

Partials let you replace individual sections of a layout without copying the entire file. A partial is an HTML file that contains one or more {{define "block-name"}}...{{end}} declarations.

Place partial files in layouts/partials/ (site root overrides theme) or {themeDir}/layouts/partials/ (theme overrides embedded defaults).

The built-in page.html layout defines the following overridable blocks:

Block name Renders
head <head> content (meta, title, stylesheets, scripts)
header Site header bar with title, version nav, and theme toggle
sidebar Left navigation panel
content Article body (breadcrumb, <h1>, page body div)
toc Right-hand table of contents aside
footer Site footer

Example — override just the header:

{{define "header"}}
<header class="site-header">
  <a class="site-title" href="/">{{.Site.Title}}</a>
  {{if .Config.Site.Meta}}
  <a class="cta-button" href="{{index .Config.Site.Meta "cta_url"}}">
    {{index .Config.Site.Meta "cta_label"}}
  </a>
  {{end}}
</header>
{{end}}

Save this as layouts/partials/header.html. Braised parses partials into every active layout, so the override applies to page, blank, reference, and any custom layouts.

Template data reference

Every layout and partial receives a PageContext value as .:

.Config.Site.Title          string          — site name from braised.yaml
.Config.Site.URL            string          — canonical base URL
.Config.Site.BaseURL        string          — sub-path prefix for assets (from site.base_url in braised.yaml)
.Config.Site.Meta           map[string]any  — arbitrary site-level values from braised.yaml
.Config.Themes.Values       []string        — ordered theme names from braised.yaml themes.values
.Config.Themes.Default      string          — default theme name from braised.yaml themes.default

.Page.Title       string          — frontmatter title
.Page.URL         string          — clean page URL (e.g. /guide/intro/)
.Page.Content     template.HTML   — rendered page body
.Page.Headings    []Heading       — H2/H3 headings for the TOC
.Page.Breadcrumb  []string        — nav trail labels
.Page.Meta        map[string]string — frontmatter extra keys
.Page.HideAside   bool            — true when hide_aside: true in frontmatter
.Page.LastMod     *time.Time      — git committer timestamp of the last commit touching this file; nil when git is unavailable or the file is untracked

.Nav              *nav.NavTree    — full nav tree with active page marked

site.meta values from braised.yaml land in .Config.Site.Meta as map[string]any. Access them with the index function:

<a href="{{index .Config.Site.Meta "cta_url"}}">{{index .Config.Site.Meta "cta_label"}}</a>

Custom sidebar breakpoints

The built-in .page-layout .sidebar collapses off-canvas at 1100px — the right threshold for the three-column docs grid (260px sidebar + content + 220px TOC ≈ 1100px minimum). Custom two-column layouts have more room and should collapse later.

Use your own selector and breakpoint — the .page-layout scope ensures the built-in rules do not affect your layout:

/* theme/main.css */
.my-layout .sidebar {
  position: sticky;
  top: var(--header-height);
  /* ... your sidebar rules */
}

@media (max-width: 768px) {
  .my-layout .sidebar {
    position: fixed;
    transform: translateX(-100%);
    transition: transform 0.2s ease;
  }
  .my-layout .sidebar.is-open {
    transform: translateX(0);
  }
}

768px is the standard tablet/desktop divide for two-column layouts. Adjust to suit your content.

Hamburger toggle

blocks.js (included in every built page) provides a nav-toggle handler automatically. Add three elements to your layout and the open/close behaviour works for free:

<!-- Hamburger button — shown/hidden via CSS at your breakpoint -->
<button id="nav-toggle" class="nav-toggle" aria-label="Toggle navigation" aria-expanded="false">
  <span></span><span></span><span></span>
</button>

<!-- Backdrop — blocks.js shows/hides via .is-visible -->
<div id="nav-overlay" class="nav-overlay"></div>

<!-- Sidebar — blocks.js adds/removes .is-open -->
<nav class="sidebar my-layout-sidebar">
  ...
</nav>

blocks.js listens for clicks on #nav-toggle, toggles .is-open on .sidebar, and .is-visible on #nav-overlay. Clicking the overlay closes the nav. If any of the three elements are absent, the handler skips silently — layouts that don't need it are unaffected.

Hide the toggle button via CSS at wide viewports where the sidebar is always visible:

.nav-toggle { display: none; }

@media (max-width: 768px) {
  .nav-toggle { display: flex; }
}

Content placement

{{.Page.Content}} renders everything the author wrote — including full-width blocks like card grids and wide tables. Place it in its own container, not inside a section that applies a max-width for visual styling.

Wrong — all page content is capped at the hero's 560px constraint:

<section class="hero-zone">
  <div class="hero-intro"> <!-- max-width: 560px -->
    <p class="hero-eyebrow">...</p>
    {{.Page.Content}}
  </div>
</section>

Right — chrome stays in the styled section; content gets its own sibling container:

<section class="hero-zone">
  <div class="hero-intro">
    <p class="hero-eyebrow">...</p>
  </div>
</section>
<article class="page-body">
  {{.Page.Content}}
</article>

This is the pattern used by the built-in page.html and blog.html layouts.


Last modified date

.Page.LastMod is a *time.Time populated from the git committer timestamp of the most recent commit touching the page's source file. It is nil — not an error — in these situations:

  • The project is not a git repository
  • The file exists but has not been committed yet
  • Git is not installed in the build environment

Always guard with {{if .Page.LastMod}} before using it:

{{if .Page.LastMod}}
<p class="last-updated">Last updated {{.Page.LastMod.Format "January 2, 2006"}}</p>
{{end}}

The Format method takes a Go time layout string. Common formats:

Layout string Example output
"January 2, 2006" April 19, 2026
"2006-01-02" 2026-04-19
"2 Jan 2006" 19 Apr 2026
time.RFC3339 2026-04-19T14:30:00+05:30

Because LastMod is nil for new and untracked files, builds on fresh branches or in CI environments that do shallow clones will produce pages without a last-modified date rather than failing. This is intentional — add the date display to your layout once and it appears automatically on any page that has commit history.