Lifecycle Events
Lifecycle hooks let plugins run code at specific points in the build pipeline. Register a hook with alloy.hook() or alloy.on() to modify content, inject pages, transform data, or observe build events.
Hooks work identically across all plugin tiers (QuickJS, WASM, Node). Payloads are JSON-serializable.
Hook Registration
The options object is required. It controls execution order and payload scoping:
See Hook Scoping for the full scoping API.
All Lifecycle Events
Per-Build Hooks
These fire once per build. Payloads are JSON objects representing build-level state.
onConfig
Fires after config is loaded but before the build starts. The hook receives the full configuration object and must return it. Only fields on the mutable allowlist are applied back — all other fields are silently ignored.
Mutable fields:
| Field | Type | Description |
|---|---|---|
build.output |
string | Output directory |
build.clean |
boolean | Clean output before build |
structure.content |
string | Content directory |
structure.layouts |
string | Layouts directory |
structure.assets |
string | Assets directory |
structure.static |
string | Static files directory |
structure.data |
string | Data directory |
passthrough |
array | Passthrough file mappings ([{ from, to }]) |
plugins.workers |
number | Worker pool size |
plugins.timeout |
number | Hook timeout in milliseconds |
Fields not listed above (title, baseURL, language, taxonomies, etc.) are present in the payload for inspection but mutations have no effect.
Return value rules:
- Must return an object. Returning
nullor a non-object produces a build error. - Multiple
onConfighooks chain in priority order — each receives the previous hook’s return value. - A timed-out hook’s mutations are discarded; the next hook receives the pre-timeout value.
Path validation
Directory path fields (build.output, structure.content, structure.layouts, structure.assets, structure.static, structure.data) and passthrough entries are validated before any are applied to the config. If any field fails validation, the entire return value is rejected — no partial mutation.
Rejected values for path fields:
- Absolute paths (
/etc/shadow,C:\Windows) ..traversals that resolve above the project root (../../evil).(current directory — would conflict with the project root)- Empty strings
- On Windows: reserved device names (
NUL,CON) and volume-relative paths
Relative paths with embedded .. segments that resolve within the project are valid and cleaned before use (e.g., subdir/../dist becomes dist).
Passthrough-specific rules:
passthrough[N].fromfollows the same rules as path fields.from: "."is rejected (would copy the entire project root into output).passthrough[N].toallows"."and""— these mean “root of the output directory,” which is a valid destination.
Error messages include the field name and array index for passthrough entries:
onDataFetched
Fires after external data sources are fetched and merged into site data. Plugin can modify or enrich the data.
This is the primary mechanism for adding computed data that templates can access via site.data.*.
onBeforeValidation
Fires before output path conflict detection. The payload contains all computed output paths. Return { addOutputs: { path: source } } to register additional output paths that feed into conflict detection.
| Payload field | Type | Description |
|---|---|---|
outputPaths |
string[] | All computed page output paths |
| Return field | Type | Description |
|---|---|---|
addOutputs |
object | Map of additional output paths to source identifiers |
Unrecognized keys in the return value produce a build error. Each plugin runs independently via RunEachWithTimeout — plugins do not see each other’s additions.
onAfterValidation
Fires after conflict detection passes. The payload includes the validated output paths and the site data cascade. Return { cascade: { ... } } to merge data into siteData for template rendering.
| Payload field | Type | Description |
|---|---|---|
outputPaths |
string[] | Validated output paths (including any added by onBeforeValidation) |
cascade |
object | Current site data cascade |
| Return field | Type | Description |
|---|---|---|
cascade |
object | Merged into siteData — keys overwrite existing values |
Returning outputPaths in the return value has no effect. Unrecognized keys produce a build error. Each plugin runs independently.
Pre-Taxonomy Hook
onPagesReady
Fires once per language batch, after the data cascade is applied but before taxonomy collection. This is the injection point for virtual pages that need to participate in taxonomies.
Virtual page fields:
| Field | Required | Description |
|---|---|---|
path |
yes | Source-relative identifier (e.g., demos/button.md) |
url |
yes | Permalink (e.g., /demos/button/) |
frontMatter |
no | Page metadata, including taxonomy terms like tags |
content |
no | Raw markdown content (rendered through the pipeline) |
dependencies |
no | Array of project-root-relative file paths for incremental rebuild tracking |
Virtual pages injected here flow through the full remaining pipeline: taxonomy collection, content rendering, layout resolution, and output writing.
When using pages: false in the options, return { addPages: [...] } to inject pages without round-tripping all existing pages through the plugin bridge.
Virtual page dependencies
During alloy dev, virtual pages are re-rendered on every incremental rebuild by default. Declare dependencies to tell Alloy which source files a virtual page depends on — it will only re-render when one of those files changes.
dependencies value |
Incremental rebuild behavior |
|---|---|
["a.html", "b.css"] |
Re-render only when a listed file appears in the changed files |
[] (empty array) |
Never re-render — no file dependencies to invalidate |
| Omitted | Always re-render on every rebuild (default, safe fallback) |
Paths must be project-root-relative strings. Absolute paths, .. traversals above the project root, and empty strings produce build errors.
On initial builds and for newly added virtual pages, dependencies has no effect — pages always render at least once before dependency filtering applies.
Content Hooks
onContentLoaded
Fires once with the full pages array after content rendering. Modify frontMatter and html on existing pages. Other fields (content, path, url) are present for inspection but mutations are not applied back.
Mutating html:
Both frontMatter and html can be mutated in the same hook call. Changes to html are applied via SetRenderedBody — the modified HTML replaces the rendered content for that page before layout rendering.
The return array must be the same length and order as the input. Virtual page injection is not supported here – use onPagesReady instead.
onDataCascadeReady
Fires once with the full pages array after the data cascade is resolved. Each entry has the per-page cascade data. Plugin can enrich cascade data.
Per-Page Hooks
These fire once per page. They receive page-scoped payloads.
onContentTransformed
Fires after Markdown-to-HTML conversion but before layout rendering. Receives a page-scoped object with html, toc, path, url, and frontMatter.
onPageRendered
Fires after template rendering produces the final page HTML. Receives an HTML string and returns an HTML string.
Per-Asset Hook
onAssetProcess
Fires once per file in the assets directory during asset copy. Each invocation receives a single file’s path and content. Multiple onAssetProcess hooks chain — each receives the content returned by the previous hook.
| Field | Type | Description |
|---|---|---|
path |
string | File path relative to the assets directory (forward slashes, e.g., css/main.css) |
content |
string | Raw file content |
Return value:
| Return | Effect |
|---|---|
{ content: "..." } |
Replaces the file content in output |
null / undefined |
Keeps the original content |
Object without content key |
Keeps the original content |
The path key in the return value is ignored — the file is always written to its original relative path in the output directory. A hook error stops the build.
Read-Only Hooks
Return values are ignored. Plugins observe but cannot modify.
onBuildComplete
Fires after the build finishes. The payload uses PascalCase keys (the BuildResult struct has no JSON tag overrides). Duration is raw nanoseconds — divide by 1e6 for milliseconds.
| Field | Type | Description |
|---|---|---|
OutputDir |
string | Output directory path |
PageCount |
number | Total pages built |
PagesSkipped |
number | Pages skipped during incremental rebuilds |
Duration |
number | Build time in nanoseconds |
onDevServerStart
Fires when the dev server starts. The payload is the full site configuration object — there is no url field with the server address.
onFileChanged
Fires once per file-watch batch during alloy dev. The payload is an array of change events, not a single file path.
| Field | Type | Description |
|---|---|---|
Path |
string | File path relative to project root |
ChangeType |
number | Change category (1–8: content, template, data, asset, etc.) |
IsRemove |
boolean | true when the file was deleted |
Hook Execution Order
Hooks execute by priority (lower runs first), then by alphabetical plugin filename within the same priority. Each hook receives the output of the previous one – they chain, not race.
Hook Timeout
Each hook call is subject to the configured timeout (default 5 seconds). A timed-out hook produces a warning, its modifications are discarded, and the build continues with the pre-hook payload.
Related
- Hook Scoping – control what data hooks receive
- Plugin System – plugin tiers and registration
- QuickJS Plugins – embedded JS plugins
- Node Plugins – subprocess plugins with npm access