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.