Implementation roadmap · revised 2026-05-11

Canvas — block-builder foundation plan.

Eighteen phases. Phase 1 (demolition + CPT bootstrap + PHP 8.2 bump) shipped 2026-05-11. License plumbing is Phase 18 and is always the last phase by convention. Each phase is independent enough that stopping between them leaves the product in a coherent state.

Phase 1 shipped Phase 2 (Inspector shell) is next Phase 5 — first end-to-end milestone

In one sentence#

Canvas is a block plugin where you compose new blocks in the standard Gutenberg editor (on our own custom post type), with universal Conditional Content and style controls injected into the Gutenberg Block Inspector — the existing right-side panel that already holds Colour, Typography, and Dimensions. Not a separate Plugin Sidebar, not a separate composer surface. Saving registers the composition as a real block type that appears in the Gutenberg inserter everywhere.

Phasing#

Phase numbers are stable across the document. Phase 1 (demolition + CPT bootstrap) is the only one that tidies; the rest build. Each phase is independent enough that stopping between them leaves the product in a coherent state. License plumbing is always the last phase by convention — when a new phase is added, it goes before license plumbing, never after.

01

Demolition + CPT bootstrap + PHP / WP baseline bump

Shipped 2026-05-11 · commit 6fbc0a8

(a) Delete Sandbox/SandboxApp.php, Sandbox/BridgeMessages.php, Sandbox/SandboxRoutes.php, Sandbox/NodeProvider/, assets/js/gc-sandbox-app.js, assets/js/gc-sandbox-frame.js, composer-specific CSS. (b) Lift the control components (BoxModelControl, ColorControl, OpacityControl, NumericControl, CollapsibleSection) into assets/js/gc-canvas-sidebar.js and its CSS — so the code exists as a component library even while it isn’t yet mounted. (c) Delete the Composer admin-menu entry. (d) Register the CPT gc_canvas_block as a backend-only workspace:

(e) Add an admin-menu entry “Blocks” that points to the CPT list. The list view uses WP 7.0’s wordpress/dataviews package when available; falls back to native WP_List_Table on WP 6.6. (f) Bump baseline: gillish-canvas.php header Requires PHP: 8.18.2; phpstan.neon.dist phpVersion: 80200. (g) composer.json require.php bumped to ^8.2.

End state. Plugin loads without composer remnants. User sees a “Blocks” menu, can create a gc_canvas_block post, types a title + composes with default blocks. Save works. Visiting the post’s would-be URL on the frontend returns 404. No Canvas inspector panels yet (Phase 2). No block-type registration yet (Phase 5). Tests green on PHP 8.2+.

02

Canvas inspector shell — summary control hub + manifest-driven accordion sections

Inject Canvas’s UI into Gutenberg’s existing Block Inspector via the editor.BlockEdit higher-order-component filter, using InspectorControls slots from @wordpress/block-editor. The shell consists of:

  1. Summary row at the top that is a control hub, not a label. Each line carries: a section name with a task-oriented subtitle (“Style — What this block looks like”, “Visibility — Who sees this block, and when”, “Schema — How search engines understand this block”); a status string in plain language when the section has changes (“2 changes”, “Only on mobile”); an eye-icon quick toggle on the Visibility line (Schema does NOT get a quick toggle — multi-step decision); a chevron that jumps to and opens the relevant section. Subtitles replaced by badge count once changes exist.
  2. Up to three accordion sections (Style, Visibility, Schema), one open at a time by default. Section visibility is manifest-driven and wholesale: when a block’s manifest declares a section is irrelevant, that entire accordion AND its summary-row line are hidden (not stubbed with “Schema: None”).
  3. Role-based default-open section: identical layout everywhere, but the default-open section switches by editor surface. CPT editor (designer-context) opens Style; regular post / page (author-context) opens Visibility.
  4. A footer-tools row with “Reset Canvas for this block” + “Copy Canvas settings”.

Sections are empty shells at this stage — the inspector architecture, summary-as-control-hub behaviour, manifest-driven section visibility, and role-based default-open must work, but the controls inside each section land in phases 3 / 4 / 13.

