Identity#
| Name | Gillish Canvas |
|---|---|
| Vendor | Gillish |
| Entry file | gillish-canvas.php |
| Namespace | Gillish\Canvas\* (PSR-4, strict types) |
| Asset prefix | gc- |
| Constants | GC_* |
| Min PHP | 8.2 (post-pivot, bumped from 8.1 on 2026-05-11 in Phase 1 demolition; tested 8.2 → 8.5) |
| Min WordPress | 6.6 baseline (stable InspectorControls slot, editor.BlockEdit HOC filter, register_block_type apiVersion 3 with render_callback). WP 7.0 features (DataViews, Block Bindings, native AI client) used opportunistically with graceful degradation. |
| License | GPLv2-or-later |
| Distribution | wp.org plugin directory |
| Companion | Gillish Node — link strategy + content-graph SEO toolkit |
| Code quality | 0 errors / 0 warnings in Plugin Check. PHPStan level 9 clean. |
| Performance principle | No wrapper bloat. Conditional Content runs through the render_block filter — enabling it adds zero extra DOM nodes. Frontend emits only the winning HTML. |
The pitch#
Canvas is the one block plugin a WordPress site owner needs. A curated library of 30+ ready-to-use blocks (charts, accordions, pricing tables, conversion blocks, FAQs, TOC, etc.) covers the typical need out of the box. When the library doesn’t have it, the user opens Canvas → Blocks → New and composes a custom block in the standard Gutenberg post editor — using core blocks, Canvas primitives, third-party blocks, or library blocks. Saving registers the composed markup as a real block type that appears in the inserter on every other post and page.
Canvas’s distinctive trick is two universal capabilities
injected into every block’s existing Gutenberg Block Inspector:
a Canvas Style panel (spacing / colour / opacity / typography /
animation) and a Canvas Visibility panel (visitor-based show/hide
rules), with a Canvas Schema panel that lights up later for blocks
whose manifests opt into Schema.org emission. Panels live side-by-side with
Gutenberg’s native Colour / Typography / Dimensions panels in the same
right-sidebar region — no separate “Canvas” icon next to the
gear, no second sidebar to switch to. Available on any block in any post or
page, not just on the custom-blocks CPT, because the controls are injected
via the editor.BlockEdit higher-order-component filter rather
than registered as a separate sidebar surface. This is the architectural
commitment that defines Canvas: personalisation and universal styling treated
as first-class capabilities, not buried add-ons.
The third pillar is first-class import/export of blocks as
.gcblock.json files via both UI and WP-CLI — a community
multiplier no other block plugin ships as a peer feature to “create”
and “save”.
Standalone, Canvas already ships country detection (Cloudflare
CF-IPCountry header + bundled MaxMind GeoLite2 one-click
install), a built-in 22-pattern crawler classifier, a working CC engine with
10 operators and 13 signals, the Container and Columns primitives, and the
launch Conditional Content wrapper block. Better-together with Gillish Node,
blocks become link-aware: managed-link health badges, anchor inheritance,
and automatic affiliate-disclosure injection.
Direction note — pivot from custom composer (2026-05-11)#
Canvas previously planned a custom composer surface (a fullscreen admin page
with iframed @wordpress/block-editor, a primitive palette, and a
morphing rail of Canvas-specific controls). Phases 1–4 of that plan shipped
in v2.0.0-dev through v2.0.1 (2026-05-09 to 2026-05-11). The direction was
reset on 2026-05-11 after the realisation that:
- Two control surfaces per block (left rail + right BlockInspector) duplicated concepts and confused users.
- The block-plugin market is Gutenberg-native (GenerateBlocks, Stackable, Kadence). Tools with custom composers (Bricks, Oxygen) are theme builders, a different category.
- A custom composer freezes us on our current Gutenberg API knowledge; building on the post editor lets us inherit WordPress’s evolution for free.
The composer code (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, ~3 000
lines total) was demolished in Phase 1 (shipped 2026-05-11). The control
components were lifted into assets/js/gc-canvas-sidebar.js as an
unmounted component library; Phase 2 mounts them inside Gutenberg’s
Block Inspector via the editor.BlockEdit HOC filter +
InspectorControls slot. The rich pre-pivot implementations
remain in git history at commit 28d9482 for Phase 2
cherry-picking.
Conditional Content as a capability (vs. a block)#
The architectural decision that defines Canvas:
- Every block in the editor gets four conditional attributes (
conditions,match,renderMode,stickyCookie) injected via a sharedblocks.registerBlockTypeJS filter (mirrors the PHPregister_block_type_argsfilter for server-side consistency). The injection touches every block, including core blocks and third-party blocks — not just Canvas-namespaced blocks. - The Canvas Visibility panel (injected into Gutenberg’s Block Inspector via the
editor.BlockEditHOC) exposes a plain-language rule builder for those attributes: Country, Language, Device, Browser, Login status, UTM tag, Visits, Local time as the signals, with operators like “is”, “is not”, “is one of”, “contains”, “is between”. - A
render_blockfilter at priority 10 intercepts every block render. If the block hasconditionsset, the centralisedShared\RuleEvaluatorruns againstShared\VisitorContextand the filter either returns the original HTML (rule passed) or returns''(rule failed → block disappears entirely; no wrapper, no placeholder, no DOM cost). - The standalone
gillish-canvas/conditional-contentblock stays in the library as the wrap-arbitrary-content escape hatch — for swapping multi-block sections, classic-editor flows, or content shapes that don’t fit a single block.
This is the architectural commitment that makes Canvas distinctive: no competitor block library treats personalisation as a first-class universal attribute. Most ship “an A/B testing block” or “a geo-content block” as one isolated block; Canvas makes every block conditionable from the same set of inspector panels.
Canvas controls in the Block Inspector#
The headline experience: Canvas injects three universal panels (Style,
Visibility, Schema) directly into Gutenberg’s existing
Block Inspector (the right sidebar that already holds
Gutenberg’s Colour, Typography, and Layout panels). The mechanism is
InspectorControls slots via the editor.BlockEdit
higher-order-component filter — the same pattern Stackable, Kadence, and
GenerateBlocks use. No separate Plugin Sidebar, no separate icon to discover,
no manual switching between sidebars.
UX architecture — contextual, not complete#
The biggest UX risk of injecting three universal panels into every block’s inspector is overwhelming the user: padding, margin, gap, four colour pickers, four opacity sliders, eight condition signals, schema mapping fields … on every block. That defeats the point.
The sidebar is built as a layered decision tool, not a settings dump:
┌──────────────────────────────────────┐
│ Canvas │ ← Section header
├──────────────────────────────────────┤
│ 👁 Style · 2 changes ↗ │ ← Summary row is a
│ 👁 Visibility · Only on mobile ↗ │ control hub. Eye
│ │ toggles "Show /
├──────────────────────────────────────┤ Conditional".
│ ▼ Style (2) │ ↗ click-to-jump
│ What this block looks like │ opens that section.
│ Spacing: padding 16px │
│ Color: background #1d2733 │ (Schema row hidden
│ [Advanced ▾] │ here because the
├──────────────────────────────────────┤ block's manifest
│ ▶ Visibility (1) │ does not declare
│ Who sees this block, and when │ schemaEmission.)
├──────────────────────────────────────┤
│ Reset Canvas for this block │ ← Footer tools.
│ Copy Canvas settings │
└──────────────────────────────────────┘
Principles enforced by this architecture:
- Summary first, and the summary is interactive. Each summary line is a control hub, not a label: an eye-icon toggles Visibility’s “Show everyone / Show conditionally” state directly from the summary, and a
↗chevron jumps to the relevant section. Schema does not get a summary-row quick toggle — enabling schema is a multi-step decision, not a one-click yes/no. - Role-based defaults, same layout. The structure is identical everywhere; only the default-open section changes. CPT block-builder (designer-context) opens Style; regular post / page (author-context) opens Visibility.
- Schema is read-only-by-default in author context. Schema fields the designer mapped in the CPT show as read-only with an “Override for this instance” disclosure. Authors don’t have to relearn Schema per instance.
- Section visibility is manifest-driven and wholesale. A Spacer or Separator has no Schema → the Schema accordion and its summary row do not render at all. Both granularities — entire sections and individual controls — are manifest-driven. Avoids “Schema: None” / “Gap: N/A” noise.
- Task-oriented subtitles, not headings. Each accordion header carries a short subtitle (“Style — What this block looks like”, “Visibility — Who sees this block, and when”). Subtitles disappear once the user customises the section (replaced by the badge count).
- One section open at a time by default. Badge counts per section (
Style (3)). Manifest-driven control rendering — only controls meaningful for the selected block render. - No duplication of Gutenberg native controls. When Gutenberg already exposes a control for a block-type (e.g. Colour panel for a Heading), Canvas hides its own and shows a note: “For text colour, see the Colour panel above.” Canvas Style fills the gaps, never duplicates the overlap.
- Advanced behind explicit disclosure. Render mode, sticky cookie, regex operators, state variants, schema mapping details — none visible until the user clicks “Advanced”.
- Per-section reset. “Reset visibility rules” is mentally safer than “Reset everything”.
Three injected panels#
Style. Universal style controls applied via a
gcStyle attribute injected into every block. Sections:
SPACING (full per-side box-model widget for padding + margin),
GAP (numeric control, only on parent blocks), COLOR
(background + text + border + shadow colour pickers, each with an opacity
slider and a Gutenberg HEX-first colour picker behind a custom swatch). Each
control is gated against Gutenberg native: if
wp.blocks.getBlockType(name).supports indicates the block
already has equivalent native support, Canvas hides its own control. State-
driven variants (active / hover / focus / disabled) land in phase 9.
Visibility. Plain-language toggle at the top: “Show this block to everyone” / “Only show under certain conditions”. When the second option is selected, recipes appear first, not the rule builder. The recipe list is grouped by intent, five flat subsections inside the Visibility accordion (NOT nested accordions):
| Group | Recipes |
|---|---|
| Device & layout | Only on mobile · Only on tablet · Only on desktop |
| Audience & auth | Only for logged-in users · Only for logged-out users · Hide from administrators · Show only to administrators |
| Location & time | Only in [Country] · Not in [Country] · Only in [list of countries] · Only between [time] and [time] today |
| Traffic source | Only from [referrer host] · Show only to traffic from [UTM source] · Hide from search engines · Show only to search engines |
| Behaviour | Show to first-time visitors · Show to returning visitors · Show to frequent visitors |
Each recipe translates to the same conditions shape the rule
builder produces; under the hood it’s one engine. Below the recipes, a
[Build custom rule ▾] disclosure expands the full rule builder
for power users. Render mode + sticky cookie live under a separate
Advanced disclosure.
Schema. A guided assistant, not a settings panel — structured as a three-step task-mode flow (Overview → Details → Advanced) because Schema is the one Canvas section that is genuinely multi-step. Style and Visibility do not use task-mode (they’re a flat field-list and a recipe-picker respectively).
- Overview — yes/no toggle (“Emit schema for this block”) + 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 — confirm?” and the user is done in one click.
- Details — revealed when the chosen type needs author input (e.g. Product needs
priceandavailability). 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 (“Name: inherited from the block’s Heading”). - Advanced — full property mapping override, inherited-field overrides, per-property
mainEntityOfPagetweaks, manual JSON-LD pass-through.
Why InspectorControls, not a separate Plugin Sidebar#
The earlier plan called for a dedicated Plugin Sidebar (icon in the sidebar-switcher, click to open). It was reconsidered on 2026-05-11:
- Two sidebar icons = friction. Setting a block’s colour (Gutenberg’s panel) and its visibility rule (a hypothetical Plugin Sidebar) would require switching between two icons in the top corner. For a block-by-block editing flow, that’s hostile UX.
InspectorControlsis the WordPress-native pattern. Every major block plugin extends the inspector this way. Users trained on Stackable / Kadence / GenerateBlocks immediately recognise Canvas’s panels.- Automatic selection tracking. Inspector panels appear when the block is selected and disappear when nothing is selected.
- Less code. No separate sidebar shell, no manual selected-block tracking, no opt-in icon discovery. The
editor.BlockEditfilter is a one-line HOC.
The trade-off: Canvas’s panels are visually mixed with Gutenberg’s. We compensate with consistent design (Canvas Ink panel headers, our typography, our spacing rhythm) so the three Canvas panels read as a coherent block without looking like alien intrusions.
If a future need arises for document-level (not block-level) Canvas settings, that can land as a Plugin Sidebar later — see the deferred Canvas lens entry below.
Per-block manifests#
Every block in src/Modules/<BlockName>/ declares its
attributes, default values, and metadata in a manifest.json
validated against Shared\ManifestSchema. The same manifest
drives the PHP-side register_block_type and (in phase 13) the
JSON-LD schema emission. One source of truth for what a block can do.
Live integration with Gillish Node (when both plugins active)#
The Canvas inspector panels query Node’s REST surface at editor-load time for link-aware features:
GET /wp-json/gillish-node/v1/links— typeahead search of managed links the author can drop onto a button or product card.- Per-link health probe. When an author selects a managed link, the inspector panel polls Node’s
LinkHealthCheckervia REST and shows Healthy / Pending / Broken inline. Broken links surface as red badges at the parent block level. - Sponsored-flag introspection. The same payload carries the link’s
rel="sponsored"flag, so Canvas can warn an author who omits the Affiliate Disclosure block.
When Node is missing (!GC_NODE_ACTIVE), the link-aware inspector panels are hidden entirely; Canvas stays fully usable.
Preview-as inside the editor#
The same Shared\VisitorContext that powers server-side rendering is exposed to a Preview-as toolbar in the post editor. Toggling between fixtures (iPhone US, Visitor from Norway, Logged-in subscriber, Search-engine crawler) re-evaluates every block’s conditional rules in place — the editor canvas updates without reloading, so the author sees instantly which blocks disappear and which stay.
Deferred UX explorations (parked, in-scope but post-launch)#
Two inspector-side UX ideas surfaced during 2026 review that are Canvas-scope but deliberately deferred — 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. A “preset” is a named bundle of
gcStyle+conditionsvalues that an author can apply to any block instance (“Brand hero”, “Mobile-only CTA”, “Logged-in-only product callout”). Two halves: per-user presets (user-meta) and site presets (options). Deferred to post-Phase 11. Free / Pro split open; lean toward per-user free, site presets Pro. - 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: which are hidden on mobile, which target a UTM source, which emit Schema. Audit / overview tool, not an editing tool. Deferred to the Phase 17–18 tier. Likely Pro; not committed.
Both ideas are concrete enough that the Inspector pattern (Phase 2) and the CPT save flow (Phase 5) must leave room for them — attributes.gcStyle + attributes.conditions need to remain serialisable in isolation, and the inspector summary row must reserve space for a future “Apply preset ▾” control.
Custom blocks via the gc_canvas_block CPT#
A backend-only custom post type for designing reusable blocks. CPT posts are workspaces, not content. They have no frontend URL, do not appear in search results, do not get canonical URLs, are not queryable on the front end, and never render as standalone pages.
CPT post (workspace) → save → registered block type →
appears in Gutenberg inserter → author inserts into a regular
post or page → frontend renders the instance
The CPT post itself never shows up on the frontend.
- Composition. The designer opens a CPT post in the standard Gutenberg post editor and composes a block template with any registered block. Canvas’s three universal inspector panels appear next to Gutenberg’s native panels. The Style and Visibility settings the designer configures here become the block type’s defaults — instances inherit these unless the author overrides per instance.
- Save. On
save_postforgc_canvas_block(priority 10), the post_content is parsed into a block tree and validated against the manifest schema. The block tree + panelConfig +schemaVersionget stored in CPT meta. - Registration. On
initpriority 12,SavedBlockRegistry::register_all()iterates published CPT posts and callsregister_block_type()for each, with arender_callbackthat returns the stored markup at frontend render time. Inner blocks carry as<InnerBlocks>withtemplateLock="contentOnly"— the author edits content per instance; the designer owns the structure. - Inserter category. Custom CPT-built blocks appear under “Canvas — My blocks”. Library blocks appear under “Canvas — Layout / Content / Conversion / Interactive”.
- Schema versioning. Each block-type instance persists its own
schemaVersionattribute. A lower version than the current one runs through amigratecallback. Allows non-breaking evolution of a CPT block’s saved markup over time. - Live edit propagation. When the designer edits the CPT post and saves again, all instances of that block type on regular posts pick up the new layout / style / CC defaults automatically.
Live frontend preview from the CPT editor#
The CPT is backend-only — its posts never publish to the frontend. But designers still need to see what their block looks like when rendered with real theme styles, real visitor context, and real render_callback output. A “Preview live” button in the CPT editor opens a render of the block in a theme-styled frontend shell, without ever creating a public URL for the CPT post.
The mechanism, with five layers of defence so a single misconfiguration can’t leak the preview to the public:
- CPT registration. The five frontend-invisibility flags listed above. WordPress itself returns 404 for any non-admin request that targets the CPT post directly.
- Dedicated preview route, not the post’s permalink. The preview URL is
https://site.tld/?gc_canvas_block_preview={ID}&_wpnonce={NONCE}— a registered query var, NOT the CPT’s would-be slug. - Capability + nonce check, both required. The handler verifies
wp_verify_nonceANDcurrent_user_can( 'edit_post', $id ). Either failing → 404 (not 401 or 403; zero signal that a preview URL might exist). - Hard noindex on the preview response. The response carries
X-Robots-Tag: noindex, nofollow, noarchive, nosnippet, noimageindexHTTP header AND matching<meta>in<head>. The nonce is single-use and short-lived (24 hours). - Explicit SEO-plugin exclusion hooks. Filter callbacks on
wpseo_sitemap_exclude_post_type(Yoast),rank_math/sitemap/exclude_post_type(Rank Math),aioseo_sitemap_post_types(All in One SEO),wp_sitemaps_post_types(WP core), and a generic catch-allgillish_canvas_sitemap_exclude_post_types.
To leak a CPT post to the public, all five layers would have to break simultaneously — which requires deliberate code change to Canvas + a misconfigured WordPress installation + a misconfigured SEO plugin + a shared preview URL with a non-expired nonce + Google ignoring the robots header. The probability is meaningfully zero.
Lifecycle safeguards (block-type evolution)#
The biggest architectural risk in the CPT model is frozen content vs evolving structure: a designer edits a CPT-block in a way that’s incompatible with existing instances, and suddenly thousands of published posts show block-validation errors. Three guardrails, all shipped as part of Phase 5:
- Breaking-change detection on CPT save. Diff old template against new. Structural changes — removed blocks, renamed inner-block slots, removed attributes, changed block types — are flagged with an admin notice. Three paths: write a
migratecallback; publish as a new slug (e.g.pricing-grid-v2); cancel. - Snapshot rollback. Every CPT save also stores the previous template as a backup in CPT meta (
_gc_template_snapshot). Whenrender_callbackfor an instance throws, the fallback path renders against the snapshot and an admin-only notice surfaces. Frontend never crashes; only admin sees the warning. - Dry-run validation pre-publish. When the designer hits “Update”, Canvas runs every existing instance through a dry render against the new template, server-side. If any instance fails, the designer sees a list and the save is blocked until they choose a path.
Two editor surfaces; one frontend render surface#
Canvas’s inspector panels appear automatically in the Block Inspector in two distinct editor contexts, doing different jobs:
- Surface A — regular post / page editor. Author is writing real content destined for the frontend. Canvas’s panels apply universal Style + CC to instances. Settings here override the block type’s defaults for that one instance.
- Surface B —
gc_canvas_blockCPT editor. Designer is building a block template. Canvas’s panels configure the block type’s defaults — what every instance starts with before the author touches it.
Both surfaces write to the same attribute schema; the difference is which level the writes affect. The frontend render surface (Surface C, no editor) reads the resolved combination at render time: instance attributes if set, falling back to block-type defaults.
The block library#
Tiering
Each block carries a 'tier' on its ModuleRegistry entry:
'free'— always available, drives adoption.'pro'— gated byShared\Licensing::has_tier( 'pro' ). Today the helper returnstrueunconditionally (dev mode); the real license-key implementation is Phase 18.ModuleRegistry::is_enabled()ANDs the per-module on/off toggle with the tier gate.
Roadmap target — 100+ blocks
The library is sliced into seven strategic categories. Every block in every category supports the Conditional Content attributes universally. The full 100-block list is in the appendix.
| Category | Coverage |
|---|---|
| Conversion & sales (15) | Product comparison, pricing tables, Pros/Cons, TL;DR, Amazon card, animated buy buttons, discount banners, trust badges, scarcity, product grids, checklists, Affiliate Disclosure, sticky bars, exit-intent overlays, lead-magnets. |
| Content & SEO structure (15) | FAQ schema, breadcrumbs, TOC, author bio, post meta, related posts, section dividers, anchor menu, glossary tooltips, footnotes, post navigation, search bar, sitemap visualiser, code snippet, blockquote variants. |
| Layout & design framework (15) | Advanced columns, container with overlays, masonry, CSS-grid layout, parallax, Lottie, shape dividers, responsive spacer, z-index layer, icon box, feature list, image comparison slider, video popup, hero sections, team grid. |
| Social & interactive (15) | Instagram / X / YouTube / Pinterest / Facebook / WhatsApp / Telegram embeds; RSS reader; social-proof popups; thumbs up/down; quiz; poll; contact form; Google Maps; donation button. |
| Dynamic & contextual (10) | Device-specific box, geo-targeted text, countdown timer, dynamic data table, user-role-gated content, time-of-day greetings, referrer greeting, weather widget, stock ticker, currency converter. |
| Specialised & niche (10) | Event timeline, recipe card, event listings, podcast player, PDF embedder, count-up counter, star-rating grid, step-by-step guide, comparison slider, logo carousel. |
| Tools & calculators (10) | BMI, loan / mortgage, percentage, crypto price card, business hours, team org chart, interactive map, job board, real-estate card, restaurant menu. |
| Advanced visual effects (10) | Glassmorphism, neumorphism, typing animation, hover flip card, scroll-reveal, floating images, background-video hero, gradient text, content reveal, custom-HTML wrapper. |
The target is to be WordPress’ best block library — not just by count but by polish. Every block is treated as a product: settings defaults that work without configuration, a block-editor preview that matches the frontend render, dark-mode pairings, accessibility review, performance budget.
Currently shipped
| Block | Tier | Status | One-line |
|---|---|---|---|
| Conditional Content | Pro | v0.1.0+ | The standalone wrap-anything-with-rules block. Same rule engine that powers the universal capability across the rest of the editor. |
| Container | Free | v2.0.0+ | Canvas primitive. Wraps any inner blocks in a stylable section. Rich Gutenberg supports. |
| Columns | Free | v2.0.0+ | Canvas primitive. Multi-column layout primitive. |
Conditional Content (the launch block)#
Two render modes
| Mode | Picks variant | SEO | Page-cache compatible |
|---|---|---|---|
| Client (default) | Server emits all variants in <template> + visible default; runtime picks in browser. | Stable — search engines see the default variant. | Yes — same HTML for all visitors, runtime swaps after cache hit. |
| Server (opt-in) | PHP at the_content time, emits only the winner. | Best — variant selected from actual visitor. | No — cached HTML pins the first visitor’s variant for everyone. |
Default flipped to client-mode 2026-05-11. Most modern WordPress hosts cache pages aggressively (managed WP, Cloudflare, LiteSpeed, WP Rocket, W3 Total Cache). Server-mode + page cache = silent cross-visitor pollution. Client-mode pays a ~100 ms flicker for cache safety, the correct trade-off for most sites. Server-mode is still available as an explicit opt-in for cache-free environments.
Cache-plugin detection. On init, Canvas checks for known caching layers (WP_Rocket, LiteSpeed_Cache, W3_Plugin_TotalCache, WP_Super_Cache, Hummingbird, HTTP_CF_RAY). When detected, an admin notice fires in the CC settings panel.
Two authoring surfaces
- Gutenberg block —
gillish-canvas/conditional-content(parent) +gillish-canvas/cc-variant(child). Inspector rule builder with human-grade pickers (250-country typeahead, OS / browser / role dropdowns, multi-token “is one of” fields). Preview-as toolbar with editable visitor-profile fixtures. Inline double-click rename. Copy variant. - Shortcode —
[node_swap]/[node_when]/[node_else]for Classic Editor / arbitrary content. Compact attr syntax (country="US,CA",country_not="DE",match="any"). Same rule evaluator as the block.
Rule DSL
Signals: country · device.os · device.type · browser · language · referrer · logged_in · user_role · utm_source · utm_medium · utm_campaign · visit_count · hour_local (browser-side only).
Operators: eq · neq · in · not_in · contains · matches (regex) · gt · lt · between · not_between.
Unknown signal handling. Positive ops (eq/in/gt/between) return false; negative ops (neq/not_in/not_between) return true. The documented “fail closed” contract — typo’d rules surface as “rule never matches”, not “rule matches everyone.”
Editable Preview-as fixtures
The block-editor toolbar’s “Preview as: …” dropdown shows visitor profiles the author can simulate. Built-in defaults ship (iPhone US, Android phone US, iPad US, Mac desktop US, Windows desktop US, Visitor from Norway, Visitor from EU, Visitor from United States, Logged-in subscriber US). Authors edit the list under Settings → Conditional Content → Preview-as fixtures. Country dropdown sourced from the 250-country alpha-2 catalogue; language dropdown is country-aware. Label auto-syncs with the country via a hidden label_template. “Live (no preview)” and “Search-engine crawler” sentinel entries always sit at the top and bottom.
Sticky bucketing (opt-in per block)
gc_cc_<blockId> cookie pins the visitor to the variant they saw on first pageview for the configured lifetime (default 30 days, clamped to [1, 365], filterable via gillish_canvas_cc_sticky_cookie_lifetime_days). Cookie writes are client-side only (page-cache safety). Server reads $_COOKIE and short-circuits rule eval.
Crawler short-circuit
gillish_canvas_cc_is_crawler filter forces the Default fallback for bot user-agents — keeping server-rendered output stable for indexing. Default implementation: a curated 22-pattern list covering search-engine crawlers (Googlebot, Bingbot, DuckDuckBot, BaiduSpider, YandexBot), SEO tools (AhrefsBot, SemrushBot, MJ12Bot), link-preview fetchers (Facebook, Twitter, LinkedIn, Pinterest, Telegram, Discord, WhatsApp), and AI crawlers (GPTBot, ClaudeBot, PerplexityBot, CCBot).
DNT respect
When the visitor sends DNT: 1 AND Options::cc_respect_dnt() is on (default true), all advanced signals (UTM × 3, visit count, hour-local) resolve to null, no cookies are read or written, and the analytics action does not fire.
Analytics
Each server-mode render writes one row to gillish_canvas_events via the gillish_canvas_cc_variant_served action listener. The Canvas → Conditional Content admin page reports an overview (total renders + top blocks by volume in 7/30/90-day ranges), per-variant drill-down, and a daily trend SVG bar chart. Daily prune cron trims rows older than Options::cc_retention_days() (default 90 days, clamped to [7, 3650], 0 = forever).
Caveat: the action only fires for server-mode renders. Client-mode-rendered blocks don’t appear in the admin analytics page (the server doesn’t know which variant the browser picked). Surfaced in the editor as an info hint.
REST endpoint
GET /wp-json/gillish/canvas/v1/cc/country — per-visitor country lookup the client-mode runtime calls after the page-cache hit. Cache-Control: private, max-age=300.
Built-in GeoIP#
Canvas’s country signal does NOT depend on Gillish Node. Two sources are tried in order, both shipped inside Canvas:
- Cloudflare
CF-IPCountryheader — zero-config, free, works automatically when the site is behind Cloudflare. - Bundled MaxMind GeoLite2 database — one-click install / update / delete from Settings → Integrations → GeoIP. ~9 MB for Country, ~70 MB for City. CC BY-SA 4.0 licensed, attributed via
GeoLite2-LICENSE.txt. Daily auto-refresh cron when the file ages past 30 days. Vendored MaxMind DB reader (Apache 2.0) atvendor-runtime/maxmind-db-reader/.
When Gillish Node is also active AND has the same database flavour installed at wp-content/uploads/gillish-node/, Canvas’s “Install database” button copies the file in place of redownloading. CC BY-SA permits this; both plugins ship their own attribution file.
Link-aware design (Better Together with Gillish Node)#
When Gillish Node is active, Canvas transforms from a design tool into a data-driven conversion tool. Three layers of integration, all opt-in per block.
1. Metadata Inheritance
A button or product card in Canvas can be linked to a gillish_node managed-link ID. The block then inherits:
- Preferred Anchor Text — Node’s per-link “anchor override” field. If the Canvas block doesn’t have a custom button label, it falls back to this.
- Destination URL — always Node’s resolved redirect URL (after Node’s dynamic-rule evaluation, A/B routing, and shortener replacement). A visitor in NO can be sent to a different affiliate destination than a visitor in US, automatically, without Canvas needing to know about Node’s rule engine.
- Link metadata —
rel="sponsored"flag, category, etc.
Data direction is one-way (Node → Canvas). Canvas blocks read Node link IDs at render time via apply_filters( 'gillish_node_resolve_managed_link', null, $link_id ). Node never has to know Canvas exists.
2. Automated Affiliate Disclosure
Canvas ships a dedicated Affiliate Disclosure block (typography, spacing, dark-mode pair, a11y review — designed to look professional in any theme).
When Node is active, Node scans post content for any Canvas blocks referencing managed links flagged rel="sponsored". If at least one is present, Node automatically injects the Canvas Disclosure block at the top of the post via the the_content filter — UNLESS the post already contains a Disclosure block (idempotent).
The site author can override the auto-injection per post (a meta-box toggle on the post editor) or globally (Settings → Privacy → Disclosure). The default-on behaviour gives FTC compliance for free on every affiliate post a user publishes.
3. Health Awareness
Node’s LinkHealthChecker already tracks per-managed-link HTTP status. Canvas blocks that reference a managed link query Node’s status at editor-load time:
- Healthy — block renders normally, no warning.
- Broken (404 / 410 / DNS / timeout) — block sidebar shows a red warning: “Linked managed link is broken.” Deep-links to Node’s Manage Links page filtered to the affected link.
- Pending — link not yet checked. Subtle hint, no action.
This catches a whole class of regressions: an affiliate program shutting down, the redirect dying, nobody noticing for weeks because the links were buried in old posts.
Extended integration opportunities (planning, not committed)
Beyond the three integrations above, ten further opportunities are tracked for future planning. Tagged by editor surface: A = regular post / page editor; B = gc_canvas_block CPT editor; C = frontend render-time.
- Universal link picker. (A + B) Every URL field in every Canvas block (button, image link, hero CTA, even inline links in paragraph via a Gutenberg RichText format) can pick a Node managed-link ID instead of typing a raw URL.
- Internal-linking suggestions from Node’s content graph. (A only) While the author writes a regular post, the Canvas inspector shows “this paragraph mentions [topic]; you have a post about [topic] → link it?”
- Per-block click analytics fed by Node. (A + B) Canvas blocks that reference Node links inherit per-link click stats.
- Geo-routed product cards. (A + B + C) A “Product Card” library block bound to a Node managed link with geo-rules renders the correct affiliate URL per visitor — no Canvas logic, Canvas just reads what Node resolved.
- Schema.org enrichment from Node-stored product metadata. (B configures, C resolves) When the designer maps a Schema type, fields like
price,currency,availabilitycan read from Node link metadata at frontend render time. - Link-expiry-aware visibility. (B configures, C resolves) Node’s per-link metadata can carry an expiry date. The designer’s Canvas Visibility panel in the CPT offers “auto-add CC rule: hide after
<link.expires_at>”. - Per-link visibility rules pushed from Node. (B configures, C resolves) Node could expose “this link is geo-restricted to US only.” The Canvas block respects it automatically.
- Bulk-relink across CPT block instances. (admin, no editor) When a Node link target changes, every Canvas block instance referring to that link auto-updates at render time.
- Disclosure-block injection scope per surface. (A + B) Auto-disclosure for surface A is on by default; for surface B, the designer might want the disclosure baked into the CPT block-type template.
- Compliance reporting. (admin) A combined “sponsored links + disclosure coverage” report.
Import / export#
First-class import/export of CPT-built blocks via both UI and WP-CLI — a community multiplier no other block plugin ships as a peer feature to “create” and “save”.
- Format.
.gcblock.jsonwithformatVersion,slug,title,composition(block tree),panelConfig,schemaVersion. - REST.
GET /wp-json/gillish/canvas/v1/blocks/<slug>/export+POST /wp-json/gillish/canvas/v1/blocks/import.permission_callback:manage_options+wp_restnonce. - WP-CLI.
wp gillish-canvas list-blocks·export-block·import-block·wipe-blocks --yes. - UI. “Export” / “Import” as peer buttons in the CPT list view (surfaced as a row action and as a bulk action) and on individual CPT edit screens via a
PluginPostStatusInfoslot in the document settings (NOT the Block Inspector — import/export is a document-level concern). An imported block lands as a newgc_canvas_blockpost in draft for review/edit.
AI integration via WordPress Abilities API + MCP#
Canvas is built to be operable by AI agents through the WordPress Abilities API (Core integration tracked for WP 7.0; usable today via the Abilities API canonical plugin). A user can connect Claude Code, Cursor, ChatGPT Plus, or any MCP-speaking AI agent to their WordPress site and say “build me an FAQ block about pricing” — the agent introspects Canvas’s capabilities and produces a working block via the same internal contracts the human UI uses.
Why this matters
The 100-block library is comprehensive, but no library covers every site-specific need. The CPT block-builder solves this for designers; the AI integration solves it for users who can describe what they want but don’t know how to build it. Plain-language “an accordion of my service hours by location” produces a draft block in admin without the user opening the block editor.
Tier split
- Discovery abilities (
canvas/list-primitives,canvas/list-library-blocks,canvas/get-block, etc.) — unrestricted, read-only, capability-gated. Free tier and Pro tier both get full discovery. - Mutation abilities (
canvas/create-block,canvas/update-block,canvas/delete-block,canvas/import-block) — quota-limited in Free (default 5 successful calls/month, stored inwp_option, resets on the 1st), unlimited in Pro (still rate-limited at 50/hour per user). Exceeding the Free quota returns 402.
Ability surface (planned)
| Ability | Purpose |
|---|---|
canvas/list-primitives | Catalogue of Canvas primitives (Container, Columns, etc.) the agent can compose with. |
canvas/list-library-blocks | Catalogue of shipped library blocks, with semantic tags (e.g. semanticType: 'collapsible-list') so the agent can match user intent to the right pattern. |
canvas/list-blocks | All gc_canvas_block CPT posts on the site. |
canvas/get-block | Block tree + panelConfig + schemaVersion for a given slug. |
canvas/list-fixtures | Preview-as visitor profiles. |
canvas/list-node-links | (When Node is active.) Managed-link directory. |
canvas/create-block | Create a new CPT block. Input validated via ManifestSchema::validate(). Lands as draft. |
canvas/update-block | Update an existing CPT block. Validated. |
canvas/delete-block | Soft-delete (move to trash) — never hard-deletes. |
canvas/import-block | Import a .gcblock.json payload. |
canvas/preview-block | Render a block tree against a VisitorContext fixture and return HTML. |
canvas/validate-composition | Dry-run a proposed block tree through full manifest + parent/child + CC validation. |
canvas/create-blocks | Batch version of create-block for bulk authoring. |
Worked example — the accordion request
Implementation cost
Modest: a thin layer that translates each WordPress ability call to Canvas’s internal API. ManifestSchema::validate() already exists; canvas/create-block wraps the same call the human UI uses. The AI integration is not a separate code path — it’s an alternate input to the same engine.
Security and capability discipline
Six non-negotiable mitigations, all shipped in the AI base phase:
- Admin-opt-in gate. Site-wide setting “Allow AI agents to create blocks” defaults OFF; mutation abilities return 403 until enabled. Discovery abilities stay on by default (read-only, capability-gated).
- Capability gates. Every mutation ability requires
manage_options+ validwp_restnonce. - Draft-only enforcement. AI-created blocks land as
draftCPT posts; trying to publish via ability returns 403. The “AI draft + human review + human publish” flow is enforced at the ability layer. - Rate limiting. Per-user limit on mutation calls (default 50/hour, admin-configurable); exceeding returns 429 with
Retry-After. - Output sanitisation beyond schema. Reject
<script>in attribute values,javascript:URLs,data:URLs, HTML event handlers, and any attribute not in the manifest schema. - Soft-delete only.
canvas/delete-blockmoves to trash; never hard-deletes.
Audit log. Every ability call (timestamp, user, ability name, payload hash, result, IP) logs to an event-type row in gillish_canvas_events. A Canvas → AI activity admin page renders the last 30 days.
Performance philosophy — lean frontend output#
WordPress block plugins have earned a reputation for bloated frontend output — three nested <div>s per block, per-instance stylesheets, per-block React apps, font loads for every typeface offered in the editor. Canvas opts out of that pattern. The philosophy is: the frontend is the user’s site, not Canvas’s. Canvas’s job is to disappear at render time.
Targets (the bar every release must clear)
| Metric | Target |
|---|---|
| Lighthouse Performance (mobile) | ≥ 95 on the standard 4-block test post (Hero + Container + Heading + Pricing + FAQ). |
| TTFB | ≤ 200 ms (PHP execution under typical hosting). |
| LCP | ≤ 2.5 s on 3G Fast throttling. |
| CLS | 0 (Canvas blocks never inject layout-shifting content after first paint). |
| HTML per block instance | ≤ 1 KB average across the library. |
| CSS per block file | ≤ 2 KB. Only enqueued when has_block() for that block is true. |
| Frontend JS | 0 KB by default. Interactive blocks share one @wordpress/interactivity runtime; never a per-block React app. |
| Font loads | 0. System fonts only on the frontend. No Google Fonts auto-embedded. |
Architectural rules that enforce the targets
- Server-side render by default. Every block ships a
render_callback. JS only when a block is genuinely interactive (Accordion, Tabs, Modal, Carousel) — and then via the WP Interactivity API, not React. - No wrapper divs unless structurally required. A block that renders a heading renders
<h2>, not<div class="wp-block-canvas-heading"><h2>…</h2></div>. gcStyleships as inlinestyle=on the rendered element, not as per-instance stylesheets. Inline style is one HTTP cost (the HTML itself) instead of a roundtrip for a tiny CSS file.- Per-block CSS only enqueued when the block is present on the page.
has_block()gate on everywp_enqueue_stylecall. - One shared interactivity runtime. All interactive Canvas blocks lean on the same
@wordpress/interactivitybundle that WP ships in core. - No editor-only JS on the frontend. The Block Inspector code (the Canvas Style / Visibility / Schema panels) is editor-only.
- No layout-property animations. Only
opacityandtransform. Respectsprefers-reduced-motionabsolutely.
Performance regression test in CI
A microbenchmark (composer perf) runs against a synthetic 200-block document: 195 blocks with no conditions, 5 with typical CC rules. Targets: total gate_block cost ≤ 5 ms for the 195 no-condition blocks (fast path), ≤ 10 ms for the 5 conditioned blocks, VisitorContext::get() cached subsequent calls < 0.1 ms each. Regression on any of these fails CI.
Architecture overview#
Canvas mirrors Gillish Node’s 1:1 directory tree, naming conventions, and bootstrap patterns. A developer who has read Node’s CLAUDE.md can open Canvas’s source tree and immediately know where to look for any concept — settings, hooks, options, admin pages, modules, REST routes, cron events. Same map, different territory.
Centralised RuleEvaluator + VisitorContext
Shared/RuleEvaluator.php— pure rule → bool resolver. Ops, dot-path signal lookup, unknown-signal handling per documented contract. No I/O. Promoted fromModules/ConditionalContent/in v0.3.0 so the whole block library leans on the same evaluator.Shared/VisitorContext.php— cross-block visitor signal collector. Country comes fromModules\GeoIp\GeoIp::resolve_country()(Cloudflare header → bundled DB). Request-cached so multiple blocks per page share one resolution cost.Shared/ConditionalRender.php— two-listener engine.inject_attributes()onFILTER_REGISTER_BLOCK_ARGSaddsconditions/match/renderMode/stickyCookieto any block whose manifest opts in.gate_block()onrender_block(priority 10) reads those attrs at render time, evaluates, and returns either the original HTML or''.
Atomic block design — shared UI primitives
The library is built from a shared kit, not as 100 isolated mini-products. Per-block manifest.json declares attributes, defaults, preview fixtures, and which inspector primitives the block opts into. Same manifest drives Canvas’s inspector-panel rendering AND Gutenberg block registration — one source of truth.
Shared inspector primitives (delivered as part of Phase 3 of the post-pivot plan): BoxModelControl (padding + margin per side), ColorControl (with Gutenberg HEX picker + opacity slider), NumericControl (slider + numeric + reset), OpacityControl, CollapsibleSection (accordion section with persisted open state).
Free / paid model#
Free tier (wp.org, no payment, no key)
- All Canvas primitives (Container, Columns, future layout primitives).
- ~25–30 library blocks across all categories.
- Universal Canvas Style panel on every block.
- Basic Conditional Content: 3 signals (country, device.type, logged_in), the 18 recipes, plain operators.
- Full CPT block-builder (no instance cap).
- Import / export.
- Basic AI integration via Abilities API (discovery unrestricted; mutation quota 5 calls/month).
Pro tier (direct sale, license key)
- Full library (100+ blocks).
- Full Conditional Content: all 13 signals, regex operators, state-driven controls (active / hover / focus / disabled), interaction system.
- Schema.org emission per block.
- Unlimited AI mutation calls (still rate-limited at 50/hour).
- Node integrations beyond the universal link picker.
- Future: site presets (Canvas presets, deferred to post-Phase 11) and Canvas lens (deferred to Phase 17–18 tier).
The mechanism (already built as stub)
Shared\Licensing::has_tier( string $tier ): bool returns true unconditionally today (dev mode). When the licensing layer ships (Phase 18), only this helper changes. Every Pro-gated feature already routes through this contract — no hardcoded checks, no “we’ll figure out monetisation later”.
Provider-agnostic Licensing contract
The contract surface stays fixed regardless of backend choice (Lemon Squeezy, Freemius, EDD Software Licensing, Paddle, custom):
interface LicensingContract {
public function has_tier( string $tier ): bool;
public function license_key(): string;
public function license_status(): array; // tier, expiry, sites used vs allowed, last validation
public function refresh_now(): void;
}
Activation flow: on key entry, REST call to the chosen backend’s validation endpoint, payload signed (HMAC) so it can’t be forged; result cached in wp_option (gillish_canvas_license_status) with 14-day grace window. Weekly cron re-validation; failure within grace window keeps the cached status; failure beyond grace window downgrades to free.
EU Cyber Resilience Act (CRA) readiness#
The EU CRA (Regulation (EU) 2024/2847) applies to commercial software with digital elements sold in the EU. Pro-tier Canvas is in scope. The Act is in force; reporting obligations land 2026-09-11, full applicability 2027-12-11.
Canvas’s CRA-readiness checklist:
- Published security contact.
[email protected]with a documented response window: 24h acknowledgement, 7d for critical, 30d for high. Page lives atgillish.com/security. - Responsible disclosure policy. “Report privately first; we’ll publish after the patch ships.”
- Declaration of Conformity per shipped plugin, linked from the security page. Updated at each major release.
- Support-period commitments. 5-year minimum from the release date of each major version.
- SBOM per plugin (CycloneDX format). Generated at build time, linked from the security page. Tracks
vendor-runtime/maxmind-db-reader/and any other runtime dependencies. - ENISA reporting commitments. Manufacturer-side filings tracked on the security page.
Competitive landscape#
Canvas overlaps three existing market segments. Each has a leader or two; Canvas’s wedge is the combination none of them ship.
Block libraries
GenerateBlocks (~$2M ARR), Stackable, Kadence, Spectra. Mature, polished, big libraries. Their tone is developer-friendly (GenerateBlocks especially) or WordPress-cheery (Stackable, Kadence). None ships universal Conditional Content as a first-class attribute on every block; none ships first-class block import/export.
Personalisation tools
If-So, Logic Hop, Convert Plug. Strong at the rule engine; weak at the design surface. Either ship a single block (“a personalisation wrapper”) or a separate admin page divorced from the block editor.
Page builders
Elementor, Bricks, Oxygen, Breakdance. Different category entirely — they replace Gutenberg, not extend it. Canvas is explicitly Gutenberg-native; the page-builder market is not the one Canvas competes in.
Canvas’s three-fold wedge
- Plain-language UX over every other block plugin. The brand voice is “built for humans, not developers” — no CSS literacy assumed, no spec numbers in labels.
- Universal Conditional Content as a first-class attribute. Personalisation isn’t a separate tool; it’s a property of every block.
- First-class block import/export. Community multiplier no competitor ships.
The fourth pillar — AI-native via Abilities API + MCP — is becoming a wedge as the WordPress ecosystem moves toward AI integration. Canvas’s schema-validated ability surface is meaningfully ahead of where competitors are.
What Canvas is NOT (yet)#
- Not a page builder. Canvas builds blocks; the block editor builds pages. Bricks, Elementor, Oxygen are different products.
- Not a theme. Canvas works with any block theme.
- Not an A/B testing tool. The rule engine is for “show this to that audience”, not split-testing variants.
- Not an analytics platform. CC analytics report served-variant volume; they don’t track clicks, conversions, or funnels.
- Not multisite-aware. Network-activate is explicitly blocked at the activation hook with a clear error message.
- Not a custom payment processor. Pro sales go through Lemon Squeezy / Freemius / etc.; we don’t build our own billing infrastructure.
Appendix — the 100-block catalogue#
The library’s target. Numbering reflects implementation order, roughly grouped by impact + difficulty. Every block in every category supports the Conditional Content attributes universally.
Conversion & sales (1–15)
- Pricing table (toggle monthly / yearly)
- Product comparison grid
- Affiliate Disclosure (auto-injected by Node)
- Pros & cons box
- Summary box (TL;DR)
- Animated buy button
- Discount banner with copy-to-clipboard
- Trust badges row
- Scarcity / stock notifier
- Product grid
- Amazon product card (via API)
- Checklist
- Sticky bar
- Exit-intent overlay
- Lead-magnet opt-in
Content & SEO structure (16–30)
- FAQ Schema Block (auto JSON-LD)
- Breadcrumbs
- Table of contents (floating sidebar)
- Author bio card
- Post meta
- Related posts
- Section divider (SVG shapes / waves)
- Anchor-link menu
- Glossary tooltips
- Footnotes (auto-numbered)
- Post navigation
- Search bar
- Sitemap visualiser
- Code snippet (syntax highlighting + copy)
- Modern blockquote / pull-quote
Layout & design framework (31–45)
- Advanced columns (mobile / tablet / desktop control)
- Container with overlays
- Masonry gallery
- CSS-grid layout
- Parallax background
- Lottie animation container
- Shape divider
- Responsive spacer
- Z-index layer
- Icon box
- Feature list
- Image comparison (before / after slider)
- Video popup / lightbox
- Hero section
- Team grid
Social & interactive (46–60)
- Instagram embed
- X (Twitter) embed
- YouTube playlist
- Pinterest embed
- Facebook page embed
- WhatsApp share button
- Telegram embed
- RSS reader
- Social-proof popup
- Thumbs up/down feedback
- Interactive quiz
- Poll
- Lightweight contact form
- Google Maps with custom pins
- Donation button (PayPal / Stripe)
Dynamic & contextual (61–70) — Canvas-defining
- Device-specific box
- Geo-targeted text (“Hi from [Country]”)
- Countdown timer (evergreen or fixed)
- Dynamic data table from custom fields
- User-role-gated content
- Time-of-day greetings
- Referrer-host greeting
- Weather widget
- Stock ticker
- Currency converter (uses GeoIP for default currency)
Specialised & niche (71–80)
- Event timeline
- Recipe card with Recipe schema
- Event listings
- Podcast audio player
- PDF embedder
- Count-up counter
- Star-rating grid (many-product comparisons)
- Step-by-step guide
- Comparison slider
- Logo carousel
Tools & calculators (81–90)
- BMI calculator
- Loan / mortgage calculator
- Percentage calculator
- Crypto price card
- Business hours
- Team org chart
- Interactive map with clickable regions
- Job board listing
- Real-estate property card
- Restaurant menu / price list
Advanced visual effects (91–100)
- Glassmorphism
- Neumorphism
- Typing animation
- Hover flip card
- Scroll-reveal
- Floating images
- Background-video hero
- Gradient text with animated colours
- Content reveal
- Custom-HTML wrapper (sandbox mode)