Blueprint API
The Blueprint is the heart of your site configuration. It provides a fluent API
to define tasks and the relationships between them.
Task Creation
Every task starts with config.task():
config.task()
.name("My Task") // Optional: Name for debugging/visualization
// ... chain methods ...
Handles and Dependencies
Hauchiwa uses a strongly-typed handle system to manage dependencies.
One<T>: A single value (e.g., a configuration object, a sitemap).- Resolved as:
&T
- Resolved as:
Many<T>: A collection of values (e.g., pages, images).- Resolved as:
Tracker<T>
- Resolved as:
You inject dependencies using .using():
let pages: Many<Document> = ...;
let config_obj: One<Config> = ...;
config.task()
.using((pages, config_obj)) // Pass a tuple for multiple dependencies
// ...
Task Operations
The final method in the chain determines how the task executes and what kind of output it produces.
1. run()
Use .run() for tasks with zero dependencies that produce a single output
(One<T>).
config.task().run(|ctx| {
Ok("Hello World")
});
2. merge() (Gather)
Use .merge() to gather dependencies. This is the most common operation. It
takes any number of dependencies (One or Many) and runs once, providing access
to all of them.
- Input: Defined by
.using(). - Output: Returns
One<R>.
// Example: Generate a sitemap from all pages
config.task()
.using(pages)
.merge(|ctx, pages| {
// 'pages' is a Tracker<Document>
let urls: Vec<String> = pages.values().map(|p| p.meta.href.clone()).collect();
Ok(urls)
});
3. spread() (Scatter)
Use .spread() to take a single input and scatter it into multiple outputs
(Many<T>).
- Input: Defined by
.using(). - Output: Returns
Many<R>.
// Example: Create a task that generates multiple variants from one config
config.task()
.using(global_config)
.spread(|ctx, config| {
Ok(vec![
("light".to_string(), Theme::Light),
("dark".to_string(), Theme::Dark),
])
});
4. each().map() (Map)
Use .each() combined with .map() to process a Many handle item-by-item.
This is efficient because it enables fine-grained invalidation. If only one
item in the source changes, only that specific item is re-processed.
- Input: A primary
Many<T>handle passed to.each().- Extras: Optional extra dependencies via
.using().
- Extras: Optional extra dependencies via
- Output: Returns
Many<R>.
// Example: Render HTML for each Markdown page
config.task()
.each(pages) // Primary dependency (Many<Document>)
.using(template) // Secondary dependency (One<Template>)
.map(|ctx, doc, template| {
// 'doc' is &Document (single item)
// 'template' is &Template (resolved dependency)
let html = template.render(doc);
Ok(html)
});
5. glob().map()
A shortcut to load files directly without a separate loader.
config.task()
.glob("src/assets/*.png")?
.map(|ctx, store, input| {
// Process file...
Ok(processed_image)
});
TaskContext
Every task callback receives a ctx: &TaskContext<G> as its first argument. It
provides two fields:
| Field | Type | Description |
|---|---|---|
ctx.env |
&Environment<G> |
Global build settings and your user data (ctx.env.data). |
ctx.importmap |
&ImportMap |
Aggregated JS import map from all upstream dependencies. |
Environment fields:
| Field | Type | Description |
|---|---|---|
env.data |
G |
Your user-defined global data (passed to .build(data) / .watch(data)). |
env.mode |
Mode |
Mode::Build or Mode::Watch. Useful to skip dev-only work in production. |
env.port |
Option<u16> |
Dev server port, None during a static build. |
ctx.importmap.to_html() serializes the accumulated import map as a
<script type="importmap"> tag ready for inclusion in your HTML <head>.
Static file copying
Use copy_static to mirror a directory tree into the output directory without
processing it. This is suitable for fonts, favicons, robots.txt, and other files
that should be served verbatim.
let config = Blueprint::<()>::new()
.copy_static("assets/fonts", "fonts")
.copy_static("assets/images", "images");
Arguments:
- First: the source directory to copy from.
- Second: the destination path inside the output directory (e.g.
"fonts"→dist/fonts/).
Paths that would escape the output directory (e.g. "../../etc") are rejected at
build time. Unchanged files are skipped via mtime and content hashing, so repeated
builds stay fast.
Directory configuration
By default, Hauchiwa writes output to dist/ and keeps its build cache in .cache/.
Both can be changed with builder methods on Blueprint:
let config = Blueprint::<()>::new()
.set_dir_dist("output") // write pages and assets here
.set_dir_cache(".hauchiwa-cache"); // store snapshot meta and hashed assets here
This is useful when your project already uses dist/ for something else, or when
you want to co-locate the cache with the build artefacts.
Custom loaders
The built-in loaders are just tasks. You can write your own by using .glob().map(),
which scans the filesystem and runs a closure for each matched file:
let data: Many<MyData> = config
.task()
.glob("data/*.json")?
.map(|_ctx, _store, input| {
let content = std::fs::read(&input.path)?;
let data: MyData = serde_json::from_slice(&content)
.map_err(|e| anyhow::anyhow!("Failed to parse {}: {}", input.path, e))?;
Ok(data)
});
The input argument provides:
input.path- the matched file pathinput.hash- BLAKE3 hash of the file content (for use as a cache key)
The returned Many<MyData> handle can be wired into any downstream task just like
a handle from a built-in loader.
Error handling
website.build() and website.watch() return Result<_, HauchiwaError>, a typed
error enum. You can propagate it with ? directly - HauchiwaError implements
std::error::Error, so it works in any anyhow::Result context too.
When you need to react to specific failures, match on the variants:
use hauchiwa::error::HauchiwaError;
match website.build(data) {
Ok(diagnostics) => { /* inspect timings */ }
Err(HauchiwaError::Preflight(msg)) => {
eprintln!("Missing dependencies:\n{msg}");
std::process::exit(1);
}
Err(HauchiwaError::GraphCycle) => {
eprintln!("Cycle detected in task graph - check your .using() wiring");
std::process::exit(1);
}
Err(e) => return Err(e.into()),
}
Notable variants:
| Variant | When it occurs |
|---|---|
HauchiwaError::Preflight(msg) |
A required binary (e.g. esbuild, deno) is missing from PATH. |
HauchiwaError::GraphCycle |
A cycle was detected in the task dependency graph. |
HauchiwaError::Build(BuildError) |
A task returned an error or an I/O failure occurred during the build. |
HauchiwaError::StepStatic(e) |
copy_static failed (unsafe path or I/O error). |
HauchiwaError::AssetNotFound(path) |
A task requested an asset that was not found in the graph. |