Verification. Open any post → select a Heading → right sidebar shows Gutenberg’s native panels followed by Canvas’s summary row + two accordion sections (Style + Visibility) + footer-tools; no Schema accordion because Heading’s manifest opts out; Visibility is default-open. Click the eye-icon on the Visibility summary line → toggles “Show everyone / Show conditionally” without opening the section. Click the on the Style line → Style opens, Visibility closes. Switch to the CPT editor → Style is default-open.

03

Style panel — universal style controls (gap-filling, not duplicating)

The Style panel adds Canvas’s controls only where Gutenberg native is missing or inconsistent — never as a duplicate. The check: wp.blocks.getBlockType(name).supports is read per block; for each Canvas control (Spacing, Color, Opacity, Drop-shadow, etc.) the panel either renders the control or shows a small note pointing to Gutenberg’s equivalent (“For background colour, see the Colour panel above”).

Universal-by-Canvas controls (Opacity, drop-shadow geometry — Gutenberg doesn’t ship these) are always rendered. Gutenberg-also-handles controls (background colour, font size) are rendered only when the selected block lacks native support. Controls write to a gcStyle attribute injected via blocks.registerBlockType. The render_block filter translates gcStyle to inline style + 8-char hex for opacity (per-instance, no separate stylesheet emitted).

Verification. Select a Heading → Canvas Style shows Opacity (always universal) + Drop-shadow (always universal), and notes “For text colour, see the Colour panel above” (Heading has native Colour) and “For padding, see the Dimensions panel above” (Heading has native padding via theme.json). Select a Separator → Canvas Style shows Color (Gutenberg doesn’t ship Color for Separator universally) + Opacity + Spacing. No control is duplicated between Canvas and Gutenberg on any block.

04

Visibility panel — recipe-first Conditional Content with intent grouping

The Visibility panel opens with a plain-language toggle: “Show this block to everyone” (default) / “Only show under certain conditions”. When the second option is selected, a recipe list grouped by intent appears first — not the rule builder, not a flat 18-item list. Five intent groups as flat subsections inside the Visibility accordion (NOT nested accordions):

Each recipe maps to the same conditions attribute shape the rule builder produces; the engine underneath is unchanged. Below the grouped recipes, a [Build custom rule ▾] disclosure expands the full rule builder for power users who need AND/OR-stacked rules, regex operators, or signal combinations no recipe covers. A separate [Advanced ▾] disclosure holds Render mode + Sticky cookie.

Free vs Pro: all 18 recipes are Free; Pro unlocks the custom rule builder + all 13 signals.

Render-mode default = client since most modern WordPress hosts cache pages aggressively and server-mode + page cache = silent cross-visitor pollution. Cache-plugin detection on init drives a dismissable admin notice when a cache is detected and server-mode rules exist.

Human-readable summary at top of the panel: when rules exist, the panel shows “This block is visible only to visitors on mobile devices” (or similar) above the controls. Same text appears in the inspector summary row.

05

Save = register block type + live preview (first end-to-end milestone)

On save_post for gc_canvas_block (priority 10): parse post_content into a block tree, validate against the manifest schema, save the tree + panelConfig + schemaVersion (= 1) in CPT meta. On init priority 12: SavedBlockRegistry::register_all() iterates published posts and calls register_block_type( "gillish-canvas/<slug>", [ apiVersion => 3, render_callback => …, attributes => […, schemaVersion] ] ) for each. The render builds HTML from the saved block tree via do_blocks( serialize_blocks( … ) ), with gcStyle + conditions evaluated. Inner blocks carry as <InnerBlocks> with templateLock="contentOnly".

