summaryrefslogtreecommitdiff
path: root/README.md
blob: 477a95bc83a3ac75e27155e0f1133006c1732e61 (plain)
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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# 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 `<tbody>` 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 `<pre>` or `<code>`
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"}}<p class="weft">{{.Content}}</p>{{end}}
{{define "markdown/ordered_list"}}
<ol class="weft"{{if ne .Start 1}} start="{{.Start}}"{{end}}>{{.Content}}</ol>
{{end}}
{{define "markdown/list_item"}}<li class="weft">{{.Content}}</li>{{end}}
{{define "markdown/fenced_code_block"}}
<pre class="weft chroma"><code class="weft language-{{.Language}}">{{if .HighlightedContent}}{{.HighlightedContent}}{{else}}{{.Content}}{{end}}</code></pre>
{{end}}
{{define "markdown/table_header"}}<thead class="weft"><tr class="weft">{{.Content}}</tr></thead>{{end}}
{{define "markdown/table_body"}}<tbody class="weft">{{.Content}}</tbody>{{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
<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'
```