Custom Layout Cookbook
TL;DR. A custom layout is one Jet template file at _layouts/<name>.html, assigned per note with layout: <name> frontmatter. Inside it you get the current note (note), every note in the vault (nvs), the standard site chrome (defaultTemplate), and AST-level access to the note's sections. Live examples: layout-sections, layout-include, layout-magazine.
Setup
- Put a Jet template in your vault:
_layouts/showcase/sections.html - Point a note at it:
---
layout: showcase/sections
---
Auto-escaping is off: layouts are trusted vault content, so {{ note.HTMLString() }} outputs raw HTML directly.
The current note: note.*
Methods need parentheses.
| Snippet | Returns |
|---|---|
{{ note.Title() }} |
note title |
{{ note.HTMLString() }} |
full rendered HTML body |
{{ note.ContentString() }} |
raw markdown source |
{{ note.Path() }} / {{ note.Permalink() }} |
vault path / URL path |
{{ note.Description() }} |
SEO description |
{{ note.Author() }}, {{ note.Tags() }}, {{ note.CreatedAt() }}, {{ note.UpdatedAt() }} |
frontmatter meta |
{{ note.ReadingTime() }} |
minutes, estimated |
{{ note.TOC() }} |
table-of-contents headings |
{{ note.M().GetString("key", "default") }} |
any frontmatter field (also GetInt, GetBool, GetStrings, Raw()) |
{{ note.Lang() }} |
note language (also LangName(), LangAlternative("ru")) |
AST sections: note.PartialRenderer()
Splits the note at headings — build your own markup around each piece.
{{ range i, sec := note.PartialRenderer().Sections(2) }}
<section class="card">
<h2>{{ sec.TitleHTML }}</h2>
{{ sec.ContentHTML }}
</section>
{{ end }}
| Snippet | Returns |
|---|---|
.Sections(2) |
list of sections split at H2; each has .Title, .TitleHTML, .ContentHTML |
sec.Sections(3) |
nested split — subsections inside a section |
.Section("Pricing") |
one section found by heading title (nil if absent) |
.Introduce() |
everything before the first heading (the lead) |
.FirstList() / .Lists() |
markdown lists as structured items (.Text, .URL, .Children) |
.FirstImageURL() |
URL of the note's first image |
Other notes: nvs.*
Pull any note in the vault into the page.
{{ pulled := nvs.ByWikilink("_snippet-cta") }}
{{ if pulled }}{{ pulled.HTMLString() }}{{ end }}
| Snippet | Returns |
|---|---|
nvs.ByPath("/_snippet-cta.md") |
note by exact vault path |
nvs.ByWikilink("target") |
Obsidian-style resolution (basename lookup) |
nvs.ByPermalink("/about") |
note by URL |
nvs.BackLinks(note) / nvs.OutLinks(note) |
notes linking here / linked from here |
nvs.List() |
all notes |
nvs.ByGlob("blog/*.md") |
query builder: chain .SortBy("created_at") / .SortByMeta("date"), .Desc()/.Asc(), .Limit(n), .Offset(n), end with .All(), .First() or .Last() |
A recent-posts block in three lines:
{{ range i, post := nvs.ByGlob("blog/*.md").SortByMeta("date").Desc().Limit(5).All() }}
<a href="{{ post.PermalinkEncoded() }}">{{ post.Title() }}</a>
{{ end }}
Every returned note is the same view as note — call .Title(), .HTMLString(), even .PartialRenderer() on it.
Site chrome: defaultTemplate.*
Reuse the standard design instead of rebuilding it.
| Snippet | Emits |
|---|---|
{{ defaultTemplate.Styles() }} |
standard stylesheet <link>s — Pico variables, typography, dark mode |
{{ defaultTemplate.Header() }} |
the normal site header |
{{ defaultTemplate.Footer() }} |
the normal site footer |
{{ defaultTemplate.UserSpaceScripts() }} |
sign-in / search widget scripts |
{{ currentUser.IsAdmin() }} gates admin-only markup.
Layout-file plumbing
| Snippet | Does |
|---|---|
{{ include "components/header.html" }} |
inline another template file |
{{ import "blocks" }} + {{ yield my_block() }} |
reusable named blocks |
{{ asset("styles.css") }} |
URL of a file next to the layout |
{{ yield_blocks("prefix") }} |
collect CSS blocks from used components |
Gotchas
rangewith one variable gives the index, not the value.{{ range x := items }}makesxan int. Always use two variables:{{ range i, item := items }}._is not a valid range variable — use a named one even if unused:{{ range i, item := items }}.