# Weft Weft is the small static-site generator used by `tjp.lol`. It recursively turns `page.md` into `page.html` and renders explicit `*.html.tmpl` and `*.xml.tmpl` outputs in place. ```sh go build -o weft . ./weft -gitignore ../tjp.lol https://tjp.lol ``` Markdown uses CommonMark with GFM tables, strikethrough, task lists, and autolinks. Raw Markdown HTML is disabled. Optional YAML frontmatter is exposed as `.Meta`; the first H1 becomes `.Title`, while `.Sections` preserves every top-level Markdown node, including that H1. ## Site templates All non-underscore `.tmpl` files are loaded into one named template set. Files ending in `.html.tmpl` or `.xml.tmpl` also render to the same path without the `.tmpl` suffix. Other `.tmpl` files only define reusable templates. Markdown pages select the first defined template matching their source path, walking toward the site root before using the fallback. For example, `notes/recipes/cocktails/old_fashioned.md` checks `notes/recipes/cocktails/old_fashioned`, `notes/recipes/cocktails`, `notes/recipes`, `notes`, then `page`. Set a different fallback with `-fallback template`; it defaults to `page`. ### Markdown element templates Templates named with the `markdown/` prefix can replace the HTML wrapper for individual Markdown elements. Their `.Content` is the recursively rendered content, so nested overrides continue to apply. Undefined element templates use Goldmark's default HTML. A build fails if it finds an unsupported `markdown/` template name. | Template | Fields | | --- | --- | | `markdown/heading1` through `markdown/heading6` | `Content template.HTML`, `ID string` | | `markdown/paragraph` | `Content template.HTML` | | `markdown/blockquote` | `Content template.HTML` | | `markdown/code_block` | `Content template.HTML` | | `markdown/fenced_code_block` | `Content template.HTML`, `HighlightedContent template.HTML`, `Language string` | | `markdown/unordered_list` | `Content template.HTML`, `Tight bool` | | `markdown/ordered_list` | `Content template.HTML`, `Start int`, `Tight bool` | | `markdown/list_item` | `Content template.HTML` | | `markdown/thematic_break` | no fields | | `markdown/emphasis` | `Content template.HTML` | | `markdown/strong` | `Content template.HTML` | | `markdown/code_span` | `Content template.HTML` | | `markdown/link` | `Content template.HTML`, `Destination string`, `Title string` | | `markdown/autolink` | `Content template.HTML`, `Destination string` | | `markdown/email_autolink` | `Content template.HTML`, `Destination string` | | `markdown/image` | `Alt string`, `Destination string`, `Title string` | | `markdown/hard_break` | no fields | | `markdown/strikethrough` | `Content template.HTML` | | `markdown/task_checkbox` | `Checked bool` | | `markdown/table` | `Content template.HTML` | | `markdown/table_header` | `Content template.HTML` | | `markdown/table_body` | `Content template.HTML` | | `markdown/table_row` | `Content template.HTML` | | `markdown/table_header_cell` | `Content template.HTML`, `Alignment string` | | `markdown/table_cell` | `Content template.HTML`, `Alignment string` | `.Content` is trusted, already-rendered HTML. For code spans and code blocks it is escaped source text instead. Images flatten their description to plain `.Alt`; elements without child content omit `.Content`. Destinations, titles, language names, IDs, and alt text remain plain strings and are contextually escaped by `html/template`. Raw Markdown HTML stays disabled and has no override. `Tight` follows CommonMark: tight list items contain unwrapped inline content, while loose-list paragraphs pass through `markdown/paragraph`. `Start` is the first ordered-list marker, including `1`; later markers do not change item numbers. Table cell `.Alignment` is `left`, `right`, `center`, or `none`. `markdown/table_body` represents the generated `
` and is omitted when a table has no body rows. Defining `markdown/fenced_code_block` also enables build-time highlighting. `.Language` is the first word of the fence info string. For a known exact Chroma language, `.HighlightedContent` contains escaped, class-based token markup with the fixed `chroma-` CSS-class prefix and no `` or ``
wrapper. It is empty for blank or unknown languages and on highlighting errors;
`.Content` always provides escaped source as a safe fallback. Weft does not
generate CSS or client-side JavaScript.
```gotemplate
{{define "markdown/paragraph"}}{{.Content}}
{{end}}
{{define "markdown/ordered_list"}}
{{.Content}}
{{end}}
{{define "markdown/list_item"}}{{.Content}} {{end}}
{{define "markdown/fenced_code_block"}}
{{if .HighlightedContent}}{{.HighlightedContent}}{{else}}{{.Content}}{{end}}
{{end}}
{{define "markdown/table_header"}}{{.Content}} {{end}}
{{define "markdown/table_body"}}{{.Content}}{{end}}
```
## Template data
Every template receives this root value:
```go
struct {
*Page
Pages map[string][]*Page
}
```
The current `Page` is embedded, so its fields can be accessed as either
`.Title` or `.Page.Title`:
| Field | Meaning |
| --- | --- |
| `.SourcePath` | Slash-separated source path relative to the site root. |
| `.OutputPath` | Slash-separated generated path relative to the site root. |
| `.URL` | Root-relative URL. Markdown `index.html` outputs use their directory URL. |
| `.CanonicalURL` | `.URL` beneath `canonical-root` |
| `.Title` | First H1, then frontmatter `title`, then a filename-derived fallback. |
| `.Meta` | `map[string]any` containing optional YAML frontmatter. |
| `.ModTime` | Source-file modification time as `time.Time`. |
| `.Sections` | `[]template.HTML` containing every rendered top-level Markdown node in source order, including the H1 used for `.Title`. Raw Markdown HTML is disabled before these trusted fragments are created. |
| `.Pages` | All discovered Markdown pages grouped by ancestor directory. This field belongs to the root template value, not `Page`. |
`.Pages` uses slash-separated directory keys. The root key is `""`. A page at
`notes/recipes/cocktails/old_fashioned.md` appears under `""`, `"notes"`,
`"notes/recipes"`, and `"notes/recipes/cocktails"`; every collection points to
the same `Page`. Underscore-excluded sources and explicit output templates do
not enter the inventory.
Explicit `*.html.tmpl` and `*.xml.tmpl` outputs receive their source and output
paths, URL fields, an empty `.Meta`, and the complete `.Pages` inventory. They
have no Markdown title, modification time, or sections.
## Template functions
| Function | Result |
| --- | --- |
| `rel from target` | Makes a root-relative site target relative to the directory containing `from`. HTTP(S), `mailto:`, and fragment targets pass through. A trailing `/` is preserved, so `rel "about/index.html" "/"` returns `../`. |
| `splitPath value` | Splits a slash-separated path into components. |
| `joinPath parts` | Joins path components with `/`. |
| `dirPath value` | Removes the final path component without leaving a trailing `/`. |
| `sortPages pages keys...` | Returns a copy. `"title"` sorts ascending case-insensitively. Date keys sort newest first, using the first valid value for each page; `"mod_time"` selects `.ModTime` and other keys select frontmatter values. |
| `filterPages pages key value` | Keeps pages whose value stringifies equally. `"title"` and `"output"` select `.Title` and `.OutputPath`; other keys select `.Meta[key]`. |
| `slicePages pages start end` | Returns the half-open range `[start:end]`, clamping both bounds instead of failing. |
| `joinSections sections` | Concatenates rendered Markdown sections and returns trusted `template.HTML`. |
| `date value layout` | Parses a `time.Time`, RFC 3339 string, or `YYYY-MM-DD` string and formats it with a Go time layout. An invalid value formats as the zero time. |
| `rfc3339 value` | Parses the same date values and formats them as RFC 3339. An invalid value formats as the zero time. |
| `xml value` | Converts a trusted value back to a plain string so `html/template` escapes it in XML text. Use this around `joinSections` for escaped Atom `content type="html"`. |
Examples:
```gotemplate
notes
{{range sortPages (index .Pages "weblog") "post_date"}}{{.Title}}{{end}}
{{range sortPages (index .Pages "weblog") "post_date" "mod_time"}}{{.Title}}{{end}}
{{with index .Sections 0}}{{.}}{{end}}
{{joinSections (slice .Sections 1)}}
{{xml (joinSections .Sections)}}
```
All standard `html/template` actions and functions remain available, including
`and`, `or`, `not`, `call`, `html`, `index`, `slice`, `js`, `len`, `print`,
`printf`, `println`, `urlquery`, `eq`, `ne`, `lt`, `le`, `gt`, and `ge`.
`html/template` applies contextual escaping to template output.
## Git ignore management
`-gitignore` atomically replaces a marked block in the site-root `.gitignore`
with exact, root-anchored paths for the current generated outputs. Outputs below
symlinked directories are omitted because Git does not traverse them. Content
outside the block is preserved, and stale output entries are removed. Without
the flag, Weft does not touch `.gitignore`.
```gitignore
# BEGIN weft generated outputs
/feed.xml
/index.html
# END weft generated outputs
```
Weft validates generated internal links and XML before changing the site. It
tracks ownership in `.weft-generated.json`, refuses to replace untracked files,
removes only tracked stale outputs, and installs a completed build with rollback
on write failure. Files and whole subtrees beginning with `_` or `.` are ignored.
Symlinked files and directories are followed under their logical site paths;
cycles fail the build.
For cron, schedule the build after Syncthing's settling window and use the
host's lock and logging tools, for example:
```cron
17 * * * * sleep 60 && flock -n /run/lock/weft.lock /usr/local/bin/weft -gitignore /srv/tjp.lol https://tjp.lol >>/var/log/weft.log 2>&1 || logger -t weft 'build failed or lock unavailable'
```