Live preview from the CPT editor ships with this phase: a “Preview live” button opens a frontend-styled render of the block via a dedicated query-var route (?gc_canvas_block_preview={ID}&_wpnonce={NONCE}). Five layers of defence prevent any leak to the public:

  1. CPT’s public: false + publicly_queryable: false already returns 404 for the would-be permalink.
  2. Dedicated query-var, not the CPT’s slug, so a missing query var means normal 404.
  3. Handler requires both current_user_can( 'edit_post', $id ) AND valid wp_verify_nonce (failure → 404, not 401, so the URL gives zero signal it might exist).
  4. Preview response carries X-Robots-Tag: noindex, nofollow, noarchive, nosnippet, noimageindex HTTP header + matching <meta> in HTML.
  5. Explicit filter hooks block gc_canvas_block from Yoast / Rank Math / AIOSEO / WordPress-core sitemaps.

Three lifecycle safeguards (non-negotiable, shipped in this phase):

  • Breaking-change detection on save. Diff old template against new before persisting; structural changes trigger admin notice with migrate / new-slug / cancel paths.
  • Snapshot rollback. Previous template stored in _gc_template_snapshot CPT meta; failed renders fall back.
  • Dry-run validation pre-publish. Existing instances dry-rendered server-side before persisting; failures listed.

Verification. Create a gc_canvas_block post → publish → navigate to a regular post → block appears in inserter under “Canvas — My blocks” → insert → frontend renders correctly. Click “Preview live” on the CPT edit screen → browser opens the query-var URL → block renders with full theme styles, view-source shows <meta name="robots" content="noindex,nofollow,...">. Curl the same URL without the nonce → 404. With wrong nonce → 404. As logged-out user → 404.

06

Library — first batch

8–12 ready-made blocks shipped as code in src/Library/<slug>/. Each is an AbstractModule subclass with a manifest (tier: 'free' or tier: 'pro') + render callback. Boot-time registered via Plugin::register_hooks() (priority < user-CPT blocks so the catalog loads first). Inserter categories: “Canvas — Layout”, “Canvas — Content”, “Canvas — Conversion”, “Canvas — Interactive”.

First-batch composition is mixed-tier from day one so the free/pro structure is real, not retrofitted: ~6–8 Free blocks (Accordion, Tabs, Hero, Basic Pricing Table, Testimonial, FAQ) and ~2–4 Pro blocks (Modal, Pricing Pro, Discount Banner) to exercise the gating path. State + interaction land in later phases, so library blocks that need those (Accordion, Tabs, Modal) ship with static structure only until phase 10/11.

Byte-budget audit: each block has a measured HTML + CSS + JS budget at ship. Target: HTML ≤ 1 KB per instance, CSS ≤ 2 KB per block file (only enqueued when block is on the page), JS 0 KB. A block that exceeds budget either shrinks or doesn’t ship.

07

Repeater + parent/child rules

A new primitive: gillish-canvas/repeater with “one row template, N instances”. Parent/child rules extend Gutenberg’s parent field to a “can contain / can only live inside / minimum N / maximum N” model. Inspector UI for setting parent restrictions per CPT block. Frontend: <InnerBlocks allowedBlocks={...}> with the restrictions, validated server-side in render_callback. Prerequisite for Accordion / Tabs / Pricing grid to function as truly reusable blocks.

08

Import / export of built blocks

A service class Modules/CanvasBlock/ImportExportService handles validation, formatVersion migration, slug collision, persistence. REST: GET /wp-json/gillish/canvas/v1/blocks/<slug>/export + POST /wp-json/gillish/canvas/v1/blocks/import. WP-CLI: wp gillish-canvas list-blocks, export-block, import-block, wipe-blocks --yes.

UI. “Export” / “Import” as peer buttons (i) in the CPT list view (surfaced as a row action and as a bulk action) and (ii) on individual CPT edit screens via a PluginPostStatusInfo slot in the document settings (NOT the Block Inspector — import/export is a document-level concern). An imported block lands as a new gc_canvas_block post in draft for review/edit.

09

State-driven controls — active / hover / focus / disabled

Each Canvas Style control gets a “states” selector. Slider UI unchanged; storage becomes nested gcStyle: { default: {...}, hover: {...}, active: {...} }. Frontend generates scoped CSS for each state variant on the server. Classes: .gc-active, .gc-hover (which fall back to :hover by default). Prerequisite for the interaction system.

