# Block Schema Evolution Opportunities

**Date opened:** 2026-05-02
**Status:** Living backlog — items shipped as v5.0.x patches
**Source:** Surfaced during the migration helper rule audit (see `class-blocks-attr-migration.php` — every rule with `EVOLUTION:` in its `purpose` note feeds this doc).

---

## What this is

When the v5.0.0 migration helper was reviewed rule-by-rule, several rules surfaced a pattern: **the canonical attr name in IM-Press's Skill spec doesn't match the attr name the block's render_callback actually reads.** The migration helper supplies a defensive alias so AI submitting the canonical name still works (additively), but the block itself should evolve to read the canonical name natively.

This doc lists those evolution opportunities so they can ship as small focused patches rather than waiting for a big-bang rename release.

**Each entry follows the format:**
- **Block** — which block needs evolution
- **Today** — what the block's render_callback currently reads
- **Should be** — the canonical name per Skill spec
- **Action** — what change is needed (typically: refactor render_callback to read canonical first, fall back to legacy)
- **Risk** — what could break if done carelessly
- **Priority** — 🔴 high (active AI confusion) / 🟡 medium / 🟢 low

---

## 🔴 High priority — actively used blocks where AI canonical doesn't match render

### 1. `imp/cta-section` — `linkUrl` ≠ `buttonUrl`
- **Today:** render reads `$attrs['buttonUrl']`
- **Should be:** read `$attrs['linkUrl']` (canonical) first, fall back to `buttonUrl`
- **Action:** add resolver `$cta_url = $attrs['linkUrl'] !== '' && $attrs['linkUrl'] !== '#' ? $attrs['linkUrl'] : $attrs['buttonUrl'];` at top of render_cta_section()
- **Risk:** existing pages stored with `buttonUrl` continue to work via fallback
- **Priority:** 🔴

### 2. `imp/callout` — same `linkUrl` ≠ `buttonUrl` issue
- **Today:** render reads `$attrs['buttonUrl']`
- **Should be:** read `$attrs['linkUrl']` first
- **Action:** add resolver, mirror cta-section approach
- **Risk:** low — additive resolver pattern
- **Priority:** 🔴

### 3. `imp/info-box` — `imageUrl` ≠ `image`
- **Today:** render reads `$attrs['image']`
- **Should be:** read `$attrs['imageUrl']` first, fall back to `image`
- **Risk:** low
- **Priority:** 🔴

### 4. `imp/photo` — `imageUrl` ≠ `url`
- **Today:** render reads `$attrs['url']`
- **Should be:** read `$attrs['imageUrl']` first, fall back to `url`
- **Risk:** medium — `url` is also used for the link destination on some photo configurations; need to disambiguate carefully
- **Priority:** 🔴

### 5. `imp/marketing-button` — `heading` ≠ `title`, `linkUrl` ≠ `url`
- **Today:** render reads `$attrs['title']` and `$attrs['url']`
- **Should be:** canonical heading + linkUrl with legacy fallback
- **Priority:** 🔴

### 6. `imp/creative-link` — `heading` ≠ `text`, `linkUrl` ≠ `url`
- **Today:** render reads `$attrs['text']` and `$attrs['url']`
- **Should be:** canonical with fallback
- **Note:** large render method with ~9 references in match() arms — use resolver pattern at top to avoid touching every arm
- **Priority:** 🔴

### 7. `imp/image-text` — `description` ≠ `text`, `imageUrl` is canonical (but block has both)
- **Today:** render reads `$attrs['text']` for body content
- **Should be:** read canonical description first
- **Priority:** 🔴

### 8. `imp/icon` — `iconName` ≠ `icon`, `iconColor` ≠ `color`, `linkUrl` ≠ `link`
- **Today:** render reads `$attrs['icon']`, `$attrs['color']`, `$attrs['link']`
- **Should be:** read canonical first with type-aware fallbacks (`icon` could be string or boolean)
- **Risk:** type-aware migration already handles the icon split at render_block_data; render_callback just needs to read iconName/showIcon canonical
- **Priority:** 🔴

### 9. `imp/modal` — `heading` ≠ `title`, `description` ≠ `content`
- **Today:** render reads `$attrs['title']` and `$attrs['content']`
- **Should be:** read canonical first
- **Priority:** 🔴

### 10. `imp/video` — `videoUrl` ≠ `url`
- **Today:** render reads `$attrs['url']`
- **Should be:** read `videoUrl` first, fall back to `url`
- **Priority:** 🔴

---

## 🟡 Medium priority — schema overlap / naming-collision resolution

