Status: all five steps shipped (v0.4.0 cycle) — code language attribute (plus a hover/caret
⚙ chip popout on editable code blocks), the inspector for attribute/dynamic islands with
server-rendered previews (minn-admin/v1/render-blocks replaced the per-block
block-renderer idea — it renders whole islands including static parents with dynamic
children), one-level child editing, add/remove/reorder children (gated on the "structural"
InnerBlocks shape: wrapper HTML preserved verbatim, whitespace-only between children), and
the minn_admin_block_forms filter with wrapperText — which delivered the useful subset of
"parent editing": declared text inside a wrapper (e.g. the conversation header) is editable
via a three-capture-group pattern, replaced only when changed. Adapters ship inside the
integrated plugin (Anchor Blocks app/MinnAdmin.php is the reference), not bundled in Minn.
Full parent-attribute regeneration for static InnerBlocks wrappers remains deliberately
unbuilt — it requires the block's JS save() and stays on the wrong side of the parity
treadmill.
Two later additions (v0.5.x cycle): the inspector on a core/embed / core/gallery
island offers "Change URL…" / "Replace images…" — these regenerate the island wholesale
through the same templates that create embeds and galleries (embedTemplate() /
galleryTemplate()), because their content lives in saved HTML where attribute edits alone
can't retarget it. Gutenberg-set captions/layout attributes reset by design; the popover
note says so. And island previews now render with the site's real front-end styles —
minn-admin/v1/editor-styles collects block style handles + theme editor styles + the
global stylesheet, and the client scopes every rule to .minn-island-preview before
injecting (see editor-direction.md, "The writing surface").
Post-v0.8.0 addition (auto-registered inserts): dynamic third-party blocks no longer
need an adapter to be insertable. Minn_Admin::insertable_blocks() walks the block-type
registry at boot (dynamic + top-level + inserter-visible + non-core; adapter insert
descriptors supersede, insert => false suppresses, minn_admin_insert_blocks filters the
result) and the slash menu carries them as search-only entries: the default list stays
curated, and typing matches title or namespace. A self-closing comment is valid saved
markup only when the block's JS save() is null — is_dynamic is NOT that guarantee
(stackable/posts pairs a render_callback with a JS save that emits wrapper HTML; a bare
comment renders empty AND fails Gutenberg validation). The server can't see JS save(), so
the shipped discriminator is a render probe: candidates whose bare comment renders
nothing are excluded (cached in a transient keyed on the candidate set). Static-save blocks
remain excluded per "The honest limit" below — but see adapters/stackable.php for the
scrape-the-data counter-move: when a plugin publishes its designs as serialized markup
(Stackable's CDN design library), those templates insert as valid islands with no runtime.
Generic text runs (same cycle): the honest limit got narrower. A static block's
STRUCTURE and STYLING still can't be regenerated outside Gutenberg, but its TEXT is just
text nodes in the saved HTML — so the inspector scans an island's raw markup by offset
(textRunsOf: block comments, tags and style/script/svg subtrees skipped) and renders one
field per text node, however deep the nesting and even for blocks with no registered
attributes. Changed runs are spliced back last-to-first with text-node escaping only;
untouched runs are never rewritten, preserving byte-identity. Gutenberg stays happy because
these blocks SOURCE their text attributes from that same HTML. Children already served by
the single-element text editor (childTextOf) keep it — the two paths are mutually
exclusive per child. Etch is the exception that lives in the comment: wp:etch/text
stores copy as a content JSON attribute (render reads that, not inner HTML), so a
sibling scan (etchTextRunsOf) arms those strings as in-place preview runs and splices
the quoted JSON value with Gutenberg comment escaping.
Image swaps (same cycle): the sibling move for pictures. Static blocks mirror an
image's URL between comment JSON (imageUrl, blockBackgroundMediaUrl) and saved HTML
(img src, background-image style), so swapIslandImage() replaces the URL string
everywhere (plus its serializeAttributes-escaped form), retargets paired XxxUrl/XxxId
media ids scoped to the comment carrying the URL, and updates wp-image-N classes on
swapped img tags. The inspector's Images section lists every image in the island with a
Replace button into the media picker; pending field edits are folded into the raw first
(buildInspectorRaw, shared with Apply) so nothing typed is lost. Embed/gallery keep
their dedicated rebuild flows — the generic swap is suppressed there, since core image
comments carry a bare id the URL-key heuristic can't safely retarget.
Block islands made complex content safe (see editor-direction.md) — this is the plan for making them workable. The goal: click an island, get a small inspector popover next to it, edit the block's configuration in place, watch the preview update. No React, no build step, no change to the safety model.
The key realization: for server-registered blocks, everything the inspector needs already exists as core REST endpoints — Minn drives the same server-side machinery Gutenberg does, minus the React shell.
| Need | Endpoint | Verified |
|---|---|---|
| What attributes does this block have? | GET wp/v2/block-types/<namespace>/<name> → full attribute schema with types and defaults |
✓ returns role / label / content for anchor/conversation-message |
| What does it look like with these attributes? | GET wp/v2/block-renderer/<name>?context=edit&attributes={…} → server-rendered HTML |
✓ renders anchor/stat-card with arbitrary attrs |
Self-closing dynamic blocks (<!-- wp:anchor/stat-card {"value":"85","label":"…"} /-->) are
pure attribute containers — no saved HTML exists to invalidate, so editing them is literally
rewriting JSON inside a comment. The island's raw markup is already stored verbatim and spliced
back on save; the inspector just modifies that stored string. Nothing about the byte-identity
model changes.
- Trigger — a ⚙ affordance on the island chip (and clicking the chip itself). Opens a popover anchored to the island, same visual family as the slash-command menu.
- Schema-driven form — fetch the block type once (cache per session), generate fields from
the attribute schema:
string→ input,boolean→ switch,number→ number input,enum→ select. Skiplock/metadata/style(Gutenberg plumbing). - Apply — rewrite the attributes JSON in the island's stored raw markup: for self-closing blocks, rebuild the whole comment; for wrapped dynamic blocks, rebuild only the opening comment and keep inner HTML untouched.
- Preview refresh — POST the new attrs to
block-rendererand swap the island preview. Blocks with norender_callbackkeep their static preview. - Serialize — unchanged. Islands still pass through verbatim from stored raw markup.
The conversation block is a static InnerBlocks wrapper whose children are self-closing dynamic
conversation-message blocks. tokenizeBlocks() already splits nested content, so the
inspector can list children and edit each one with the same schema-driven form — including
add / remove / reorder, since children are attribute-only comments. The parent's own wrapper
markup (<div class="wp-block-anchor-conversation"><div class="ab-conv-header">…</div>) is
reproducible but is saved by the block's JS save(), so regenerating it risks Gutenberg's
block validation — ship child editing first, parent-attribute editing last, behind
round-trip tests.
<!-- wp:code {"language":"sql"} --> islands today only because language isn't in
EDITABLE_ATTRS. Add code: [ 'language' ], map the attribute into the existing toolbar
language picker on load, re-emit attribute + language-* class on serialize. The block becomes
fully editable and the picker (built in v0.2.0) just works. Small, high value — the most common
island on real content.
Static third-party blocks — ones whose save() lives in their editor JS bundle — stay
verbatim islands. Changing their attributes requires regenerating HTML only Gutenberg's runtime
can produce; that's the parity treadmill editor-direction.md refuses to get on. They already
display and survive edits safely, and WordPress has pushed block development server-side for
years, so the reachable set keeps growing on its own.
Authors who want first-class Minn editing should follow the content-block contract in
content-blocks.md (dynamic leaves, PHP schema, optional
minn_admin_block_forms). That is how Anchor Blocks stay editable in Minn without Minn
becoming a layout runtime.
Schema types alone can't express intent: role is really a user/assistant enum, content
deserves a textarea, color is a fixed palette. A minn_admin_block_forms filter (same
declarative-descriptor pattern as surfaces and editor panels — see
for-plugin-authors.md) lets a plugin refine the generated form for its
own blocks: labels, control types, option lists, field order. Anchor Blocks becomes the
reference adapter, the same role ACF plays for editor panels.
code.languageviaEDITABLE_ATTRS— small.- Inspector for attribute-only / dynamic islands (stat-card, callout, timeline-item…) — the core build: schema fetch, form generation, comment rewrite, renderer preview.
- Child-block editing inside InnerBlocks islands (conversation messages).
minn_admin_block_formsfilter + Anchor Blocks descriptors.- (Maybe, last) parent-attribute editing for simple InnerBlocks wrappers, gated on proven round-trips.