1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
|
# 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`.
## 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 `../`. |
| `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
<a href="{{rel .OutputPath "/notes/"}}">notes</a>
{{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)}}
<content type="html">{{xml (joinSections .Sections)}}</content>
```
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 `_` 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'
```
|