# Template Guide
tilstream's templates are plain Go `text/template`. This document
lists the variables each template receives, the partials you can
compose from, and some example overrides.
For how templates fit into the pipeline, see
[docs/architecture.md](/src/tilstream/docs-architecture-md/).
## Templates
There are four top-level templates:
| Name | Purpose |
|---------------|----------------------------------|
| `base.tmpl` | HTML shell used by all pages |
| `post.tmpl` | single post page |
| `index.tmpl` | home page, list of posts |
| `tag.tmpl` | filtered list for a single tag |
`base.tmpl` defines blocks (`content`, `title`, `head_extra`) that
the child templates fill. Look at
[`templates/post.tmpl`](/src/tilstream/templates-post-tmpl/) for the
canonical example of extending `base.tmpl`.
## How to override
Supply `--templates path/to/dir` and put files with the same names
there. Each file must `{{ define "name" }}...{{ end }}` blocks; the
base shell picks them up.
Start by copying the built-in templates. They are short.
## Variables
### Common (all templates)
| Variable | Type | Meaning |
|--------------------|-------------------|----------------------------------------|
| `.Site.Title` | string | from site config |
| `.Site.URL` | string | canonical URL, trailing slash |
| `.Site.Author` | string | author name |
| `.Site.BuiltAt` | time.Time | build start time |
| `.Site.Posts` | []Post | every published post, newest first |
| `.Site.Drafts` | []Post | present only when `--drafts` was set |
| `.Site.Tags` | map[string][]Post | tag name to posts |
### `post.tmpl` extras
.Post Post // the current post
.Prev *Post // previous by date, or nil
.Next *Post // next by date, or nil
### `index.tmpl` extras
.Posts []Post // ordered newest first; drafts only if flag set
### `tag.tmpl` extras
.Tag string // which tag we're rendering
.Posts []Post // posts in that tag
## `Post` fields available in templates
.Post.Slug string
.Post.Title string
.Post.Date time.Time
.Post.Tags []string
.Post.Categories []string
.Post.Draft bool
.Post.Source string // original filename
.Post.BodyHTML template.HTML
.Post.BodyText string // plain text, stripped
.Post.Meta map[string]any // any other front-matter keys
Anything you put in a post's front-matter that isn't one of the
known keys ends up in `.Post.Meta`. For example, a custom
`cover_image` field is reachable as `{{ .Post.Meta.cover_image }}`.
## Built-in template functions
In addition to Go's stdlib funcs (`printf`, `len`, `eq`, etc.):
| Function | Signature | Purpose |
|--------------------|-----------------------------------|----------------------------------------|
| `date` | `date "2006-01-02" t time.Time` | format time |
| `markdown` | `markdown s string` | render a string as markdown |
| `trim` | `trim s string` | strings.TrimSpace |
| `slug` | `slug s string` | normalise to URL slug |
| `join` | `join xs []string sep string` | strings.Join |
| `truncate` | `truncate s string n int` | word-respecting truncation |
| `readingTime` | `readingTime s string` | minutes at 200 wpm |
## Partials
Any template file under `--templates/partials/` is available as
`{{ template "partials/FILENAME" . }}`. The context is the caller's
context by default; pass something else explicitly if you want.
Built-in partials:
- `partials/nav.tmpl` - site navigation
- `partials/tag-list.tmpl` - inline tag list for a post
- `partials/post-list.tmpl` - list of post cards, reused by index
and tag pages
- `partials/footer.tmpl` - footer
### Example: custom nav
`templates/partials/nav.tmpl`:
{{ define "partials/nav.tmpl" }}
<nav>
<a href="{{ .Site.URL }}">Home</a>
<a href="{{ .Site.URL }}tags/">Tags</a>
<a href="{{ .Site.URL }}feed.xml">RSS</a>
</nav>
{{ end }}
## Example: minimal `post.tmpl`
{{ define "title" }}{{ .Post.Title }} - {{ .Site.Title }}{{ end }}
{{ define "content" }}
<article>
<header>
<h1>{{ .Post.Title }}</h1>
<time datetime="{{ date "2006-01-02" .Post.Date }}">
{{ date "Jan 2, 2006" .Post.Date }}
</time>
{{ if .Post.Tags }}
{{ template "partials/tag-list.tmpl" .Post }}
{{ end }}
</header>
{{ .Post.BodyHTML }}
<footer>
{{ if .Prev }}<a href="{{ .Prev.Slug }}">← {{ .Prev.Title }}</a>{{ end }}
{{ if .Next }}<a href="{{ .Next.Slug }}">{{ .Next.Title }} →</a>{{ end }}
</footer>
</article>
{{ end }}
## Example: index page grouped by year
{{ define "content" }}
{{ range $year, $posts := .Posts | groupBy "Year" }}
<h2>{{ $year }}</h2>
<ul>
{{ range $posts }}
<li>
<a href="{{ .Slug }}">{{ .Title }}</a>
<time>{{ date "Jan 2" .Date }}</time>
</li>
{{ end }}
</ul>
{{ end }}
{{ end }}
`groupBy` is not a built-in. Add it in your own template set - see
"adding functions" below.
## Adding template functions
You can't add functions via templates alone. For that, patch
`internal/render/render.go` to add to `funcMap`:
funcMap["groupBy"] = func(posts []Post, field string) map[any][]Post {
m := map[any][]Post{}
for _, p := range posts {
switch field {
case "Year":
m[p.Date.Year()] = append(m[p.Date.Year()], p)
...
}
}
return m
}
This is a code change, not a template override. I have resisted
adding a plugin system; the tool is small, and hacking the render
package is fine.
## Live reload
`tilstream serve` injects a small JS snippet at the end of `<body>`
that opens an SSE connection to `/__tilstream/reload` and refreshes
on each message. The snippet is only injected in `serve` mode, not
`build` output. If you override the template shell, the snippet
goes in automatically - you don't need to add it.
## Gotchas
- Template parse errors are fatal at startup. The error message
includes the offending file and line.
- `template.HTML` bypasses escaping. Only use it for content that
is already safe HTML (like `.Post.BodyHTML`, which came from
goldmark and is sanitised at render time).
- A template that references a missing field prints `<no value>` in
text mode; in tilstream it's a runtime error instead. We use the
`missingkey=error` option, so typos fail loudly.
## Shipping your theme
If you've built a template set you like, put the directory under
version control and pass `--templates` in your build command. No
plugin install step. Copy-pasteable themes are the model.