10

Interaction system — click / scroll triggers

“When X happens on block A, do Y on block B.” Triggers: click, hover, scroll-into-view, scroll-out-of-view, keyboard-enter. Actions: toggle-class, set-state, add-class, remove-class, scroll-to, focus. Builds on the WP Interactivity API (data-wp-on--click etc.). A visual builder on top of the same runtime. Prerequisite: state-driven controls (phase 9).

11

Library upgrade — interactive blocks

Accordion, Tabs, Modal, Carousel, Counter, Reveal-on-scroll in the library from phase 6 are upgraded with full state + interaction. Demonstrates that the primitive + system model delivers what the 50-hardcoded-blocks model would deliver.

12

Third-party blocks — verification

Stackable / Kadence / GenerateBlocks blocks are tested with the Canvas inspector panels (Style + Visibility). Expected to just work: we inject gcStyle + CC attrs via the blocks.registerBlockType filter on every block, third-party included; the editor.BlockEdit HOC injects the Canvas panels into every block’s inspector. Any incompatibilities are documented + addressed. Third-party blocks with their own save() (not render_callback) may have edge cases around markup freezing.

13

Schema panel — guided JSON-LD assistant in Overview / Details / Advanced task-mode

Each block can declare a Schema.org @type and map its own attributes to schema properties. Frontend emits aggregated JSON-LD in <head>. Attribute injection via blocks.registerBlockType: schemaType + schemaProperties. Runtime: Shared/SchemaCatalogue.php (closed enum of supported types) + Shared/SchemaEmitter.php (wp_head listener, parse_blocks() walk, dedupe on @type, one <script type="application/ld+json"> per type or a combined @graph).

Inspector UI is a guided assistant in three-step task-mode (Overview → Details → Advanced) — Schema is the one Canvas section that uses task-mode (Style is a flat field-list, Visibility is a recipe-picker, neither benefits from being step-gated). Schema is genuinely multi-step (decide if; pick a type; fill required fields; optionally override mapping):

  • Overview — first visible state. Yes/no toggle + type picker if yes. Plain-language type list (FAQ, Product, Review, Recipe, Article, Organization, BreadcrumbList, etc.). For library blocks with a default mapping, this step shows “Using FAQ schema — confirm?” and the user is done in one click.
  • Details — revealed when the chosen type needs author input. Only the fields that don’t auto-map from block attributes are surfaced for input here; auto-mapped fields are listed read-only at the bottom of Details (“Name: inherited from this block’s Heading”).
  • Advanced — full property mapping override, inherited-field overrides, per-property mainEntityOfPage tweaks, manual JSON-LD pass-through.

Role-based behaviour: in the CPT editor (designer-context), the designer sees the full Overview → Details → Advanced flow. In a regular post (author-context), Schema fields the designer mapped in the CPT show as read-only with an “Override for this instance” expander.

Library blocks from phases 6/11 ship with default schema mappings (Pricing → Product/Offer, FAQ → FAQPage, Recipe → Recipe). Third-party parity via the same injection. Server-side validation against the catalog skeleton before output.

14

AI integration — Abilities API base surface

