# 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` and is removed from `.Sections`. ## 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`. ## 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. The extracted H1 is removed from the body. | | `.Meta` | `map[string]any` containing optional YAML frontmatter. | | `.ModTime` | Source-file modification time as `time.Time`. | | `.Sections` | `[]template.HTML` containing one rendered fragment per top-level Markdown node. 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 `../`. | | `sortPages pages key` | Returns a copy. `"title"` sorts ascending case-insensitively; `"mod_time"` sorts newest first; every other key sorts newest first using that frontmatter value as a date. | | `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}} {{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. 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 `_` are ignored. 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' ```