Examples
EN

Build Hooks

Build hooks allow you to run custom shell commands before and after the build process. This is useful for tasks like installing dependencies, preprocessing data, optimizing assets, or triggering deployments.

Configuration

Define hooks in config.toml:

[build]
hooks.pre = ["npm install", "npx tsc"]
hooks.post = ["npm run minify", "npx pagefind --site public"]
Key Type Default Description
hooks.pre array [] Commands to run before building
hooks.post array [] Commands to run after building

How It Works

Pre-Build Hooks

Pre-build hooks run before any content processing begins. They are ideal for:

[build]
hooks.pre = [
  "npm ci",
  "npx tailwindcss -i src/input.css -o static/assets/css/main.css",
  "python scripts/fetch-data.py"
]

If a pre-build hook fails (exits with a non-zero status), the build process is aborted. This prevents building with missing dependencies or broken assets.

Post-Build Hooks

Post-build hooks run after the site has been generated in the output directory. They are ideal for:

[build]
hooks.post = [
  "npx imagemin public/images/* --out-dir=public/images",
  "npx pagefind --site public",
  "./scripts/deploy.sh"
]

If a post-build hook fails, a warning is shown but the build is not considered failed. The generated site remains intact.

Execution Order

Commands are executed sequentially in the order they are defined:

[build]
hooks.pre = ["echo Step 1", "echo Step 2", "echo Step 3"]

Output:

Running pre-build hook: echo Step 1
Step 1
Running pre-build hook: echo Step 2
Step 2
Running pre-build hook: echo Step 3
Step 3

Serve Mode

Build hooks also run during hwaro serve, but only on full rebuilds:

This keeps save-to-reload fast. If a hook produces something you need refreshed while the server is running, touch config.toml or restart hwaro serve to force a full rebuild.

Rebuild loops

A hook that writes into a watched directory (content/, templates/, static/, data/, i18n/), or into config.toml itself, feeds the watcher its own output. The harmless case is handled for you: when a run lands the same bytes the build already read (curl -o data/team.json fetching an unchanged payload, a bundler re-emitting an identical file, a hook that only touches config.toml), the session settles instead of rebuilding forever. Depending on where the file lives, the rewrite is either ignored outright or absorbed by a partial rebuild that does not re-run your commands.

Two patterns still rebuild on every run, because to the watcher they are genuine changes:

Make the output deterministic, or write it somewhere the watcher does not look (.hwaro/ is ignored, as is anything outside the directories listed above).

Use Cases

TypeScript Compilation

[build]
hooks.pre = ["npx tsc --outDir static/assets/js"]

Tailwind CSS

[build]
hooks.pre = [
  "npx tailwindcss -i src/styles.css -o static/assets/css/styles.css --minify"
]

Fetching Data from an API

For the common case, a GET request whose payload should land in site.data, you don't need a hook at all. Declare a [[data.remote]] source in config.toml and Hwaro fetches it once per build, with disk caching and explicit error handling built in.

A pre-build hook remains the right tool when the fetch is more than a single request. load_data() reads from disk only, so remote data is baked in by fetching it into data/ before the build. Every file under data/ is then exposed as site.data:

[build]
hooks.pre = ["curl -sfL https://api.example.com/team -o data/team.json"]
{% for member in site.data.team %}
  <li>{{ member.name }}</li>
{% endfor %}

curl -f exits non-zero on an HTTP error, which aborts the build rather than publishing a site with missing data. For anything beyond a single request (pagination, auth headers, reshaping the response), write a script and call that instead:

[build]
hooks.pre = ["./scripts/fetch-data.sh"]

Commit the fetched file if you want reproducible builds offline; add it to .gitignore if you always want it fresh.

Generate a client-side search index after build:

[build]
hooks.post = ["npx pagefind --site public"]

Image Optimization

[build]
hooks.post = [
  "npx imagemin public/**/*.{jpg,png} --out-dir=public"
]

Custom Deploy Script

[build]
hooks.post = ["./scripts/deploy.sh"]

Full Pipeline Example

[build]
hooks.pre = [
  "npm ci",
  "npx tsc",
  "npx tailwindcss -i src/input.css -o static/assets/css/main.css --minify"
]
hooks.post = [
  "npx pagefind --site public",
  "npx imagemin public/images/* --out-dir=public/images",
  "echo 'Build complete!'"
]

Error Handling

Hook Type On Failure
Pre-build ❌ Build aborted — no content is processed
Post-build ⚠️ Warning shown — generated site is preserved

This design ensures that critical setup tasks (pre-build) must succeed, while optional optimization tasks (post-build) don't block the build output.

Tips

See Also