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

5.9 KiB

name description
developing-blockninja-plugins 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 (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.

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

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

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.