Inky Example Suite: Node.js
July 13, 2026 · View on GitHub
Ten runnable, numbered examples showing how to use the Inky email framework from Node.js — from the smallest possible transform up to a transactional-email capstone and a nunjucks/Total-CMS-style integration.
This is the Node.js port of the PHP reference suite
(see its SUITE.md for the full,
language-neutral porting spec that both suites implement against). The same
ten examples, against the same required output markers, exist here.
Requires Inky v2. See the main inky repo.
The Node exception — read this first
The WASM/Node binding has no build. inky's WASM build
(bindings/node) runs in a sandboxed WASM runtime with no filesystem
access, so it cannot resolve <layout src="...">, <include src="...">,
or <link href="*.scss"> — those all require reading files relative to a
base_path, which only build needs. validate and
migrate/migrate_with_details take a template string directly and never
touch disk, so they're fully available in WASM — the gap is build
alone. What the WASM binding DOES export directly (see
bindings/node/inky.d.ts): transform, transform_with_config,
transform_inline, transform_with_data, transform_hybrid,
to_plain_text, migrate, migrate_with_details, validate,
validate_with_config, version — all filesystem-free, single-call
component/text transforms.
So this port uses two driver paths, chosen per example:
- WASM binding directly (
lib/wasm.mjs) — examples 01, 06, 07. These never needed a layout: 06'svalidateand 07'smigrate/transformInlineoperate on a legacy template and its migrated form that are bare components with no filesystem refs, and neithervalidatenormigrateneedsbuild's filesystem resolution in the first place — exactly the gap above, and exactly why these two land on WASM directly instead of the CLI. inkyCLI subprocess (lib/cli.mjs) — examples 02, 03, 04, 05, 08, 09, 10. These need real<layout>/<include>/<link href="*.scss">resolution (02, 03, 04, 05, 08, and both capstones'emails/trees), or (08 specifically) an option the WASM binding doesn't expose at all (bulletproof_buttons—transform_hybrid()takes no such parameter). Every such example shells out to a realinky build/validate/migrateinvocation rather than faking the API surface, and says so in its own header comment.
Exact command used (see lib/cli.mjs): the prebuilt release binary at
../inky/target/release/inky, if present; otherwise falls back to
cargo run -q -p inky-cli --release --manifest-path ../inky/Cargo.toml --.
Both produce identical output — the fallback just costs a compile the
first time.
The suite never loosens or reinterprets SUITE.md's required output
markers to make them CLI-shaped: the markers are about the resulting files
in dist/NN-name/, and a CLI invocation produces the same files a library
call would, because both call the same inky-core pipeline underneath.
Requirements
- Node.js >= 18
- A Rust toolchain, to build
libinky's CLI and WASM artifacts:
(this repo must be checked out as a sibling ofcd ../inky cargo build -p inky-cli --release # target/release/inky, used by lib/cli.mjs cd bindings/node wasm-pack build ../../crates/inky-wasm --target nodejs --out-dir . --out-name inkyinky/—lib/wasm.mjsandlib/cli.mjsboth resolve../inky/...relative paths) nunjucks(installed vianpm install) — used only by example 10, as the closest Jinja-family templating engine to Twig available in the Node ecosystem (Total CMS's own engine is Twig; there's no Twig-for-Node)
60-second quick start
cd ../inky && cargo build -p inky-cli --release
cd bindings/node && wasm-pack build ../../crates/inky-wasm --target nodejs --out-dir . --out-name inky
cd ../../../inky-example-node
npm install
npm run examples # runs every examples/*/run.mjs, writes dist/
npm run verify # same, plus greps every output for its required markers
npm run verify prints ok for all ten examples. Output lands in
dist/NN-name/ (gitignored).
The ten examples
| # | Name | Teaches | Path |
|---|---|---|---|
| 01-quickstart | quickstart | The smallest possible thing Inky does: transform() turns a <button> into table markup, no layout or data involved. | WASM |
| 02-build-pipeline | build-pipeline | The full build pipeline in one call: shared layout + includes + a linked SCSS theme, all resolved and inlined. | CLI |
| 03-data-merge | data-merge | Merging JSON data into a template: variables, a conditional, and a loop rendered as real <tr> rows. | CLI (--data) |
| 04-theming | theming | Building the identical template twice with a different linked SCSS theme each time. | CLI (stdin + cwd) |
| 05-plain-text | plain-text | Deriving a plain-text alternative alongside the HTML for multipart transactional email. | CLI (--plain-text) |
| 06-validate-gate | validate-gate | Using validate() as a pre-send CI gate: block on errors, let warnings through. | WASM |
| 07-migrate | migrate | Upgrading a v1 Inky template to v2 syntax programmatically, with a reviewable change report. | WASM (migrateWithDetails + transformInline) |
| 08-outlook-hybrid | outlook-hybrid | Building for Outlook desktop: hybrid column layout, bulletproof VML buttons, <outlook>/<not-outlook> branching. | CLI |
| 09-transactional | transactional (capstone) | A real three-email transactional set (welcome, receipt, password reset) built through EmailRenderer, a small production-shaped service class. | CLI (via lib/email-renderer.mjs) |
| 10-twig-cms | twig-cms | Integrating Inky into a Jinja-family-templated CMS (nunjucks standing in for Twig): both valid processing orders, timed, plus the one <raw> rule that makes the fast path safe. | CLI + nunjucks |
Run any single example directly, e.g. node examples/03-data-merge/run.mjs
— every run.mjs is a self-contained, top-to-bottom tutorial with comments
at each decision point.
For Total CMS / CMS integrators
If you're wiring Inky into a Jinja-family-templated CMS (Total CMS's own
engine is Twig; this port substitutes nunjucks since there's no Node Twig
binding), start at 09-transactional to see the
production-shaped EmailRenderer wrapper (lib/email-renderer.mjs), then
read 10-twig-cms for the CMS-specific question:
should the templating engine or Inky run first? Both orders are
implemented, timed, and proven to agree — see that example's run.mjs and
emails/newsletter.inky.njk header comment for the full trade-off,
including the one <raw>-plus---no-inline-css rule that makes the
faster, build-once-per-template order safe.
Layout
package.json scripts: examples / verify / send; nunjucks dependency
bootstrap.mjs dist-dir helper (inkyExample()) shared by every run.mjs
lib/wasm.mjs thin wrapper around the WASM binding (../inky/bindings/node)
lib/cli.mjs subprocess wrapper around the inky CLI (build/validate/migrate)
lib/email-renderer.mjs EmailRenderer — Node port of src/EmailRenderer.php (example 09)
shared/ brand layout, includes, SCSS themes used by examples 02-05 and 08
examples/NN-name/ one directory per example: run.mjs (tutorial) + verify.mjs (smoke test)
dist/ build output (generated, gitignored)
run-all.mjs runs every example (npm run examples / verify)
send.mjs multipart send demo reading example 05's output
Examples 09 and 10 each ship their own self-contained emails/ tree
(emails/layouts/, emails/themes/, emails/includes/) instead of
referencing shared/ — see SUITE.md's "two fixture conventions" for why.
Porting deviations from the PHP reference (report these, don't hide them)
- 07-migrate's "build the migrated template" step uses WASM's
transformInline()instead of shelling out to the CLI, sincelegacy-v1.inky(and its migrated v2 form) never references a shared layout/theme — proving it "still builds cleanly" needs no filesystem access here, unlike the general case. - 08-outlook-hybrid uses the CLI even though it sits in the same
"05–08" grouping the porting brief associated with the WASM path — two
independent reasons: it needs the shared layout/theme (filesystem), and
bulletproof_buttonshas no WASM-exposed equivalent at all (transform_hybrid()takes no such parameter). See that example'srun.mjsheader comment. - 10-twig-cms substitutes nunjucks for Twig (no Twig binding exists
for Node); the template file is
newsletter.inky.njk, notnewsletter.inky.twig. The lesson (Order A vs. Order B,<raw>as load-bearing rather than defense-in-depth,--no-inline-css) is unchanged — only the templating engine playing "the CMS's own engine" role differs. Nunjucks' syntax for everything this template uses ({{ }}, {% if %}, {% for %},{%- -%}whitespace control,{# #}comments, custom filters) is Jinja-compatible with Twig, so the template reads the same either way.
Documentation
../inky-example-php/SUITE.md— the language-neutral porting spec (the Stage C contract) this suite implements- Getting Started
- Component Reference
- Language Bindings