core/docs/codeless-bnp.md
Alex Dunmow 52d7413aa0 feat(bnp): codeless system/page templates — layouts without code (WO-WZ-020)
manifest.yaml gains system_templates + page_templates declarations; each page
template's .ninjatpl source lives at templates/<system>/<key>.ninjatpl and is
validated at pack time. Verify no longer rejects declared system/page
templates on codeless manifests (guest template_keys stay rejected) — the cms
host registers them source-tracked and renders the files through the same
host pongo pipeline powered blocks use. Blog/system/normal page layouts are
now fully expressible with zero code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 11:50:38 +08:00

4.8 KiB

Codeless .bnp — Declarative Plugins (WO-WZ-020)

A codeless .bnp is a plugin artifact with no plugin.wasm: pure declaration the host runs. No wazero compile, no instance pool, no capability binding — zero guest code ever executes. This is the end-state of "injection, not reliance" for themes, content sites, and template-only block packs: the plugin is data the host runs, not a program that links a library. No Go, no block/core dependency, no toolchain beyond the ninja CLI.

The classifier — exactly one rule

ninja plugin build classifies by repo shape:

  • No Go source at the repo root → codeless. The manifest is synthesized from plugin.mod + the declarative files below; the artifact packs without a wasm. --codeless asserts this and fails if Go is present.
  • Go source → wasm, exactly as before. A converted repo DELETES its Go — partial conversions keep a smaller wasm and still ship blocks/ + seed/ alongside it (both artifact kinds carry the declarative dirs).

The declarative / logic split

Codeless-expressible (declaration or host-rendered template):

  • Blocks: a .ninjatpl template + declared data providers (the CMS's own provider set — posts, site, menus, authors, … ADR 0018). The host fetches, the engine renders. Defined in blocks/blocks.yaml — the SAME manifest-FS layout core builtin block definitions use.
  • Master pages, theme presets, fonts, CSS manifest, icon packs, settings schema (manifest.yaml), assets (host-served), migrations (host-run Goose), seed data (seed/seed.json, applied via the WO-WZ-019 provisioner).

Requires code (a wasm plugin):

  • A block whose data no declared provider can produce; a computing tag or filter; HTTP handlers; background jobs; Load/Unload logic; RAG fetchers; media hooks; bridge services; AI tools; data_dir. A codeless manifest declaring any of these is rejected at build (ninja plugin verify / CodelessHookViolation) AND at load (cms reader codelessHookViolation).

Repo layout

my-theme/
  plugin.mod            # name/version/kind (data_dir forbidden)
  manifest.yaml         # optional: theme_presets/bundled_fonts/settings_schema
                        #   (JSON file refs), master_pages (JSON file),
                        #   required_icon_packs, css {...}, dependencies [...]
  blocks/
    blocks.yaml         # key/title/category/schema/template/providers per block
    hero.schema.json
    hero.ninjatpl
  templates/            # <system>/<key>.ninjatpl layout sources for the
                        #   system/page templates declared in manifest.yaml,
                        #   rendered host-side (blog/system/normal page layouts)
  seed/
    seed.json           # settings {merge/override/ensure}, media, pages, menu_items
    hero.jpg            # media bytes referenced by seed.json entries
  assets/               # host-served statics
  migrations/           # Goose SQL, host-run under the plugin schema

ninja plugin build<name>-<version>.bnp (tar.zst, no plugin.wasm, manifest.pb with codeless: true). ninja plugin verify re-runs the loader's checks standalone.

How the host runs it (cms plugin/codeless_loader.go)

  • The .bnp reader accepts a missing plugin.wasm iff manifest.codeless, and rejects codeless manifests with computing-hook declarations.
  • Load: blocks/blocks.LoadManifestRegistry.RegisterDefinition — rendering flows through the existing definition engine (providers + ninjatpl + layer fallback, WO-089..096). seed/seed.json → provisioner (EnsureMedia/MergeSiteSettings/EnsureSetting/EnsurePage/ EnsureMenuItem), idempotent by construction. Presets/fonts/CSS/master pages/settings schema come from the manifest exactly as for wasm plugins.
  • Unload/disable: definitions unregister; seeded content stays (it is site data, not runtime state). Uninstall: normal teardown (schema/role drop, artifact removal).
  • Hot-swap: codeless→codeless swaps run migrations first, then re-register definitions and re-apply seed. Wasm↔codeless transitions require uninstall + reinstall.

Seed schema (seed/seed.json)

{
  "settings": {"merge": {"k": "v"}, "override": {"k": "v"}, "ensure": {"k": "v"}},
  "media":    [{"id": "<uuid>", "file": "hero.jpg", "alt": "", "folder": ""}],
  "pages":    [{"slug": "/", "title": "Home", "template_key": "landing",
                "parent_slug": "", "reconcile_blocks": false,
                "blocks": [{"block_key": "hero", "title": "Hero",
                            "content": {}, "slot": "main", "sort_order": 1}]}],
  "menu_items": [{"menu": "main", "label": "Home", "page_slug": "/", "sort_order": 1}]
}

Media id is the deterministic, template-referable key (media:<uuid>, {% img %}); bytes live in seed/ next to the JSON. Apply order: media → settings → pages → menu items.