Recipes
This document defines how recipe QIP components are discovered from disk.
Root #
- Recipe root directory is discovered from the site root as
_recipes by default.
--recipes <dir> overrides discovery for shared or unusual layouts.
- Recipes are grouped by exact MIME type:
_recipes/text/markdown/
_recipes/text/html/
_recipes/text/javascript/
_recipes/image/png/
_recipes/application/warc/
Given MIME type/subtype, recipe directory is:
<recipe-root>/<type>/<subtype>/
WARC Recipes #
application/warc recipes run at the whole-site layer instead of one page at a time. You can use them for site-wide transforms, such as adding trailing-slash redirects, verifying there are no broken links, or using the path to modify body content.
- Directory:
_recipes/application/warc/
- Typical use:
- link integrity checks on the full routed archive
- JavaScript module import checks across rendered HTML
- archive rewrites before export (for example tar/static packaging pipelines)
- Example filenames:
10-warc-check-broken-links.wasm
20-warc-check-broken-module-imports.wasm
30-warc-to-sitemap.wasm
40-warc-add-open-graph-image-meta.wasm
Debugging broken links
warc-check-broken-links.wasm traps when an internal HTML link does not resolve. To inspect the failures, run the same archive through warc-extract-broken-links.wasm:
qip router warc ./site --view-source \
| qip run components/application/warc/warc-extract-broken-links.wasm
The result is another application/warc archive. It keeps only response pages containing broken links and reduces each HTML body to the exact opening tags with broken href, src, action, data, or srcset values. An archive with no broken links produces an empty WARC.
Rendering referenced content sizes
recipes/application/warc/25-add-content-size.wasm fills
<qip-content-size> elements from the response body stored at an absolute
site path:
<qip-content-size src="/components/example.wasm"></qip-content-size>
Bodies below 1,000 bytes render as bytes. Larger bodies render as decimal
kilobytes with two fractional digits. The recipe updates the enclosing HTTP
and WARC content lengths. During single-route development, the router adds
direct static src dependencies to the subset WARC. An unresolved size path
is an error instead of producing a plausible size.
Loading custom elements selectively
components/application/warc/warc-add-custom-element-scripts.wasm connects element routes to the pages that use them. It discovers top-level /elements/<tag-name>.js responses in the archive, detects matching custom-element tags in each HTML response, and inserts one external module script per used element:
<script type="module" src="/elements/qip-edit.js"></script>
The recipe ignores tag-shaped text in comments, script, style, textarea, and title content. Existing scripts are not inserted again. Nested routes such as /elements/lib/shared.js remain available to imports but are not treated as entrypoints.
Run this recipe late in the WARC chain so it sees elements introduced by earlier transforms. This repository links it as 99-add-custom-element-scripts.wasm. During single-route development, the router includes transformed top-level element modules in the subset WARC so discovery has the same inputs as a whole-site export.
Execution Context #
WARC recipes can run in two useful scopes. Pick the scope based on the question you are answering.
- Subset/path scope (faster iteration):
qip router dev applies WARC recipe behavior on the currently resolved response.
qip router get / qip router head let you inspect one routed path.
- Whole-site scope (final archive behavior):
qip router warc <site> ... enumerates the full routed site, builds one WARC, then applies _recipes/application/warc/*.
Use subset scope while developing recipe logic. Use whole-site scope before publishing so final archive semantics are still exercised.
qip router get ./site /docs/router
qip router warc ./site
Planning And Dry Runs #
qip dry run resolves and validates the same ordered component pipeline as
qip run, but does not read input, call render, or write output:
qip dry run \
components/text/markdown/commonmark.0.31.2.wasm \
components/text/html/html-page-wrap.wasm
The report is intended to be useful in CI logs without another formatting
step:
Pipeline compatible: 2 step(s)
1. components/text/markdown/commonmark.0.31.2.wasm — Content
Input: encoding=UTF-8, type=text/markdown, capacity=2.0 MiB (2097152 bytes)
Output: encoding=UTF-8, type=text/html, capacity=2.0 MiB (2097152 bytes)
Buffers: 4.0 MiB (4194304 bytes)
2. components/text/html/html-page-wrap.wasm — Content
Input: encoding=UTF-8, type=text/html, capacity=256.0 KiB (262144 bytes)
Output: encoding=UTF-8, type=text/html, capacity=512.0 KiB (524288 bytes)
Buffers: 768.0 KiB (786432 bytes)
Note: step 2 (components/text/html/html-page-wrap.wasm): previous output capacity 2.0 MiB (2097152 bytes) exceeds this input capacity 256.0 KiB (262144 bytes); qip run remains valid when the actual intermediate output fits
Total declared buffer capacity: 4.8 MiB (4980736 bytes)
Warnings: 1
A compatible plan exits successfully. Invalid component contracts, uniforms,
encoding or MIME composition, and module-policy violations return a non-zero
exit status.
Use --capacities-must-fit to turn capacity warnings into errors:
qip dry run --capacities-must-fit \
components/text/markdown/commonmark.0.31.2.wasm \
components/text/html/html-page-wrap.wasm
The check requires each Content component's declared maximum output capacity
to fit the next Content component's input capacity. This is useful in CI and
when refining component contracts: without the flag, the pipeline remains
valid when its actual intermediate values fit. Tile capacities are per-tile
working buffers rather than whole-image Content capacities, so the Tile
contract validates those separately.
The host first extracts a plain description for each recipe step: component
kind, input and output encoding, optional MIME types, declared buffer
capacities, and Tile halo or Interactive frame dimensions. A pure planner then
validates those values and returns the ordered plan used by both commands. The
dry-run output prints every step and the sum of its declared input/output
buffer capacities. An in-place Tile buffer appears as both input and output but
is counted once.
Composition is directional and based only on the ordered step descriptions.
The planner does not inspect example input bytes or use browser/runtime
heuristics, so the same component artifacts and uniforms produce the same plan
or the same error:
- UTF-8 may flow into a raw-bytes input. This is safe widening: UTF-8 is already
bytes, and browser hosts encode the string before calling a bytes component.
Raw bytes never flow implicitly into a UTF-8 input.
- A step with a declared input MIME type requires the current type to match
exactly. An unspecified current type does not satisfy a declared type.
- A step with no input MIME type is generic and accepts the current type when
the encoding matches.
- A declared output MIME type replaces the current type. Generic same-encoding
transforms preserve it. Raw bytes converted to UTF-8 produce an unspecified
MIME type.
- Direct
qip run input has no MIME channel, so the first step's declared input
type expresses the user's intent. This exception applies only at the pipeline
boundary, not between steps.
- A Tile group is an explicit image bridge: it accepts
image/bmp raw bytes,
processes RGBA32Float tiles internally, and returns image/bmp raw bytes.
There is no generic bytes-to-pixels rule. Image tiling is available only
through that explicit bridge, which keeps text, opaque binary data, and pixel
buffers from being guessed into one another.
The encoding relationship is small:
Content encodings
raw bytes
└── valid UTF-8
Allowed widening: UTF-8 ──> raw bytes
Rejected narrowing: raw bytes -X-> UTF-8
Explicit Tile bridge (not subtyping)
image/bmp raw bytes
│ host decodes
v
RGBA32Float tiles (width × height × 4 channels, in-place)
│ host encodes
v
image/bmp raw bytes
RGBA32Float pixels are physically held in linear memory, but they are not an
opaque Content bytes value. Their dimensions, channels, coordinates, tile
size, and halo are part of the Tile contract. Only the host's explicit image
bridge may cross that boundary.
For example, this plan decodes SVG Content to BMP, applies an in-place Tile
filter, then passes BMP Content to the ICO encoder:
qip dry run \
components/image/svg+xml/svg-rasterize.wasm \
components/rgba/brightness.wasm '?brightness=0.1' \
components/image/bmp/bmp-to-ico.wasm
The middle step reports RGBA32Float tile for its input and output encoding;
the adjacent Content steps report image/bmp raw bytes at the bridge.
Capacity maxima do not make two steps incompatible by themselves: an upstream
component may declare a larger output buffer while producing an actual value
that fits the next input buffer. Dry run reports this as a warning because only
execution can determine the intermediate byte count.
Host And URLs #
qip router warc controls canonical route host via --host <host>. We prefer setting this explicitly for production builds so recipe logic that reads target URLs sees stable, deploy-intended origins.
Example:
qip router warc ./site --host https://qip.dev
Adding Routes #
WARC recipes can synthesize or rewrite archive records, which means they can add output routes (for example /sitemap.xml) when they emit additional WARC records.
- In this repo, route assets like
/favicon.ico and /robots.txt are present in the content/static output.
- WARC QIP components such as
components/application/warc/warc-to-sitemap.wasm show the pattern for deriving site-wide artifacts from the archive.
Ordering #
- Recipe execution order is determined by a required two-digit prefix.
- Prefix range is
00 to 99.
- Lower number runs first.
Filename format:
NN-name.wasm
NN is two ASCII digits.
name is ASCII-only.
Disabled filename format:
-NN-name.wasm
- Leading
- means the recipe is disabled and must be ignored.
- Example:
-10-normalize.wasm
Examples:
10-normalize.wasm
20-markdown-render.wasm
90-html-wrap.wasm
-10-normalize.wasm (disabled)
Tie-Breaking #
- Primary sort: numeric prefix ascending.
- Secondary sort: full filename lexicographic ascending.
Validation #
Host should reject recipe entries if:
- filename is non-ASCII
- filename does not match either
NN-name.wasm or -NN-name.wasm
Host should ignore non-.wasm files in the recipes tree.
Scope #
- This contract only defines recipe discovery and order.
- Which MIME type applies to a content file is determined by routing/build logic.
- Nested
_recipes directories are reserved for future path-scoped recipes and are not active in this version.