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
122
123
124
125
|
# 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 `../`. |
| `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
<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. 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'
```
|