This guide walks through building a complete static site with schemaflux: content structure, layout templates, site-wide navigation, taxonomy pages, pagination, and a search index. It assumes you have completed the Quick Start.
A typical schemaflux site looks like this:
my-site/
content/
index.md # home page
about.md # about page
blog/
2026-01-post.md
2026-02-post.md
2026-03-post.md
docs/
overview.md
quick-start.md
reference.md
templates/
partials/
head.html
nav.html
footer.html
base.html
post.html
list.html
tag-list.html
static/
style.css
favicon.ico
schemaflux.yml
---
title: "My Site"
layout: base
permalink: /
weight: 0
description: "A site about things."
---
Welcome to my site.
Setting weight: 0 and permalink: / ensures this entity appears first in navigation.
---
title: "My First Post"
date: 2026-01-15
layout: post
tags:
- go
- tooling
description: "A post about building with schemaflux."
---
Post content here.
Place all blog posts in content/blog/. The section field will be automatically inferred as blog.
---
title: "Overview"
layout: docs-page
weight: 1
section: docs
description: "Introduction to the project."
---
Documentation content here.
Use weight to control page ordering within the docs section.
# schemaflux.yml
site:
title: "My Site"
url: https://example.com
description: "A site about things."
author: "Your Name"
input:
dir: ./content
output:
dir: ./_site
templates:
dir: ./templates
passes:
taxonomy:
tags:
enabled: true
urlPrefix: /tags/
layout: tag-list
minCount: 1
relationships:
topN: 5
graph:
prevNextScope: section
backends:
html:
enabled: true
templates:
default: base
pagination:
pageSize: 10
sections:
- blog
pageLayout: list
sitemap:
enabled: true
file: sitemap.xml
exclude:
- /tags/**
rss:
enabled: true
section: blog
file: feed.xml
title: "My Site — Blog"
limit: 20
search:
enabled: true
file: search.json
fields:
- title
- description
- content
- tags
- url
templates/partials/head.html
{{ define "head" }}
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ .Title }} — {{ .Site.Title }}</title>
{{ if .Description }}<meta name="description" content="{{ .Description }}">{{ end }}
<link rel="canonical" href="{{ .Site.URL }}{{ .URL }}">
<link rel="stylesheet" href="/static/style.css">
<link rel="alternate" type="application/rss+xml" title="{{ .Site.Title }}" href="/feed.xml">
</head>
{{ end }}
templates/partials/nav.html
{{ define "nav" }}
<nav class="site-nav">
<a href="/" class="site-title">{{ .Site.Title }}</a>
<ul>
{{ range .Graph.Nav }}
<li>
<a href="{{ .URL }}"{{ if eq .URL $.URL }} aria-current="page"{{ end }}>
{{ .Title }}
</a>
</li>
{{ end }}
</ul>
</nav>
{{ end }}
templates/partials/footer.html
{{ define "footer" }}
<footer>
<p>© {{ now | dateFormat "2006" }} {{ .Site.Author }}</p>
<nav>
<a href="/feed.xml">RSS</a>
<a href="/sitemap.xml">Sitemap</a>
</nav>
</footer>
{{ end }}
templates/base.html
<!DOCTYPE html>
<html lang="en">
{{ template "head" . }}
<body>
{{ template "nav" . }}
<main>
<h1>{{ .Title }}</h1>
{{ .Content }}
</main>
{{ template "footer" . }}
</body>
</html>
templates/post.html
<!DOCTYPE html>
<html lang="en">
{{ template "head" . }}
<body>
{{ template "nav" . }}
<main>
<article>
<header>
<h1>{{ .Title }}</h1>
<div class="post-meta">
<time datetime="{{ .Date | iso8601 }}">{{ .Date | humanDate }}</time>
{{ if .Tags }}
<span class="tags">
{{ range .Tags }}
<a href="/tags/{{ .Slug }}/" class="tag">{{ .Name }}</a>
{{ end }}
</span>
{{ end }}
</div>
{{ if .Description }}<p class="description">{{ .Description }}</p>{{ end }}
</header>
<div class="post-content">
{{ .Content }}
</div>
<footer class="post-footer">
<nav class="prevnext">
{{ if .Prev }}
<a href="{{ .Prev.URL }}" class="prev">← {{ .Prev.Title }}</a>
{{ end }}
{{ if .Next }}
<a href="{{ .Next.URL }}" class="next">{{ .Next.Title }} →</a>
{{ end }}
</nav>
{{ if .Related }}
<aside class="related">
<h2>Related posts</h2>
<ul>
{{ range .Related }}
<li><a href="{{ .URL }}">{{ .Title }}</a></li>
{{ end }}
</ul>
</aside>
{{ end }}
</footer>
</article>
</main>
{{ template "footer" . }}
</body>
</html>
templates/list.html
<!DOCTYPE html>
<html lang="en">
{{ template "head" . }}
<body>
{{ template "nav" . }}
<main>
<h1>Blog</h1>
<ol class="post-list">
{{ range .Pagination.Items }}
<li>
<a href="{{ .URL }}">{{ .Title }}</a>
<time>{{ .Date | humanDate }}</time>
{{ if .Description }}<p>{{ .Description }}</p>{{ end }}
</li>
{{ end }}
</ol>
{{ if gt .Pagination.TotalPages 1 }}
<nav class="pagination">
{{ if .Pagination.Prev }}
<a href="{{ .Pagination.Prev }}">← Newer</a>
{{ end }}
<span>Page {{ .Pagination.Page }} of {{ .Pagination.TotalPages }}</span>
{{ if .Pagination.Next }}
<a href="{{ .Pagination.Next }}">Older →</a>
{{ end }}
</nav>
{{ end }}
</main>
{{ template "footer" . }}
</body>
</html>
templates/tag-list.html
<!DOCTYPE html>
<html lang="en">
{{ template "head" . }}
<body>
{{ template "nav" . }}
<main>
<h1>Posts tagged "{{ .Title }}"</h1>
<p>{{ len .Children }} post{{ if ne (len .Children) 1 }}s{{ end }}</p>
<ol class="post-list">
{{ range .Children | sortBy "Date" "desc" }}
<li>
<a href="{{ .URL }}">{{ .Title }}</a>
<time>{{ .Date | humanDate }}</time>
{{ if .Description }}<p>{{ .Description }}</p>{{ end }}
</li>
{{ end }}
</ol>
</main>
{{ template "footer" . }}
</body>
</html>
The .Graph.Nav variable returns all entities at depth 1 (direct children of the root) sorted by weight. To control which pages appear in the navigation, set weight on entities you want included and leave out the layout field on index pages you don’t want to surface:
---
title: "Blog"
layout: base
permalink: /blog/
weight: 20
---
For multi-level navigation, use the .Children field recursively:
{{ define "nav-item" }}
<li>
<a href="{{ .URL }}">{{ .Title }}</a>
{{ if .Children }}
<ul>{{ range .Children }}{{ template "nav-item" . }}{{ end }}</ul>
{{ end }}
</li>
{{ end }}
<nav>
<ul>{{ range .Graph.Nav }}{{ template "nav-item" . }}{{ end }}</ul>
</nav>
schemaflux copies the static/ directory to the output directory verbatim. Reference static files with root-relative paths in templates:
<link rel="stylesheet" href="/static/style.css">
<img src="/static/logo.png" alt="Logo">
Configure the static directory in schemaflux.yml:
static:
dir: ./static
outputDir: ./static # relative to output.dir
With backends.search.enabled: true, schemaflux writes search.json to the output directory. Use a client-side library like Fuse.js to enable search:
<!-- In your base template, include Fuse.js and the search index -->
<script src="/static/fuse.min.js"></script>
<script>
fetch('/search.json')
.then(r => r.json())
.then(data => {
const fuse = new Fuse(data, {
keys: ['title', 'description', 'content'],
threshold: 0.3
});
// Wire fuse to a search input
document.getElementById('search').addEventListener('input', e => {
const results = fuse.search(e.target.value);
// Render results
});
});
</script>
# Build
schemaflux build --clean
# Deploy (example: rsync to a server)
rsync -avz _site/ user@server:/var/www/html/
# Deploy to Netlify (netlify.toml)
# [build]
# command = "go install github.com/greynewell/schemaflux/cmd/schemaflux@latest && schemaflux build"
# publish = "_site"