From cbc598f5d5795bc2e790013142350e0ce7ee475d Mon Sep 17 00:00:00 2001 From: Alex Dunmow Date: Sun, 5 Jul 2026 15:36:43 +0800 Subject: [PATCH] developing-blockninja-plugins: add theme-overrides reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codeless theme override authoring learned from the theme-fleet build: - template override mechanism (templates/overrides + manifest template_overrides) - override dispatch model (definition-backed → providers via RenderDefinition; compiled auth/404 built-ins via GetForTemplate) - legacy field renames (button href→link etc.), ninjatpl engine gotchas - presets/fonts/email/motion rules, seed single-segment page-slug constraint - git -C commit-hook trap, check-safety gate - screenshot/preview pipeline (local ImageExists provisioning, showcase recipe) Co-Authored-By: Claude Fable 5 --- developing-blockninja-plugins/SKILL.md | 143 ++++++++++--- .../theme-overrides.md | 190 ++++++++++++++++++ 2 files changed, 309 insertions(+), 24 deletions(-) create mode 100644 developing-blockninja-plugins/theme-overrides.md diff --git a/developing-blockninja-plugins/SKILL.md b/developing-blockninja-plugins/SKILL.md index 4c31493..49d47a6 100644 --- a/developing-blockninja-plugins/SKILL.md +++ b/developing-blockninja-plugins/SKILL.md @@ -1,6 +1,6 @@ --- name: developing-blockninja-plugins -description: Use when creating, modifying, building, or publishing BlockNinja CMS plugins or themes — work in plugins/* repos, plugin registration, blocks, templates, plugin Connect services, migrations, ninja plugin commands, check-safety for plugins, or block/core SDK usage in standalone plugin repos. +description: Use when creating, modifying, building, or publishing BlockNinja CMS plugins or themes — work in plugins/* or sites/* repos, codeless .bnp artifacts (blocks.yaml, seed.json, manifest.yaml), plugin registration, blocks, templates, plugin Connect services, migrations, ninja plugin commands, check-safety for plugins, or block/core SDK usage in standalone plugin repos. --- # Developing BlockNinja Plugins @@ -16,16 +16,44 @@ description: Use when creating, modifying, building, or publishing BlockNinja CM > compile. A registry version published as a source archive is **rejected** at install > ("legacy source archive; republish as .bnp"). +**The ABI is the capability model; the Go SDK is only its ergonomic front-end.** +Since WO-WZ-019, every host interaction is reachable purely over the ABI — a +manifest declaration, a `blockninja.host_call` method, or a host-invoked hook — +so a plugin in any language gets 100% of the surface; linking Go `core` is a +convenience, not a dependency of the model. The canonical plugin-need → ABI- +mechanism map is `~/src/blockninja/core/docs/abi-capability-surface.md`. + Never write plugin code from memory — every SDK symbol is locally verifiable. Truth lives at: -- **SDK source:** `~/src/blockninja/core` — import prefix `git.dev.alexdunmow.com/block/core/...` is the ONLY one allowed; never `block/cms/...` -- **ABI contract (authoritative):** `~/src/blockninja/core/docs/wasm-abi.md` — reactor mode, hook catalog, capability dispositions, error codes, `plugin.mod` `data_dir` +- **SDK source:** `~/src/blockninja/core` — for WASM plugins, import prefix `git.dev.alexdunmow.com/block/core/...` is the ONLY one allowed; never `block/cms/...`. Codeless plugins import NOTHING (no Go at all). +- **ABI contract (authoritative):** `~/src/blockninja/core/docs/wasm-abi.md` — reactor mode, hook catalog, capability dispositions, error codes, `plugin.mod` `data_dir`; capability matrix: `core/docs/abi-capability-surface.md` - **Architecture spec:** `~/src/blockninja/cms/docs/superpowers/specs/2026-07-03-wasm-plugin-migration-design.md` - **Canonical guide (registration concepts, blocks, provisioning):** `~/src/blockninja/cms/docs/PLUGIN_DEVELOPMENT.md` - **Exemplars:** `plugins/testplugin` — THE reference wasm plugin, every surface + failure endpoints (see its README surface table); `plugins/symposium` — the largest real port (service-heavy: RPC, jobs, AI, templ blocks; its Makefile is the build/publish convention) Core-vs-plugin: platform-wide behavior → core; domain-specific or owns its own data/UI → plugin (decision table in PLUGIN_DEVELOPMENT.md). +**Codeless plugins (WO-WZ-020):** a repo with NO Go source builds a codeless +`.bnp` — blocks as `blocks/blocks.yaml` definitions (host-rendered via data +providers), `seed/seed.json`, `manifest.yaml` for presets/fonts/master pages. +No wasm, no `block/core` dependency at all. Prefer this form for themes, +content sites, and template-only block packs; write Go only for genuine logic +(services, jobs, computing tags, custom fetch). Contract: +`core/docs/codeless-bnp.md`. + +## Choose the artifact kind FIRST: codeless vs wasm + +`ninja plugin build` classifies by repo shape: **no Go source → codeless** +(declarative `.bnp`, no `plugin.wasm`); Go source → wasm. Start every new +plugin/theme by asking whether it needs code AT ALL — themes, content sites, +and template-only block packs should be codeless (blocks as `blocks.yaml` +definitions + providers, `seed/seed.json`, `manifest.yaml`; contract: +`core/docs/codeless-bnp.md`). Write Go only for genuine logic: Connect +services, jobs, computing tags/filters, custom fetch, HTTP handlers, media +hooks, RAG fetchers. `--codeless` asserts the expectation. Mixed form is +legal: a wasm plugin may ALSO ship `blocks/` + `seed/` and keep Go only for +its logic (the WZ-021 "reduced wasm" target). + ## Anatomy of a wasm plugin `PluginRegistration` is **unchanged** from the .so era — same fields, same `Register`, @@ -71,7 +99,29 @@ named error at build time. Load-time work belongs in `Load`. | Settings schema, theme preset (go:embed'd JSON) | `assets/` | | `data_dir = true` grant + `/data` round-trip | `plugin.mod`, `onLoad` | -## Blocks: prefer the built-in `html` block +## Blocks: prefer declarative forms — definitions first, then `html`, custom Go LAST + +For a plugin-owned block type, the preferred form is a **block definition**: +an entry in `blocks/blocks.yaml` (schema + `.ninjatpl` template + declared +data providers — `posts`, `site`, `menus`, `authors`, …), rendered host-side +by the definition engine with layer fallback. Zero Go, works in codeless AND +wasm artifacts, admin-forkable. Register a Go `BlockFunc` only when no +declared provider can produce the data (then return `blocks.PoweredBlock` so +the template still renders host-side; final-HTML string building is the last +resort). + +For one-off page sections on a SITE (not a reusable type), the built-in +`html` block below still applies. + +**Themes re-skin built-ins via template OVERRIDES, not new blocks.** A codeless theme ships +`templates/overrides//.ninjatpl` (+ a `template_overrides` entry in +`manifest.yaml`) for each built-in it restyles, plus its own page templates, presets, fonts, +email wrapper, and demo seed. The override dispatch model (definition-backed built-ins render +WITH their providers' data via `RenderDefinition`; the four compiled auth/404 built-ins +dispatch via the compiled path), the legacy field renames (`button` `href`→`link` etc.), the +full **ninjatpl engine gotcha list**, seed page-slug rules, the `git -C` commit-hook trap, and +the screenshot/preview pipeline are all in **[theme-overrides.md](theme-overrides.md)** — read +it BEFORE writing any theme override or `.ninjatpl`. The default reflex — register a custom block type (`blocks.BlockMeta` + a `func(ctx, content) string` render func + a Module Federation editor) — is usually the wrong altitude for a section/content @@ -117,12 +167,32 @@ marshaling to host functions. Full family/method table + disposition of every - `Interceptors`, `CoreServiceBindings` — **host-side**; RBAC method roles merge from the manifest, auth context arrives via forwarded HTTP headers (host interceptors already ran). - `AppURL`/`MediaPath` — delivered once in `LoadRequest.host_config`. -- `Bridge.GetService` reports availability but returns `nil` (typed values can't cross — - open item); `RegisterService` forwards names only. +- **`deps.Provisioner` (core ≥ v0.17.x) is the seeding path** — `EnsurePage`, + `EnsureMenuItem`, `EnsureMedia`, `EnsureSetting`, `MergeSiteSettings`, data + tables, embeds, job schedules, custom colors — call it from **`Load`**. + **`RegisterWithProvisioner` is INERT under wasm**: Register runs at DESCRIBE + where host functions are stubbed, so its provisioner is a no-op — seed logic + living there silently does nothing. Move it to `Load`. (`EnsureEmbed` crosses + template embeds only; a `RenderFunc` embed errors — functions can't serialize.) +- **`deps.ContentAuthor`** — imperative authoring: `CreatePage`, `SetPageBlocks`, + `PublishPage`, `SetPageSEO`, `UpsertPost` (blog_posts table). +- `Bridge.GetService` still returns `nil` across the sandbox (typed values can't + cross — by design), but **cross-plugin calls now work via `Bridge.Invoke`** + (opaque payloads, JSON by convention): the provider's service value implements + `plugin.BridgeInvokable` and answers over `HOOK_BRIDGE_CALL`. +- **AI tools execute**: `deps.ToolRegistry.Register` handlers run via + `HOOK_AI_TOOL_CALL`. Register tools in `Register` (see per-instance state) or + they exist on one pooled instance only. +- **Job progress crosses**: `JobHandlerFunc`'s `progress(current, total, msg)` + reaches the job system as `jobs.progress` (correlated via the call context). +- **Directory extensions work**: panel sections / pin decorators are invoked via + their own hooks from the manifest-captured counts. +- `SettingsUpdater.UpdatePluginSettings` writes the plugin's OWN settings (the + host pins the name to the caller; another plugin's name is rejected). - `RAGService.RegisterContentFetcher` — records guest-side + a manifest declaration; the host calls back via `HOOK_RAG_FETCH`. -- Methods without an error channel (`Slugify`, `EvaluateAccess`, `Bridge.*`, …) degrade to - zero values on transport failure. +- Methods without an error channel (`Slugify`, `EvaluateAccess`, `Bridge.RegisterService`, …) + degrade to zero values on transport failure. ### Per-instance state (the #1 wasm gotcha) @@ -138,6 +208,11 @@ others. Two sanctioned patterns (both in testplugin/symposium): **nil-check it**: in native/DESCRIBE builds it's the zero value. See symposium `register.go`. +The same rule governs the callback hooks: **AI tool handlers and bridge service +values must be registered in `Register`** (via `wasmguest.HostServices()`), not +`Load` — `HOOK_AI_TOOL_CALL` / `HOOK_BRIDGE_CALL` can land on ANY pooled +instance, and only Register runs on all of them. + ## Sandbox constraints — write reload-safe code - **No filesystem, no network, no env** in the guest. One opt-in exception: `data_dir = true` @@ -170,13 +245,27 @@ cd ~/src/blockninja/check-safety && go run . # MUST exit 0 (unch ninja plugin bump patch # commits plugin.mod — does NOT git-tag git tag vX.Y.Z && git push origin main vX.Y.Z # --follow-tags skips lightweight tags -ninja plugin build --dir . # rebuild at the bumped version -ninja plugin publish --bnp -.bnp # --bnp is REQUIRED: without it publish - # ships a source archive, which instances REJECT +ninja plugin publish # builds the .bnp itself at the bumped version and + # uploads it — the .bnp is the ONLY publish form + # (--bnp ships a prebuilt artifact instead) ``` -The `.bnp` (tar.zst) packs `plugin.wasm`, `plugin.mod`, `manifest.pb`, plus `migrations/`, -`schemas/`, `assets/`, and `web/dist` (flattened under `web/`) when present. Statics are +The `.bnp` (tar.zst) packs `plugin.mod` + `manifest.pb`, plus — when present — +`blocks/` (definition manifest), `templates/`, `seed/`, `migrations/`, +`schemas/`, `assets/`, and `web/dist` (flattened under `web/`); wasm artifacts +add `plugin.wasm`, codeless ones don't (`manifest.codeless` is set, and +verify/reader reject a mismatch in either direction). Bundling is a **fixed +allowlist** (`cli/internal/bnp/build.go`), not config or git: each optional dir +ships only if it exists with ≥1 regular file (symlinks skipped), read straight +from the working directory — an untracked file under `assets/` ships; Go +source, Makefile, README, or anything outside the listed dirs never does +(README/CHANGELOG reach the registry as publish *metadata*, not artifact +contents). `manifest.pb` is never read from disk: wasm builds DESCRIBE-probe +the module, then plugin.mod's `data_dir` and any root `manifest.yaml` +declarative surfaces are folded in, with referenced JSON (presets.json etc.) +embedded INTO manifest.pb rather than packed as files. Packing is +deterministic (sorted entries, fixed modes) — same tree, byte-identical +artifact. Statics are consumed host-side: Goose runs migrations, assets are served directly, and the Module Federation admin bundle is served from the artifact at `/plugins//` — **no longer go:embed'd**, but `web/dist` must be built (`cd web && pnpm run build`) before @@ -186,7 +275,7 @@ proves `git archive HEAD` wasm-compiles). Generated Go (`*_templ.go`, `db/*.sql. `*connect.go`) and `web/dist` stay committed; `.gitignore` adds `*.bnp` and `*.wasm`. **Dev loop:** publish to the DEV orchestrator (`ninja --host -https://my.localdev.blockninjacms.com plugin publish --bnp …` — `my.blockninjacms.com` +https://my.localdev.blockninjacms.com plugin publish` — `my.blockninjacms.com` without `localdev` is PRODUCTION). Install via Admin → Plugins → Browse Registry (`InstallFromRegistry`): the instance downloads the `.bnp`, checksum-verifies, and **hot-loads without restart**; updates hot-swap the same way. First publish lands @@ -201,10 +290,13 @@ capability disposition table in `core/docs/wasm-abi.md` (does it cross the ABI, (3) nearest exemplar usage in testplugin/symposium; (4) the CMS-side implementation in `cms/backend` for semantics. -**Missing capability** ⇒ two sanctioned paths only: extend `block/core` (guest stub + host -function + ABI proto — additive within ABI major 1), or vendor the cms-internal package -into the plugin's `internal/` with a provenance header. NEVER `replace` directives; NEVER -`block/cms` imports. Remember the wasm boundary: a new capability that returns +**Missing capability** ⇒ first check `core/docs/abi-capability-surface.md` — the surface +is complete for every known plugin need since WO-WZ-019. A genuinely new capability is an +**ABI extension** (manifest field, `host_call` method pair, or hook — additive within ABI +major 1), landed in the ABI proto + cms host first; the Go guest stub is then the +ergonomic wrapper, not the capability itself. Alternatively vendor the cms-internal +package into the plugin's `internal/` with a provenance header. NEVER `replace` +directives; NEVER `block/cms` imports. Remember the wasm boundary: a value that carries functions/handlers can't cross — it needs a hook or a manifest declaration instead. ## Doc routing @@ -212,8 +304,10 @@ functions/handlers can't cross — it needs a hook or a manifest declaration ins | Working on | Read first | |---|---| | ABI, hooks, capabilities, error codes, `data_dir`, build/pack internals | `core/docs/wasm-abi.md` | +| Which ABI mechanism serves a plugin need (matrix; codeless equivalents) | `core/docs/abi-capability-surface.md` | | Scaffold, registration, custom block types, `CoreServices` concepts | `cms/docs/PLUGIN_DEVELOPMENT.md` | | **Section/content blocks — html block vs custom; menus; live vs pre-render** | **[html-blocks.md](html-blocks.md)** (in this skill) — read BEFORE writing a custom block type | +| **Codeless THEMES — template overrides, ninjatpl gotchas, override dispatch, seed slugs, screenshots** | **[theme-overrides.md](theme-overrides.md)** (in this skill) — read BEFORE writing any theme override or `.ninjatpl` | | Depositing media (seed `EnsureMedia` / runtime `CoreServices.Media`) | PLUGIN_DEVELOPMENT.md §Depositing Media | | Plugin migrations / DB tables — own schema, per-plugin role | PLUGIN_DEVELOPMENT.md §Migrations + this skill §Database | | Platform data tables (`Provisioner.EnsureDataTable`, View-rootable JSONB store) | PLUGIN_DEVELOPMENT.md §Provisioning → Data tables | @@ -230,16 +324,17 @@ compiles, go:embed'd web bundles, or core-version skew. ## Release traps -- `ninja plugin publish` **without `--bnp` ships a source archive** — the registry accepts - it but every instance install fails with "legacy source archive; republish as .bnp". - Always build first and pass `--bnp` (or `make publish`). +- `ninja plugin publish` **builds the .bnp itself and ships it** — the source-archive + path is gone (CLI feeb311). A wasm plugin whose Go `Registration.Version` drifts from + plugin.mod fails the publish ("manifest version != plugin.mod version") — sync the + constant when bumping. `--bnp ` ships a prebuilt artifact instead of building. - **`kind` is frozen at first publish.** The registry stores `kind` (`plugin`|`theme`) on first `CreatePlugin` and never updates it; publish only compares. A theme MUST declare `kind = "theme"` BEFORE its first publish. Dev-only remedy for a misregistered row: `UPDATE registry_plugins SET kind='theme'` on `orchestrator-db`, then republish. -- `ninja plugin publish` ships metadata from `git HEAD` (README/CHANGELOG, warnings on - dirty tree) and the artifact from `--bnp` — keep the tree committed at the bumped - version so the two agree; `make archive-check` proves HEAD compiles. +- `ninja plugin publish` builds the artifact from the **working directory** and warns on + a dirty tree (`--strict` aborts) — keep the tree committed at the bumped version so + the artifact matches the tag; `make archive-check` proves HEAD compiles. - `ninja plugin init` is interactive and DROPS unknown `plugin.mod` keys on rewrite — re-check `plugin.mod` after running it (`data_dir` is a known key and survives). diff --git a/developing-blockninja-plugins/theme-overrides.md b/developing-blockninja-plugins/theme-overrides.md new file mode 100644 index 0000000..00d9586 --- /dev/null +++ b/developing-blockninja-plugins/theme-overrides.md @@ -0,0 +1,190 @@ +# Codeless themes: template overrides, the ninjatpl engine, seed, screenshots + +Read this before building or editing a codeless **theme** (a `.bnp` that re-skins the +built-in blocks and ships page templates, presets, fonts, an email wrapper, and demo +content). Distilled from the theme-fleet build (16 themes, 2026-07-05). Companion to +`cms/docs/TEMPLATE_PLUGINS.md` and `cms/docs/theme-previews.md`. + +## What a codeless theme is + +No Go, no `plugin.wasm`, **no `go.mod`, no `block_core` pin**. The repo is: + +``` +manifest.yaml # theme_presets, bundled_fonts, master_pages, system_templates, + # page_templates, template_overrides, css.input_css_append, email wrappers +plugin.mod # name/display_name/kind="theme"/scope="@themes"/categories/tags/required_icon_packs +templates//*.html # the page templates (default, full-width, landing, article, blog-index, contact, auth) +templates/overrides//*.ninjatpl # one per built-in block key you re-skin +templates/email/.ninjatpl # email wrapper +blocks/*.ninjatpl + *.schema.json # persona blocks ONLY (things no built-in covers) +presets.json fonts.json master_pages.json +seed/seed.json seed/workflows.json # demo content (demo:true) +assets/ # fonts, css, static +``` + +Compatibility is not pinned in the repo — it is enforced by manifest synthesis in +`ninja plugin build` and the registry gates. `kind = "theme"` is frozen at first publish. + +## Re-skinning a built-in block = a template override + +Ship `templates/overrides//.ninjatpl` **plus** a `template_overrides` entry in +`manifest.yaml` (`template_key: `, `block_key: `). Keys use the **dashed +canonical form** (`video-embed`, `feature-grid`, `author-bio-hero`). Some accept underscore +aliases, but overrides target the dashed key. + +A re-skin is **presentation only**. Copy the built-in's default template as your starting +point (`cms/backend/blocks/builtin/manifest/{blocks.yaml,schemas,sample}` for declarative +builtins; the compiled builtin's Go for the rest) and preserve **every** content-field read, +provider variable, custom tag (`{% img %}`, `{% button %}`, `{% form %}`, `{% signup_form %}`, +`{% auth_form %}`), and inline `