extend)Extensions (themes) are Lunet's way to layer reusable site templates, layouts, and assets on top of your site without modifying your content.
An extension is typically a GitHub repository containing:
dist/ folder with the theme contentdist/config.scriban that runs when the extension is loadeddist/.lunet/ folder for layouts, includes, and data shipped by the thememain)extend "owner/repo"
Downloads from the default main branch. Extensions downloaded without a tag are re-downloaded once per build session to pick up latest changes.
extend "owner/repo@1.0.0"
Tagged extensions are cached locally and not re-downloaded unless the cache is cleaned.
extend "https://github.com/owner/repo@1.0.0"
Full URLs work too. A trailing .git suffix is automatically stripped.
extend {
repo: "owner/repo",
tag: "1.0.0",
directory: "dist",
public: true
}
| Property | Aliases | Default | Description |
|---|---|---|---|
repo |
url |
(required) | GitHub owner/repo or full URL |
tag |
version |
null (latest main) |
Tag or branch name |
directory |
— | "dist" |
Subfolder within the repository to extract |
public |
— | false |
When true, install to .lunet/extends/ (version-controllable). When false (default), install to build cache. |
name |
— | (derived from repo) | Display name for the extension |
public parameterBy default (public: false), extensions are installed to the build cache at .lunet/build/cache/.lunet/extends/. This keeps your site repository clean — cached extensions are not tracked by version control.
When public: true, extensions are installed to .lunet/extends/ within your site directory, so they are tracked by version control. This is useful when you want your site to be fully self-contained without network access.
When using the string syntax (extend "owner/repo"), extensions are always installed to the build cache (private). Use the object syntax to control installation location.
The extend function returns an ExtendObject (or null on failure). You can capture it:
myext = extend "owner/repo@1.0.0"
# myext.name, myext.version, myext.url are available
When you call extend, Lunet downloads (and caches) the extension, then adds its files as a content filesystem layer below your site:
┌───────────────────────────────────────┐
│ Your site files │ ← highest priority
├───────────────────────────────────────┤
│ Extension files (dist/) │ ← from extend "..."
├───────────────────────────────────────┤
│ Lunet built-in shared files │ ← lowest priority
└───────────────────────────────────────┘
Everything under dist/ in the extension becomes available as if it were part of your site, but at a lower priority. This means your files always win when both layers have a file at the same path.
The extension's dist/ folder typically includes:
dist/
config.scriban ← extension configuration (runs during your config)
readme.md ← optional default home page
css/
theme.css ← theme styles
.lunet/
layouts/
_default.sbn-html ← default layout
docs.sbn-html ← section-specific layout
includes/
partials/
header.sbn-html ← reusable template fragments
footer.sbn-html
data/
theme.yml ← theme data
When your config.scriban calls extend "owner/repo":
config.scriban (at the root of the extracted dist/ folder) is imported immediately — it runs in the current site context with full site function access.config.scriban and continues with the next line.This means:
layout = "base").extend call.Your local files always take priority. Common override patterns:
The extension provides dist/.lunet/layouts/_default.sbn-html. To customize it, create the same path in your site:
<your-site>/.lunet/layouts/_default.sbn-html
Your version will be used instead of the extension's.
The extension provides dist/.lunet/includes/partials/header.sbn-html. Override it:
<your-site>/.lunet/includes/partials/header.sbn-html
The extension provides dist/css/theme.css. Override it:
<your-site>/css/theme.css
You never need to modify extension files. Just create the same path in your site. This makes theme updates safe — your overrides are preserved when you update the extension tag.
To iterate on a theme without publishing to GitHub, put it under your site's .lunet/extends/ folder:
<your-site>/.lunet/extends/mytheme/
config.scriban
.lunet/
layouts/
_default.sbn-html
Then load it by name:
extend "mytheme"
When the name doesn't contain a /, Lunet looks for it in /.lunet/extends/<name>/ (checking both the cache and site directories).
You can call extend multiple times. Each extension adds a new filesystem layer. Later extensions have higher priority than earlier ones (but all are below your site files):
extend "base-theme/base@1.0.0" # lowest extension priority
extend "custom-theme/custom@2.0.0" # higher priority than base
If you call extend with the same extension (same name/tag/directory) twice, the duplicate is silently reused without re-downloading.
The extends builtin (note the plural s) provides a read-only list of all loaded extensions:
{{ for ext in extends }}
{{ ext.name }} - {{ ext.version }}
{{ end }}
Each ExtendObject exposes:
| Property | Type | Description |
|---|---|---|
name |
string | Extension display name |
version |
string | Tag/version (null if using latest main) |
url |
string | GitHub URL (null for local extends) |
| Scenario | Cache location |
|---|---|
| Private (default) | .lunet/build/cache/.lunet/extends/github/<owner>/<repo>/<tag>/<directory>/ |
| Public | .lunet/extends/github/<owner>/<repo>/<tag>/<directory>/ |
| Local | .lunet/extends/<name>/ |
Run lunet clean to clear all cached extensions and force a re-download.
config.scriban) — how extension config integrates with site config