How a Build Works
Three stages, each driven by an optional function on the wattle table. Define none of them and nothing happens. Define one and the other two are skipped.
Gather
function wattle.gather(path)
if wattle.path.extname(path) == ".md" then
return { type = "page", file = path }
end
end
Called once for every file under every input path. Return a table to claim the file, or nothing to ignore it. What you return is stored at wattle.gathered[path].
This is where you decide what a file is. Nothing is read from disk unless you read it, so claiming a thousand images costs a thousand table constructions and no I/O.
Process
function wattle.process(item, path)
item.content = wattle.markdown.render(item.file)
return item
end
Called once per gathered item. Return a table, or an array of tables when one input should become several outputs. Results are stored at wattle.processed[path], always as an array.
This is the stage that can change the count — one Markdown file becoming a page and an excerpt, one photograph becoming four sizes — and the stage where anything needing to know about other files belongs.
Transform
function wattle.transform(item, path)
local out = o("index.html")
wattle.file.write(out, item.content)
return out
end
Called once per processed item, and it exists for its side effects: this is where files get written. Return the path you wrote, or an array of paths, and they are recorded at wattle.transformed[path]. Returning nothing is fine.
o(path) is shorthand for wattle.path.out(path), which resolves a name against the output directory.
Hooks
Six more optional functions run around the stages. Each after_ hook receives what that stage produced during this run.
function wattle.before_gather() end
function wattle.after_gather(items) end
function wattle.before_process() end
function wattle.after_process(items) end
function wattle.before_transform() end
function wattle.after_transform(items) end
before_gather is where setup goes: registering template helpers and partials, reading configuration, generating a file the rest of the build will find. after_process is where anything needing the whole set goes: a tag list, an archive page, a feed.
Pipeline State
wattle.inputs, wattle.output, wattle.gathered, wattle.processed, wattle.transformed.
inputs is writable. Append to it in your entry script and the walk picks it up, which is how a script owns the shape of its own build instead of taking it from the command line:
table.insert(wattle.inputs, "content")
table.insert(wattle.inputs, "static")
Commands
wattle build [ENTRY] -i <dir>... -o <dir> # run the pipeline
wattle clean -o <dir> # delete the output directory
wattle watch [ENTRY] -i <dir>... --serve # rebuild on change, optionally serve
wattle secrets <init|encrypt|decrypt|edit|show>
watch takes --serve to run a static file server over the output, --port (default 3000), --host (default 127.0.0.1), and --no-initial-build to skip the build on startup. The server resolves /about to about.html before about/index.html, and falls back to index.html inside a directory. There is no live reload; refresh the page yourself.
It listens on loopback because it serves a directory to anyone who asks and has no authentication. --host 0.0.0.0 makes it reachable from a phone or another machine, which is a reasonable thing to want and worth deciding on purpose.
clean refuses to remove ., .., or /.
Errors
A Lua error during a build is rendered with the file, the line, and the source underlined, plus every frame of the traceback:
error: [string "wattle.lua"]:8: attempt to index field 'sidecar' (a nil value)
|- wattle.lua:8:1
|
8 | item.title = item.sidecar.title
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Error: Build failed
This is the message the tool lives or dies by, so it got more attention than most of the rest of it.