Hauchiwa Docs

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.

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.

// 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>).

// 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.

// 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:

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:

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.