Falz Docs

Build phase

Our indexed site is complete. Now we can use it to understand how Falz decides what to do on later builds. Keep working in the same project, or use the indexed-site checkpoint.

Notice what changes together

Run falz index followed by falz build twice. The second run has no changed inputs, so no transformations are needed.

Then try these changes one at a time, running both commands after each:

  • Edit only the body of posts/hello.md. Its page changes; the other post's listing does not need new metadata.
  • Change its front-matter title. Both post pages must refresh their shared list.
  • Edit .falz/schemes/nav.md. Home, about, and both posts use that fragment, so all their pages change.
  • Edit .falz/schemes/posts/post.html. Only posts use this template; home and about remain current.

Watch mode coordinates index and build automatically. In a one-shot workflow, falz build uses existing index data without refreshing it, so remember to index after indexed inputs change.

These different scopes are the point of incremental builds: Falz tracks the inputs of each output, rather than treating the whole project as one unit.

A source match becomes an entry

Falz walks the source tree directory by directory. In each directory it resolves the nearest layout and evaluates that layout's <Content> and nested <Asset> declarations. Top-level scheme assets are discovered from the layout as well.

Every source/declaration match becomes a build entry. An entry carries the information needed for one output:

  • whether it is content or an asset;
  • the source path and source tree it belongs to;
  • the governing layout and content slot;
  • the selected pipe;
  • <Var> values from the declaration;
  • the to output pattern.

The dependency graph connects entries to their layouts, templates, fragments, pipes, and consumed index namespaces. This graph lets a shared change affect all entries that rely on it without rebuilding unrelated work.

For example, posts/hello.md matched by the article declaration is one entry. A source can match differently named declarations to produce multiple representations, provided their output paths do not collide. The declaration's name is part of the entry identity.

For more detail on matching and layout lookup, see Layouts and the layout reference.

Decide what is build-dirty

Falz hashes the inputs that influence each entry and compares the result with dist/.manifest.json. A missing manifest record or a changed input makes the entry build-dirty.

What makes a build entry dirty?

Change Entries affected
Source content Entries created from that source file.
Selected pipe or bundled dependency Entries using that pipe.
Layout XML Content and assets governed by that layout.
Layout template Content rendered with that template.
Fragment content or fragment pipe Content rendered with layouts using that fragment.
<Var> value Entries created by that content declaration.
Consumed KV namespace Entries whose pipe lists the namespace in contract.consumes.
No manifest record The newly discovered entry.
Forced build Every discovered entry.

The build and index phases have separate dirty checks. A layout change can make output dirty without changing index data. A source or pipe change can make both phases dirty, but falz build does not run index() itself; run falz index first in a one-shot workflow.

Falz normally avoids reading and hashing a file again when its recorded size and modification time are unchanged. force_hash: true disables that shortcut. It changes how file hashes are calculated, whereas falz build --force tells Falz to schedule every entry regardless of the result.

Restore or transform

Each dirty entry is handed to a build worker. When a matching artifact is available in the content-addressed build cache, Falz can restore those bytes. Otherwise it reads the source and calls the selected pipe's transform() function.

The transform receives:

  • source bytes as a Buffer;
  • file and entry metadata;
  • project and content variables;
  • the entry's cached index blob;
  • KV namespaces declared by the pipe as consumed.

The result contains output bytes and may contain an alias. Falz expands the entry's to XML attribute after transformation, so {ALIAS} can take part in the destination path.

See Pipes for the transform contract and JavaScript API for the complete context.

Assemble the artifact

For a content entry, the transformed bytes fill the slot named by <Content name="...">. Falz uses the compiled layout template, rendered fragments, and <Var> values to produce the complete artifact.

For an asset entry, the transformed bytes already are the complete artifact, so no layout template is applied.

This distinction is why the same pipe can be used in either place. A text minifier can process a scheme asset directly, while a Markdown converter can produce a fragment that is inserted into a full page.

The detailed placeholder and rendering rules are in Layouts and Layout reference.

Write and commit state

Write workers create destination directories and write completed artifacts. The manifest is updated only with successfully written results. For each entry it records the build hash, output path, source hash information, and references to index data.

If an entry still exists but its calculated destination changes, Falz writes the new path and removes the old path recorded for that entry. For example, changing a returned alias from old-title to new-title moves that entry's generated page.

Try changing slug: first to slug: welcome in posts/hello.md, then index and build. The page moves to /posts/welcome/, the old destination disappears, and both post lists update.

Deleting that document is different: it removes the entry rather than moving its output. A deleted source or a layout pattern change can remove an entry entirely from the current graph. With build.clean: true, Falz compares the graph with the manifest and removes outputs belonging to entries that no longer exist. Without that setting, old output is left alone until an explicit orphan cleanup.

The output tree is a build result, not an input. Manual changes are outside dependency tracking and may be replaced by later writes or cache restoration.

Build cache and manifest

These two pieces of state have different jobs:

  • dist/.manifest.json maps build entries to their current hashes, output paths, and index references. It answers whether an entry is current.
  • The content-addressed store keeps artifact bytes under the user's cache directory. It can supply bytes for a scheduled entry without rerunning its transform.

Disabling the build cache does not disable incremental dirty checking; the manifest still prevents unchanged entries from being scheduled. For exact command behavior, see falz build and falz clean.

Concurrency and failure behavior

build.concurrency controls how many transforms can run at once. Completed artifacts move to a smaller pool of write workers, which batches writes by directory. This separation allows CPU-heavy conversion and filesystem writes to overlap.

Each JavaScript transform() call also has its pipe-specific transformTimeoutMs. A timeout, pipe exception, read error, or write error is attached to that entry and reported at the end of the build. Successful entries may already have been written, but the command returns an error when any scheduled entry failed.

Cancellation, including Ctrl+C, stops new work and lets the command shut down its workers. The manifest is saved from the completed state rather than claiming failed entries were built.

The guide's site now demonstrates the whole model: layouts select and compose files, pipes interpret them, indexing shares preparatory data, and builds update the affected outputs.

From here, use the layout reference, pipe reference, and JavaScript API to adapt the project to your own formats. For symptoms and recovery steps, use Troubleshooting.