Layouts wrap your page content in a reusable template (header, navigation, footer, etc.). Includes are reusable template fragments you can insert from any page or layout.
Both are evaluated at render time, not during config.scriban loading.
Lunet has two distinct contexts where Scriban code runs. Understanding the difference is critical:
| Context | When it runs | Main purpose | include allowed? |
Key objects |
|---|---|---|---|---|
config.scriban |
Before content is loaded | Configure site and modules | No | Site (as context), module objects |
| Page/layout templates | During content processing | Render HTML/XML output | Yes (from /.lunet/includes/) |
site, page, content |
In config, you set up defaults:
layout = "_default"
In templates, you generate output:
{{ include "partials/nav.sbn-html" }}
<main>{{ content }}</main>
Layout files are stored under /.lunet/layouts/ in the site's meta filesystem. This folder can come from:
<site>/.lunet/layouts/.lunet/layouts/Your local files always take priority (see Site structure for the layered filesystem).
When Lunet renders a page, it looks for a matching layout using three pieces of information:
page.layout — the layout name (defaults to page.section, i.e. the first directory segment of the file path)page.layout_type — the type of rendering (single by default)page.content_type — the output format (html, xml, rss, etc.)Layout names cannot contain \, /, or . characters. If present, they are replaced with - and a warning is logged. For example, layout: "my.custom" in front matter becomes my-custom.
If the layout name is empty or null, Lunet falls back to site.layout (if set) or _default.
Suppose you have a file docs/intro.md:
page.section = "docs" (first directory segment).page.layout defaults to "docs" (same as section).page.layout_type defaults to "single".markdown, gets converted to html.(docs, single, html).The search tries these paths under /.lunet/layouts/, in order:
docs/single.sbn-html ← section-specific single layout
docs.single.sbn-html
docs.sbn-html ← section-specific (any type)
_default/single.sbn-html ← fallback default single layout
_default.sbn-html ← fallback default (any type)
If site.layout is set (e.g. site.layout = "mybase"), it is tried between the section-specific and _default layouts.
The first matching file wins. All registered extensions for the content type are tried (e.g. .sbn-html, .scriban-html, .html, .htm).
The search order differs between single and all other layout types:
| # | Path pattern | Note |
|---|---|---|
| 1 | {layout}/{type} |
e.g. docs/single |
| 2 | {layout}.{type} |
e.g. docs.single |
| 3 | {layout} |
e.g. docs — single only (bare name without type) |
| 4 | {site.layout}/{type} |
only if site.layout is set and differs from layout |
| 5 | {site.layout}.{type} |
same condition |
| 6 | {site.layout} |
single only — bare name |
| 7 | _default/{type} |
only if layout ≠ _default |
| 8 | _default |
single only — bare _default |
| # | Path pattern | Note |
|---|---|---|
| 1 | {layout}/{type} |
e.g. docs/list, tags/term |
| 2 | {layout}.{type} |
e.g. docs.list, tags.term |
| 3 | {site.layout}/{type} |
only if site.layout is set and differs from layout |
| 4 | {site.layout}.{type} |
same condition |
| 5 | _default/{type} |
only if layout ≠ _default |
| 6 | _default.{type} |
only if layout ≠ _default |
The key difference: single tries the bare layout name without the type suffix (paths 3, 6, 8), so docs.sbn-html matches single pages in the docs section. Non-single types always require the type in the path.
For each candidate path, all registered file extensions for the content type are tried. For html, this includes: .htm, .html, .scriban-htm, .scriban-html, .sbn-htm, .sbn-html.
/.lunet/layouts/
_default.sbn-html ← catches all single pages with no specific layout
docs.sbn-html ← layout for all pages in the docs/ section
docs.single.sbn-html ← explicit single layout for docs
docs.list.sbn-html ← list layout for docs (used by taxonomy pages, etc.)
_default.rss.xml ← RSS feed layout
tags.term.sbn-html ← layout for individual tag pages
tags.terms.sbn-html ← layout for the tag list page
content variableInside a layout template, the content variable holds the rendered body of the page (or the output of a previous layout in a chain):
<!DOCTYPE html>
<html>
<head><title>{{ page.title }}</title></head>
<body>
{{ include "partials/header.sbn-html" }}
<main>{{ content }}</main>
{{ include "partials/footer.sbn-html" }}
</body>
</html>
A layout can specify its own layout, layout_type, or layout_content_type in its front matter. Lunet will then wrap the result in another layout.
At each step, the layout's output becomes the new content variable passed to the next layout.
A typical theme uses layout chaining to separate concerns:
Page body
↓ wrapped by
"default" layout (adds sidebar menu + TOC + content area)
↓ wrapped by
"base" layout (adds navbar + footer + Prism setup)
↓ wrapped by
"_default" layout (adds <!DOCTYPE>, <html>, <head>, <body>)
/.lunet/layouts/_default.sbn-html — the outermost shell:
<!DOCTYPE html>
<html {{ site.html.attributes }}>
<head>
{{ include "_builtins/head.sbn-html" }}
</head>
<body {{ site.html.body.attributes }}>
{{ content }}
{{ include "_builtins/bundle.sbn-html" }}
</body>
</html>
/.lunet/layouts/base.sbn-html — adds navbar, footer, sets layout: _default:
---
layout: _default
---
<div class="container">
<nav>...</nav>
<section>{{ content }}</section>
<footer>...</footer>
</div>
/.lunet/layouts/default.sbn-html — adds sidebar menu and TOC, sets layout: base:
---
layout: base
---
<div class="row">
<nav>{{ page.menu.render { kind: "menu", collapsible: true } }}</nav>
<div>{{ content }}</div>
<aside class="js-toc"></aside>
</div>
When a page uses layout: default (or defaults to it via site.layout = "default" in config), the chain runs:
default layout wraps it with sidebar/TOC structure.base layout wraps that with navbar/footer._default layout wraps everything with the HTML document shell.Each step passes its output as the content variable to the next layout.
A page can bypass the sidebar layout by using layout: simple:
---
title: Home
layout: simple
---
If simple.sbn-html sets layout: base in its front matter, it skips the sidebar step but still gets navbar/footer and the HTML shell.
Lunet detects infinite loops (same layout/type/content-type tuple visited twice) and stops with an error.
For Markdown pages, Lunet first converts the content from Markdown to HTML, then searches for an HTML layout:
content_type = markdown.content_type = html → finds an HTML layout.This means your layout files should be .sbn-html (not .sbn-md) even when wrapping Markdown pages.
single is the default rendering mode for every page (weight 0 — processed first).list is used for index/collection pages (weight 10 — processed after single). In list mode, Lunet injects pages = site.pages into the template scope.rss, sitemap, term, terms) also have weight 10 and are processed after single pages, but they do not inject a pages variable — each module injects its own data.Modules can register custom layout types. Currently registered types:
| Type | Weight | Module | Description |
|---|---|---|---|
single |
0 | Built-in | Default for all pages |
list |
10 | Built-in | Index/collection pages |
rss |
10 | RSS | RSS feed generation |
sitemap |
10 | Sitemaps | Sitemap generation |
term |
10 | Taxonomies | Individual term pages (e.g. a specific tag) |
terms |
10 | Taxonomies | Term list pages (e.g. all tags) |
Lunet and its modules ship default layouts:
| Layout file | Module | Description |
|---|---|---|
_default.sbn-html |
Layouts | HTML document shell (<!DOCTYPE>, <html>, <head>, <body>) |
_default.rss.xml |
RSS | Default RSS 2.0 feed |
_default.sitemap.xml |
Sitemaps | Default sitemap XML |
_default.api-dotnet*.sbn-md |
API .NET | API documentation pages |
You can override any built-in layout by creating the same path under your site's /.lunet/layouts/.
Include templates live under /.lunet/includes/ in the meta filesystem. Like layouts, they can come from your site, a theme, or Lunet's built-in shared files.
{{ include "partials/nav.sbn-html" }}
{{ include "_builtins/head.sbn-html" }}
Includes resolve paths relative to /.lunet/includes/. You cannot use:
/ or \..)Include files are cached during a build — each file is read once and reused across all pages.
Includes work in page/layout templates and in front matter (both --- and +++ blocks). They do not work in config.scriban.
Lunet and its modules ship several built-in includes under _builtins/:
| Include | Module | Description |
|---|---|---|
_builtins/head.sbn-html |
Core | Standard <head> content (title, metas, head includes) |
_builtins/bundle.sbn-html |
Bundles | CSS/JS bundle injection |
_builtins/cards.sbn-html |
Cards | OpenGraph/Twitter meta tags |
_builtins/google-analytics.sbn-html |
Tracking | Google Analytics snippet |
_builtins/livereload.sbn-html |
Server | Live reload WebSocket script |
Themes typically include these in their base layout. You can override any built-in by creating the same path under your site's /.lunet/includes/.
| Variable | Type | Availability | Description |
|---|---|---|---|
site |
SiteObject | All templates and front matter | Global site state and module configuration |
page |
ContentObject | Page and layout rendering | Current page metadata and content |
content |
string | Layout rendering only | The inner content being wrapped |
pages |
PageCollection | list layouts only |
Shortcut to site.pages (only for layout_type = "list") |
page.* fieldsSee Content & front matter for the full page variable reference.
site.* fields| Field | Type | Description |
|---|---|---|
site.baseurl |
string | Canonical host URL |
site.basepath |
string | URL prefix for sub-path hosting |
site.environment |
string | Build environment (dev or prod) |
site.layout |
string | Default layout fallback |
site.pages |
PageCollection | All loaded pages |
site.data |
object | Data loaded from /.lunet/data/ (Data modules) |
site.html |
object | HTML head/body configuration (see Configuration) |
site.builtins |
object | Built-in helper functions |
site.pages helpers| Helper | Result |
|---|---|
site.pages.by_weight |
Pages ordered by weight, then date |
site.pages.by_date |
Pages ordered by date |
site.pages.by_length |
Pages ordered by source length |
site.pages.by_title |
Pages ordered by title |
site.builtins.* (commonly used)| Helper | Description |
|---|---|
site.builtins.log.info/warn/error |
Emit log messages from templates |
site.builtins.lunet.version |
Current Lunet version string |
site.builtins.defer(expr) |
Defer expression evaluation to end of processing |
site.builtins.ref(url) |
Resolve an absolute URL using site routing |
site.builtins.relref(url) |
Resolve a relative URL from the current page |
site.builtins.xref(uid) |
Resolve a UID to an object with url, name, fullname, page |
site.html.*| Field | Type | Description |
|---|---|---|
site.html.attributes |
string | Attributes injected on <html> |
site.html.head.title |
string/template | Custom <title> override (supports deferred do/ret) |
site.html.head.metas |
collection | <meta> entries rendered in <head> |
site.html.head.includes |
collection | Include templates rendered in <head> |
site.html.body.attributes |
string | Attributes injected on <body> |
site.html.body.includes |
collection | Include templates rendered at end of <body> |
config.scriban) — site.html configuration, site.layout fallbackpage.layout, page.layout_type, page.content_type