Canvas registers a set of WordPress Abilities API capabilities under the canvas/* namespace so any MCP-speaking AI agent can introspect and build blocks. Minimum surface for v1: discovery (canvas/list-primitives, canvas/list-library-blocks, canvas/list-blocks, canvas/get-block, canvas/list-fixtures, and canvas/list-node-links when Node is active) and mutation (canvas/create-block, canvas/update-block, canvas/delete-block, canvas/import-block). Each ability has a JSON-schema input + output spec, runs through permission_callback, and routes through ManifestSchema::validate().

Free / Pro tier split: discovery abilities are unrestricted (read-only, capability-gated). Mutation abilities have a per-site monthly quota in Free (default 5 successful calls/month, stored in wp_option, resets on the 1st). Pro unlocks unlimited mutation calls (still rate-limited at 50/hour per user). Exceeding the Free quota returns 402.

Six security mitigations, all shipped in this phase (non-negotiable): admin-opt-in gate (mutation 403 until enabled); capability gates (manage_options + wp_rest nonce on every mutation); draft-only enforcement (AI-created blocks land as draft; publish via ability returns 403); rate limiting (50/hour per user); output sanitisation beyond schema (rejects <script>, javascript: URLs, data: URLs, event handlers, unknown attributes); soft-delete only (move to trash, never hard-delete).

AI-friendly error shapes: when validation fails, return { field: 'items[2].text', reason: 'required attribute missing', suggestion: 'Add a text value to the button block at items[2]' } instead of raw schema-error payloads.

Audit log logs every ability call (timestamp, user, ability name, payload hash, result, IP) to a new event-type row in gillish_canvas_events. A Canvas → AI activity admin page renders the last 30 days.

15

AI integration — richer introspection

Library blocks gain semantic descriptions in their manifests (semanticType: 'collapsible-list', 'tabbed-content', 'pricing-matrix', etc.) so AI agents can match user intent to the right pattern without guessing from block names. canvas/list-library-blocks returns these semantic tags. New ability canvas/validate-composition runs a proposed block tree through full manifest + parent/child + CC validation and returns a structured report. Per-attribute introspection: each block exposes which gcStyle keys are meaningful for it.

16

AI integration — preview rendering + batch

New ability canvas/preview-block takes a block tree + optional fixture ID and returns the rendered HTML against that fixture’s VisitorContext. AI agents can verify visual output (“does this CC rule actually hide the right element for a Norwegian visitor?”) before claiming completion. Batch abilities: canvas/create-blocks accepts an array of block specifications for bulk authoring. Asynchronous queue for slow operations.

17

Block Bindings integration — bind core blocks to Canvas data sources

The Block Bindings API (mature in WP 7.0, partial in 6.5+) lets core blocks (Paragraph, Heading, Image, Button) pull their content from structured data sources rather than holding text in the block’s HTML. Canvas registers a binding source gillish-canvas/data that exposes Canvas’s own data layer to core blocks: CPT-block instance attributes, post meta, dynamic visitor-context values, Node-managed-link metadata (when Node is active).

Concrete use cases:

  • A Pricing library block binds Heading’s content to a gc_price post meta — one source of truth, every Pricing instance reflects edits to the meta value.
  • A Testimonial block binds Image’s url + alt to a Testimonials taxonomy term meta — designer changes one term, every instance updates.
  • A Canvas-built block declares <Paragraph> slots that bind to author-editable fields via templateLock="contentOnly" plus a binding spec — author edits a small form, structured data fills the block.

AI-readability win: core blocks with bindings are transparent to the Abilities API (phase 14–16); an AI agent can introspect “this Paragraph displays content from meta key gc_price” and write changes via meta updates rather than parsing/rewriting HTML.

WP 6.6 fallback: when the user is on 6.6 (no Block Bindings API), the binding configuration is stored but ignored at render time; library blocks render their default static content instead. Graceful degradation, no breakage.

18

License plumbing — make Pro real (always the last phase)

Replace the Shared\Licensing dev-mode stub (which currently returns true unconditionally) with a real key-validated implementation. Decision on backend (Lemon Squeezy, Freemius, EDD Software Licensing, Paddle, or other) is made at phase-start. The contract surface stays fixed regardless of choice: has_tier( string $tier ): bool, license_key(): string, license_status(): array, refresh_now(): void.

Deliverables (provider-agnostic):

  • Admin “License” tab in Settings with key input + activate / deactivate buttons + status display (tier, expiry, sites used vs allowed, last validation timestamp).
  • Activation flow: on key entry, REST call to the chosen backend’s validation endpoint, payload signed (HMAC); result cached in wp_option with 14-day grace window.
  • Weekly cron re-validation idempotent on init; failure within grace window keeps the cached status, failure beyond grace window downgrades to free.
  • Phone-home pirate detection: weekly cron reports site URL + key hash; server detects same key on many sites and revokes (privacy-compliant opt-out in Settings, defaulting to ON).
  • Pro-build separate from wp.org zip (or single-zip-with-runtime-gating, depending on chosen backend).

Cross-cutting requirements (every phase)#

Discipline requirements that apply each time a phase touches UI, REST, or plugin activation. Listed here so they don’t get repeated in every phase row.

  • i18n. Every visible string goes through __() / esc_html__() / esc_attr__() with the 'gillish-canvas' text-domain.
  • REST permission discipline. Every new REST endpoint has an explicit permission_callback (never __return_true) + nonce verification.
  • Multisite defensive guard. register_activation_hook blocks network-activate with a clear error message.
  • Hook registry. Every add_action / add_filter lives in Plugin::register_hooks().
  • Option / hook constants. Every option key and hook name exists as a constant in Shared/Options.php and Shared/Hooks.php.
  • Cron self-heal. Cron events are registered idempotently on init.
  • Plain-language UX. Every visible string must be readable without a developer glossary. “Country”, not “Country code”. An acceptance criterion in every phase that touches UI.
  • Lean frontend output. Every phase that produces frontend HTML/CSS/JS has a byte-cost review step before merge. Lighthouse Performance ≥ 95, TTFB ≤ 200 ms, LCP ≤ 2.5 s, ≤ 1 KB HTML per block average, 0 frontend JS by default, no new font loads. render_block filter benchmark: total gate_block cost ≤ 5 ms for 195 no-condition blocks, ≤ 10 ms for 5 conditioned blocks.
  • Tier classification at merge. Every new block, every new feature flag, every new ability gets a clear 'free' or 'pro' classification when it lands. No “we’ll figure out monetisation later”.
  • Progressive UI, never exhaustive. Each Canvas-injected panel must follow the contextual-not-complete model: summary row as control hub, manifest-driven section visibility (wholesale), role-based default-open section, task-oriented subtitles, one section open at a time, badge counts, advanced behind [Advanced ▾], per-section reset, no duplication of Gutenberg native controls, task-mode reserved for Schema only.
  • PHP 8.2 minimum / WordPress 6.6 minimum. WP 7.0 features (DataViews, Block Bindings, native AI client) are used opportunistically with graceful degradation — Canvas works on 6.6 and 7.0 alike.
  • DataViews for admin lists. WP 7.0+ when available; falls back to native WP_List_Table on 6.6 with feature parity at the essentials.
  • License plumbing is always the last phase. Convention for plan maintenance: when a new phase is added, insert it before the license-plumbing phase (currently phase 18).

Deferred (in-scope, parked beyond launch)#

Inspector-side UX ideas that are Canvas-scope and concrete enough to leave room for in the launch architecture, but are deliberately deferred because the launch experience does not need them and shipping them now would expand surface area before the core inspector pattern has stabilised.

  • Canvas presets — saved Style + Visibility combinations, apply with one click. Per-user presets in user-meta, site presets in options. Mechanism is small (serialise attributes.gcStyle + attributes.conditions into a named slot, expose “Apply preset ▾” from the inspector summary row); UX of “what’s in this preset, what happens to existing values” is the work. Deferred to post-Phase 11.
  • Canvas lens — document-level Canvas view. A document-scope Plugin Sidebar (separate from the block-level Inspector) that lists every Canvas-configured block on the current post. Audit / overview tool, not an editing tool. Deferred to the Phase 17–18 tier. Likely Pro; not committed.

Non-goals#

Explicitly OUT of this plan:

  • A custom composer / fullscreen composition surface. That direction is parked.
  • Whole pages via Canvas (Bricks / Elementor competition). We build blocks, not pages.
  • Custom payment processor / store for Pro sales. Phase 18 wires Shared\Licensing to an external provider; we don’t build our own billing infrastructure.
  • Multi-site / network features. Explicitly blocked at activation.
  • Edit-in-Sandbox or any other instance-bound editing mode. The author owns the content, the designer owns the structure.