### 11. `imp/pricing-table` — `items` vs `plans` overlap
- **Today:** block declares both `items` and `plans` attrs in schema
- **Should be:** pick one as canonical, deprecate the other in render
- **Action:** read canonical `items` first, fall back to `plans`. Update Skill spec to recommend `items`.
- **Risk:** existing pages may use either — additive migration handles AI cases; render fallback handles stored markup
- **Priority:** 🟡

### 12. `imp/dual-button` — denormalized → array-based
- **Today:** block stores buttons as 4 hardcoded attrs (`button1Text`, `button1Url`, `button2Text`, `button2Url`)
- **Should be:** accept `items: [{text, url}, {text, url}]` array AND optionally support legacy denormalized form
- **Action:** add `items` attr to schema; render_callback reads items first, falls back to button1/button2 mapping
- **Risk:** low — additive, both paths work
- **AI impact:** medium — this is a frequent block AI generates and the denormalized schema is awkward
- **Priority:** 🟡

### 13. `imp/dual-heading` — generic `heading` auto-split
- **Today:** block requires explicit `firstPart` + `secondPart` (and optionally `highlight`)
- **Should be:** ALSO accept a single `heading` attr that auto-splits at the last word for highlight emphasis
- **Action:** in render, if `heading` is set and firstPart/secondPart are empty, split heading on last space; firstPart = everything before, secondPart = last word
- **Risk:** low — only kicks in when explicit parts are missing
- **AI impact:** medium — AI thinks of it as "a heading" not "two parts"
- **Priority:** 🟡

### 14. `imp/team` — `members` vs `items` decision
- **Today:** block uses `members` natively
- **Should be:** decide: keep `members` as semantic-canonical (clearer than generic `items`) OR rename to `items` for cross-block uniformity
- **Recommendation:** keep `members` — semantic clarity wins for blocks where the array name has obvious domain meaning. Update Skill spec to document `members` as canonical for team specifically.
- **Priority:** 🟡 (decide-and-document, not refactor)

### 15. `imp/gallery` / `imp/logo-grid` / `imp/social-icons` — `images`/`logos`/`icons` vs `items`
- **Today:** each uses its semantic name (images, logos, icons)
- **Should be:** same decision as team — either keep semantic OR rename to items
- **Recommendation:** keep semantic names; document in Skill spec as canonical-per-block
- **Priority:** 🟡 (decide-and-document)

### 16. `imp/icon-group` — accept `items` alias
- **Today:** uses `icons` array natively
- **Should be:** accept either `items` or `icons` for consistency with how AI generates "list of icons" content
- **Action:** render reads icons first, falls back to items
- **Priority:** 🟡

---

## 🟢 Low priority — defensive-only / unlikely to bite

### 17. `imp/flip-box` — frontText not a real concern
- Block uses `frontDescription`/`backDescription` natively (semantic per-face)
- AI primed on BB/generic might use `frontText` — defensive alias exists in migration helper
- No render refactor needed; the migration helper covers AI variation
- **Priority:** 🟢 (no action — defensive alias is sufficient)

### 18. Universal `bgColor` → `backgroundColor` in render_callbacks
- 14+ blocks read `$attrs['bgColor']` directly
- Migration helper translates AI's `backgroundColor` to alias, but render still uses bgColor
- **Should be:** each block's render reads `backgroundColor` first, falls back to `bgColor`
- **Action:** sweep across blocks during normal feature work — not urgent, just cleanup
- **Priority:** 🟢

---

## Proposed new blocks (surfaced by migration audit)

### `imp/stats-row` — multi-stat display
- **Why:** AI naturally generates "300 clients | 50K users | $5M revenue" content. Today no IM-Press block expresses this — `imp/numbers` is single-value, `imp/features-grid` is wrong shape (no number-emphasis).
- **Shape:** `items: [{number, suffix?, prefix?, label, animationStyle?}]`
- **When:** when AI demand surfaces; not blocking v5.0.0
- **Priority:** 🟢

---

## How items get resolved

1. **Quick wins (🔴 high priority):** ship as v5.0.1 patch. Each is a small per-block resolver pattern (~10 lines) using the same template I applied to imp/heading and imp/button in this session.
2. **Decisions needed (🟡 medium):** John reviews the recommendation, confirms canonical choice, then ships. Mostly Skill-spec doc updates plus a small render refactor.
3. **Defensive-only (🟢 low):** rolled into normal feature work; never blocking.

---

## Validation principle

Whenever a block's render_callback is updated to read canonical names, **add a smoke test** verifying:
1. Stored markup using legacy attrs still renders correctly (back-compat)
2. New markup using canonical attrs renders identically (canonical works)
3. Both rendered outputs are byte-equivalent (resolver pattern doesn't drift)

The migration-regression test format established in this session (`/tmp/migration-regression.php` style) is the template.
