schemaflux uses Go’s standard text/template package for HTML rendering, extended with a set of built-in functions. Template files live in the directory configured at templates.dir (default: ./templates).
Each file in the templates directory is a named template. A file at templates/post.html is referenced as layout: post in frontmatter. A file at templates/tag-list.html is referenced as layout: tag-list. The extension is configured at backends.html.templates.extension (default: .html).
Every template receives the current entity as the root context (.). The entity struct and all its fields are directly accessible.
| Variable | Type | Description |
|---|---|---|
.Title |
string |
Entity title |
.Slug |
string |
URL-safe slug |
.URL |
string |
Canonical URL path |
.Description |
string |
Short description |
.Content |
template.HTML |
Rendered HTML body (safe to render with {{ .Content }}) |
.Date |
time.Time |
Publication date |
.UpdatedAt |
time.Time |
Last updated date |
.Weight |
int |
Sort weight |
.Draft |
bool |
Draft status |
.Tags |
[]Tag |
Tags; each has .Name (string) and .Slug (string) |
.Categories |
[]Category |
Categories; same structure as Tags |
.Series |
string |
Series name |
.Section |
string |
First path component |
.Depth |
int |
URL depth (number of path segments) |
.Related |
[]*Entity |
Top-N related entities (computed) |
.Backlinks |
[]*Entity |
Entities that link to this one (computed) |
.Parent |
*Entity |
Nearest URL parent (may be nil) |
.Children |
[]*Entity |
URL child entities |
.Prev |
*Entity |
Previous entity in section sort order (may be nil) |
.Next |
*Entity |
Next entity in section sort order (may be nil) |
.Frontmatter |
map[string]any |
All frontmatter key-value pairs |
.Graph |
*Graph |
Full entity graph |
.Site |
*SiteConfig |
Site-level configuration |
| Variable | Type | Description |
|---|---|---|
.Graph.Entities |
[]*Entity |
All non-draft entities in sort order |
.Graph.BySection |
map[string][]*Entity |
Entities grouped by section name |
.Graph.ByTag |
map[string][]*Entity |
Entities grouped by tag slug |
.Graph.Nav |
[]*Entity |
Top-level navigation: root-depth entities sorted by weight |
.Graph.Tags |
[]*Entity |
All taxonomy tag entities |
.Graph.Sections |
[]string |
All unique section names |
.Graph.TotalCount |
int |
Total entity count |
| Variable | Type | Description |
|---|---|---|
.Site.Title |
string |
Site title from config |
.Site.URL |
string |
Site base URL from config |
.Site.Description |
string |
Site description from config |
.Site.Author |
string |
Site author from config |
Taxonomy pages (tag pages, category pages) and paginated list pages have these additional variables:
| Variable | Type | Description |
|---|---|---|
.Children |
[]*Entity |
All entities in this taxonomy term or page |
.Pagination.Page |
int |
Current page number (1-based) |
.Pagination.TotalPages |
int |
Total pages |
.Pagination.Prev |
string |
URL of previous page |
.Pagination.Next |
string |
URL of next page |
.Pagination.Items |
[]*Entity |
Entities on this page (same as .Children when not paginating) |
schemaflux extends Go’s template functions with the following:
{{ .Date | dateFormat "January 2, 2006" }}
{{ .Date | dateFormat "2006-01-02" }}
{{ .Date | iso8601 }} {{/* outputs: 2026-03-15T00:00:00Z */}}
{{ .Date | humanDate }} {{/* outputs: March 15, 2026 */}}
{{ .Date | relativeDate }} {{/* outputs: 2 days ago */}}
{{ .Title | lower }} {{/* lowercase */}}
{{ .Title | upper }} {{/* uppercase */}}
{{ .Title | title }} {{/* title case */}}
{{ .Title | truncate 60 }} {{/* truncate to 60 chars with ellipsis */}}
{{ .Slug | slugify }} {{/* re-slugify a string */}}
{{ "hello world" | replace " " "-" }}
{{ .Content | stripHTML }} {{/* strip HTML tags, return plain text */}}
{{ .Content | excerpt 160 }} {{/* first 160 chars of stripped content */}}
{{ .RawContent | markdownify }} {{/* render markdown string to HTML */}}
{{ .Graph.Entities | limit 5 }} {{/* first 5 entities */}}
{{ .Graph.Entities | offset 10 }} {{/* skip first 10 */}}
{{ .Graph.Entities | where "Section" "blog" }} {{/* filter by field value */}}
{{ .Graph.Entities | sortBy "Date" "desc" }} {{/* sort by field */}}
{{ .Tags | pluck "Name" }} {{/* extract field from list */}}
{{ .Graph.BySection | keys }} {{/* keys of a map */}}
{{ .URL | absURL }} {{/* prepend site.url */}}
{{ "/path/" | absURL }} {{/* https://example.com/path/ */}}
{{ .URL | relURL }} {{/* ensure relative URL */}}
{{ .Date | isZero }} {{/* bool: true if zero value */}}
{{ .Description | default "No description available." }}
{{ .Layout | default "base" }}
{{ add 1 2 }} {{/* 3 */}}
{{ sub .Pagination.TotalPages 1 }}
{{ mul .Weight 10 }}
{{ div 100 .Pagination.PageSize }}
{{ mod .Pagination.Page 2 }}
Go templates support {{ define }} and {{ template }} for composition. The recommended pattern:
templates/partials/head.html
{{ define "head" }}
<head>
<meta charset="UTF-8">
<title>{{ .Title }} — {{ .Site.Title }}</title>
{{ if .Description }}<meta name="description" content="{{ .Description }}">{{ end }}
<link rel="canonical" href="{{ .Site.URL }}{{ .URL }}">
</head>
{{ end }}
templates/partials/nav.html
{{ define "nav" }}
<nav>
{{ range .Graph.Nav }}
<a href="{{ .URL }}"{{ if eq .URL $.URL }} aria-current="page"{{ end }}>{{ .Title }}</a>
{{ end }}
</nav>
{{ end }}
templates/base.html
<!DOCTYPE html>
<html lang="en">
{{ template "head" . }}
<body>
{{ template "nav" . }}
<main>
<h1>{{ .Title }}</h1>
{{ .Content }}
</main>
</body>
</html>
templates/post.html
<!DOCTYPE html>
<html lang="en">
{{ template "head" . }}
<body>
{{ template "nav" . }}
<main>
<article>
<h1>{{ .Title }}</h1>
<time datetime="{{ .Date | iso8601 }}">{{ .Date | humanDate }}</time>
{{ if .Tags }}
<ul class="tags">
{{ range .Tags }}
<li><a href="/tags/{{ .Slug }}/">{{ .Name }}</a></li>
{{ end }}
</ul>
{{ end }}
{{ .Content }}
</article>
{{ if .Related }}
<aside>
<h2>Related</h2>
{{ range .Related }}
<a href="{{ .URL }}">{{ .Title }}</a>
{{ end }}
</aside>
{{ end }}
</main>
</body>
</html>
schemaflux loads all template files from the templates directory at build start, making all {{ define }} blocks available to all templates without explicit import.
Templates can traverse the full entity graph to build navigation, tag clouds, related content sections, and more:
<!-- Site-wide navigation from all root-level entities -->
<nav>
{{ range .Graph.Nav }}
<a href="{{ .URL }}">{{ .Title }}</a>
{{ end }}
</nav>
<!-- All posts in the blog section, sorted by date -->
{{ range .Graph.BySection.blog | sortBy "Date" "desc" }}
<article>
<a href="{{ .URL }}">{{ .Title }}</a>
<time>{{ .Date | humanDate }}</time>
</article>
{{ end }}
<!-- Tag cloud with post counts -->
{{ range .Graph.Tags }}
<a href="{{ .URL }}" style="font-size: {{ len .Children | mul 0.1 | add 0.9 }}em">
{{ .Title }} ({{ len .Children }})
</a>
{{ end }}
<!-- Most recent 5 posts (any section) -->
{{ .Graph.Entities | where "Section" "blog" | sortBy "Date" "desc" | limit 5 }}
<!-- Breadcrumb navigation using Parent chain -->
{{ $e := . }}
{{ range $e | ancestors }}
<a href="{{ .URL }}">{{ .Title }}</a> /
{{ end }}
Taxonomy pages (tag pages, category pages) have .Title set to the term name and .Children set to all matching entities:
<!-- templates/tag-list.html -->
<h1>Posts tagged "{{ .Title }}"</h1>
<p>{{ len .Children }} posts</p>
{{ range .Children | sortBy "Date" "desc" }}
<article>
<a href="{{ .URL }}">{{ .Title }}</a>
<time>{{ .Date | humanDate }}</time>
{{ if .Description }}<p>{{ .Description }}</p>{{ end }}
</article>
{{ end }}
Custom fields from frontmatter are available via .Frontmatter:
{{ $author := index .Frontmatter "author" }}
{{ if $author }}<p>By {{ $author }}</p>{{ end }}
{{ $difficulty := index .Frontmatter "difficulty" }}
{{ if $difficulty }}<span class="badge {{ $difficulty }}">{{ $difficulty }}</span>{{ end }}
{{ if .Parent }}<a href="{{ .Parent.URL }}">← Back to {{ .Parent.Title }}</a>{{ end }}
{{ if .Backlinks }}
<aside>
<h3>Referenced by {{ len .Backlinks }} pages</h3>
{{ range .Backlinks }}<a href="{{ .URL }}">{{ .Title }}</a>{{ end }}
</aside>
{{ end }}
{{ if not .Date.IsZero }}<time>{{ .Date | humanDate }}</time>{{ end }}