Alex Dunmow b23e0219b7 developing-blockninja-plugins: add html-blocks guidance
Agents default to registering a custom block type (BlockMeta + Go render func +
Module Federation editor) for section/content blocks. RED baseline confirmed it.
Add html-blocks.md: when to use the built-in html block vs a custom block,
pre-rendered vs live render modes, driving nav/footer from editable menus, and
the master-page reconcile / publish / ReconcileBlocks gotchas. SKILL.md gains a
routing row + a decision callout so the html-block path is seen before an agent
reaches for a custom block. GREEN-verified: fresh agents now pick the html block
(with reasoning) on both a clear case and the nuanced field-form case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 09:53:38 +08:00

79 lines
5.9 KiB
Markdown

---
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.
---
# Developing BlockNinja Plugins
## Overview
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/...`
- **Canonical guide:** `~/src/blockninja/cms/docs/PLUGIN_DEVELOPMENT.md`
- **Publish workflow + hard rules:** `~/src/blockninja/plugins/CLAUDE.md`
- **Exemplars:** `plugins/messenger` (compact), `plugins/symposium` (service-heavy: RPC, jobs, AI, embeddings)
Core-vs-plugin: platform-wide behavior → core; domain-specific or owns its own data/UI → plugin (decision table in PLUGIN_DEVELOPMENT.md).
## Doc routing
| Working on | Read first (under `cms/docs/`) |
|---|---|
| Scaffold, registration, custom block types, `CoreServices` | 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 |
| Depositing media into the library (seed `EnsureMedia` / runtime `CoreServices.Media`) | PLUGIN_DEVELOPMENT.md §Depositing Media |
| Plugin migrations / DB tables — each plugin owns a Postgres schema (named after it), never `public` | PLUGIN_DEVELOPMENT.md §Migrations |
| **Platform data tables** — provision via `Provisioner.EnsureDataTable` (`RegisterWithProvisioner`); the row-as-JSONB store the admin Data Platform / buckets / **data Views** read, idempotent + **View-rootable**. NOT the plugin's own Postgres schema above | PLUGIN_DEVELOPMENT.md §Provisioning → Data tables |
| Public HTTP routes (webhooks, widgets) | PLUGIN_HTTP_HANDLERS.md |
| Load/Unload, runtime state, goroutines | PLUGIN_LIFECYCLE_HOOKS.md |
| Themes, templates, master pages, CSS | TEMPLATE_PLUGINS.md |
| Theme preview screenshots (showcase preset, gallery instances, `ninja theme screenshot`, registry `previewImageUrl`) | theme-previews.md |
| Block editor / settings UI (Module Federation) | PLUGIN_EDITOR_SDK.md |
| .so build pipeline, loader internals | compiled-plugin-architecture.md |
| Release, registry, install | `plugins/CLAUDE.md` (not cms/docs) |
## Blocks: prefer the built-in `html` block
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
block. Most sections (hero, features, nav, footer, CTA, legal) are built-in **`html`** blocks: a
pongo2 template rendered into `_html_content`, `BlockKey: "html"`, zero Go render code and zero
custom editor. Live `html` blocks even drive admin-editable **menus** and use `context.*`. Register a
custom block only when you need a structured field-form editor or render logic a template can't
express. Custom keys also risk red `block-fallback` boxes when a key isn't registered; built-in
`html` never does. Decision guide, render modes, menu wiring, and gotchas: **[html-blocks.md](html-blocks.md)**.
## Verifying SDK symbols
Before using an unfamiliar SDK call, check: (1) the pinned SDK the build compiles against — `go doc git.dev.alexdunmow.com/block/core/plugin CoreServices` from the plugin dir; (2) nearest exemplar usage in messenger/symposium; (3) the CMS-side implementation in `cms/backend` for semantics.
**Missing capability** ⇒ two sanctioned paths only: extend `block/core` (long-term; needs SDK release + re-pin + image rebuild), or vendor the cms-internal package into the plugin's `internal/` with a provenance header (established convention — check-safety vendors cms `internal/theme` this way). NEVER `replace` directives; NEVER `block/cms` imports.
**Version pinning:** `go.mod` pins `block/core` to exactly what the CMS uses:
`grep 'block/core ' ~/src/blockninja/cms/backend/go.mod`
## Gates — run before commit / bump / publish
```bash
make # CGO build; templ/sqlc drift surfaces here
cd ~/src/blockninja/check-safety && go run . <plugin-path> # MUST exit 0
make archive-check # proves `git archive HEAD` (= what publish ships) compiles
```
check-safety traps: plugin `web/` lint extends `../../../cms/web/eslint.config.js`, so it only runs from the canonical sibling layout; raw `<button>` in plugin UI fails (use `@block-ninja/ui` Button); `any` usage warns.
## Release traps
```bash
ninja plugin bump patch # commits plugin.mod — does NOT git-tag
git tag vX.Y.Z # manual; must equal plugin.mod version (hard rule 6)
git push origin main vX.Y.Z # explicit — --follow-tags skips lightweight tags
ninja plugin publish # ships `git archive HEAD`: untracked files DON'T ship;
# web/dist and ALL generated Go must be committed
```
**`kind` is frozen at first publish.** The registry stores `kind` (`plugin`|`theme`) on the first `CreatePlugin` and NEVER updates it; `publish` only *compares* plugin.mod `kind` against that frozen DB column and rejects a mismatch with `plugin.mod kind does not match registered kind`. A theme MUST declare `kind = "theme"` BEFORE its first publish — editing plugin.mod afterwards is not enough (no RPC re-aligns it). Remedy for an already-misregistered theme (dev only, no supported RPC): `UPDATE registry_plugins SET kind='theme' WHERE id=<row>` on `orchestrator-db` (preserves version history), then republish. gotham/lcars first shipped `kind=plugin`; lcars corrected this way 2026-06.
New plugin: `ninja plugin init`, minimal files per PLUGIN_DEVELOPMENT.md §Minimal Layout, copy the Makefile from messenger. First publish lands `private`; review via orchestrator dashboard.