Block Type Contracts

Purpose And Scope

This document began as the Phase 1 inventory of the currently shipped published core block types in WebBlocks CMS, now also documents the Phase 2 read-only admin contract view, and records the Phase 3 gap-standardization fixes completed so far, including the Layout + Card cleanup for section, container, grid, cluster, card, and content_header, the Marketing / Structured Content cleanup for hero, columns, column_item, cta, feature-grid, and feature-item, and the Legacy / Transitional cleanup that keeps old compatibility slugs documented honestly without promoting them into the published core catalog.

Phase 1 is read-only documentation only.

  • It does not redesign block forms.
  • It does not add a DB-driven form builder.
  • It does not migrate block content.
  • It does not change public rendering.
  • It does not change how Admin -> System -> Block Types edits work today.

The current Block Types admin screen remains a catalog and metadata screen. In Phase 2 it can now open a read-only Contract modal for each listed row, but it is still not a dynamic form builder or schema editor.

Plugin Phase 3 adds declaration-only plugin block hooks through PluginBlockTypeDefinition, PluginBlockPackDefinition, and PluginBlockRegistry. Enabled plugins can expose plugin-owned handles such as analytics-tools::score-card for discovery, but these declarations do not replace the shipped core block contracts, core block views, core seeders, or block editing services. Unqualified core-style handles such as hero remain core-owned and are rejected for plugin declarations.

Definition

A Block Type Contract is the current technical agreement for how one block type behaves across CMS layers:

  • catalog identity: slug, label, category, status, and system or container metadata
  • admin form source: which Blade partial currently edits the block and which fields it exposes
  • validation and request handling: how App\Http\Requests\Admin\BlockRequest currently normalizes and validates block payloads
  • storage ownership: which values live on blocks, dedicated translation rows, block_media, or related records
  • translation ownership: which user-facing fields are locale-owned
  • shared ownership: which settings or relationships remain shared across locales
  • media or relationship ownership: direct media_id, ordered block_media, navigation lookups, or same-page relationships
  • child support: whether the block is a container and whether child types are restricted
  • public renderer source: which public Blade partial renders the block today
  • renderer root contract: whether the block owns its public root markup or relies on the generic wrapper path
  • portability and revisions: whether the current storage shape should continue to travel through revisions, clone, export/import, and promotion
  • tests and gaps: known focused coverage, unclear behavior, or compatibility debt

Current Contract Terms

Status terms used below:

  • clear: admin form, request handling, storage, and renderer mostly line up
  • mostly clear: current contract is understandable but has one notable caveat
  • transitional: current contract intentionally carries a compatibility path or mixed ownership pattern
  • needs review: current code paths disagree or behavior is underdocumented enough that Phase 2 should surface it more clearly
  • legacy/fallback: published behavior depends on a fallback or compatibility path

Storage Ownership Rules

Current and future contract work should keep these ownership rules explicit:

  • user-facing copy belongs in translation rows when the block is locale-owned
  • shared operational or settings data belongs in shared block settings or explicit relationships
  • media should use media_id or block_media ownership paths where applicable
  • avoid moving user-facing content into arbitrary settings JSON
  • keep compatibility storage paths documented when they still affect public output or imports

Shipped Sources

This Phase 1 inventory is based on the shipped source, not guesses.

  • catalog source: app/Support/Blocks/CoreBlockTypeCatalogSyncer.php
  • block edit request normalization: app/Http/Requests/Admin/BlockRequest.php
  • persistence helpers: app/Support/Blocks/BlockPayloadWriter.php, app/Support/Blocks/BlockTranslationWriter.php, app/Support/Blocks/BlockTranslationResolver.php
  • translation registry: app/Support/Blocks/BlockTranslationRegistry.php
  • admin forms: resources/views/admin/blocks/types/*.blade.php
  • public renderers: resources/views/pages/partials/blocks/*.blade.php
  • coverage references: block catalog, rendering, translation, and slug-specific tests under tests/Feature/

Published core block types currently documented here: 42.

Audit Command

Phase 1 adds a safe developer audit command:

php artisan block-types:contracts-audit
php artisan block-types:contracts-audit --json

The command is read-only.

  • it does not modify the database
  • it does not depend on installed site content
  • it reads the shipped core catalog definitions
  • it verifies shipped admin form and public renderer file presence
  • it reports translation-family metadata and basic container support

The command is a freshness aid for catalog and file-presence drift. It is not a substitute for the fuller contract notes in this document.

Phase 2 Admin View

Phase 2 exposes contract details read-only in Admin -> System -> Block Types.

  • each row can open a Block Type Contract modal
  • the modal is informational only and does not submit updates
  • it shows catalog, admin form, storage, translation, media or relationship, child, renderer, and gap details from shipped code
  • it does not make Block Types admin a schema editor or form builder
  • custom or draft block types can still open the modal, but may show No shipped contract is documented for this block type yet. when no core contract is defined

Published Block Contract Matrix

Content

<table><thead><tr><th>Slug</th><th>Label</th><th>Category</th><th>Admin form source</th><th>Translatable fields</th><th>Shared/settings fields</th><th>Media/relationship fields</th><th>Child/container behavior</th><th>Public renderer source</th><th>Renderer root contract</th><th>Current status</th><th>Tests / coverage</th><th>Known gaps / notes</th></tr></thead><tbody><tr><td><code>header</code></td><td>Header</td><td><code>content</code></td><td><code>resources/views/admin/blocks/types/header.blade.php</code></td><td><code>title</code> via text translation rows</td><td><code>variant</code> heading level; <code>settings.alignment</code>; shared anchor in <code>settings.anchor</code> with legacy <code>url</code> fallback</td><td>Same-page TOC reads anchored Header blocks</td><td>Not a container</td><td><code>resources/views/pages/partials/blocks/header.blade.php</code></td><td>Owns its root heading element</td><td><code>clear</code></td><td><code>SyncCoreBlockTypesCommandTest</code>, <code>PublicEditorialBlocksRenderingTest</code>, <code>BlockTranslationIntegrityTest</code></td><td>Canonical heading block; shared anchor contract is clear but still has legacy <code>url</code> fallback behavior.</td></tr><tr><td><code>plain_text</code></td><td>Plain Text</td><td><code>content</code></td><td><code>resources/views/admin/blocks/types/plain_text.blade.php</code></td><td><code>content</code> via text translation rows</td><td><code>settings.alignment</code></td><td>None</td><td>Not a container</td><td><code>resources/views/pages/partials/blocks/plain_text.blade.php</code></td><td>Owns its root <code>&lt;p&gt;</code></td><td><code>clear</code></td><td><code>PublicEditorialBlocksRenderingTest</code>, <code>BlockTranslationIntegrityTest</code></td><td>Simple translated body-copy primitive.</td></tr><tr><td><code>rich-text</code></td><td>Rich Text</td><td><code>content</code></td><td><code>resources/views/admin/blocks/types/rich-text.blade.php</code></td><td><code>content</code> via text translation rows</td><td>None</td><td>None</td><td>Not a container</td><td><code>resources/views/pages/partials/blocks/rich-text.blade.php</code></td><td>Owns its <code>.wb-rich-text</code> root when content exists</td><td><code>clear</code></td><td><code>RichTextBlockTest</code>, <code>PublicRichContentTest</code>, <code>BlockTranslationIntegrityTest</code></td><td>Safe HTML storage and translation ownership are clear.</td></tr><tr><td><code>code</code></td><td>Code</td><td><code>content</code></td><td><code>resources/views/admin/blocks/types/code.blade.php</code></td><td><code>title</code>, <code>subtitle</code>, <code>content</code> via text translation rows</td><td><code>settings.language</code></td><td>None</td><td>Not a container; historical child rows are preserved but new child placement is not supported</td><td><code>resources/views/pages/partials/blocks/code.blade.php</code></td><td>Owns its <code>&lt;pre&gt;&lt;code&gt;</code> root</td><td><code>clear</code></td><td><code>PublicRichContentTest</code>, <code>BlockTypePhaseThreeContractsTest</code>, <code>PageBuilderExperienceTest</code></td><td>Phase 3 aligns translated code title, label, and snippet body with the existing text-translation architecture and now ignores arbitrary historical child trees in public output.</td></tr><tr><td><code>button_link</code></td><td>Button Link</td><td><code>content</code></td><td><code>resources/views/admin/blocks/types/button_link.blade.php</code></td><td><code>title</code> label via text translation rows</td><td><code>settings.url</code>; <code>settings.target</code>; shared <code>variant</code></td><td>None</td><td>Not a container</td><td><code>resources/views/pages/partials/blocks/button_link.blade.php</code></td><td>Owns its <code>&lt;a.wb-btn&gt;</code> root</td><td><code>clear</code></td><td><code>PublicEditorialBlocksRenderingTest</code></td><td>Shared URL and target with translated label are consistent.</td></tr><tr><td><code>card</code></td><td>Card</td><td><code>layout</code></td><td><code>resources/views/admin/blocks/types/card.blade.php</code></td><td>None</td><td><code>settings.layout_name</code></td><td>Child region blocks define structure; no direct media or translation contract</td><td>Container; allowed direct children are <code>card_header</code>, <code>card_body</code>, and <code>card_footer</code> only</td><td><code>resources/views/pages/partials/blocks/card.blade.php</code></td><td>Owns its <code>&lt;article class=&quot;wb-card&quot;&gt;</code> root</td><td><code>clear</code></td><td><code>PublicEditorialBlocksRenderingTest</code>, <code>PageBuilderExperienceTest</code>, <code>BlockTypePhaseThreeContractsTest</code>, <code>BlockTranslationIntegrityTest</code>, <code>MediaVisualBlockContractsTest</code></td><td>Card is now a composable shell. It owns the card root, clears legacy content or media fields on save, and keeps only a minimal legacy render fallback for older saved rows that have no Card region children.</td></tr><tr><td><code>card_header</code></td><td>Card Header</td><td><code>layout</code></td><td><code>resources/views/admin/blocks/types/card_header.blade.php</code></td><td>None</td><td><code>settings.layout_name</code></td><td>Parent <code>card</code> relationship only</td><td>Container; may only be placed under <code>card</code>; card-region children are not allowed</td><td><code>resources/views/pages/partials/blocks/card_header.blade.php</code></td><td>Owns its <code>&lt;div class=&quot;wb-card-header&quot;&gt;</code> root</td><td><code>clear</code></td><td><code>PublicEditorialBlocksRenderingTest</code>, <code>PageBuilderExperienceTest</code>, <code>BlockTypePhaseThreeContractsTest</code></td><td>Composable Card region block for header content.</td></tr><tr><td><code>card_body</code></td><td>Card Body</td><td><code>layout</code></td><td><code>resources/views/admin/blocks/types/card_body.blade.php</code></td><td>None</td><td><code>settings.layout_name</code></td><td>Parent <code>card</code> relationship only</td><td>Container; may only be placed under <code>card</code>; card-region children are not allowed</td><td><code>resources/views/pages/partials/blocks/card_body.blade.php</code></td><td>Owns its <code>&lt;div class=&quot;wb-card-body&quot;&gt;</code> root</td><td><code>clear</code></td><td><code>PublicEditorialBlocksRenderingTest</code>, <code>PageBuilderExperienceTest</code>, <code>BlockTypePhaseThreeContractsTest</code></td><td>Composable Card region block for main body content.</td></tr><tr><td><code>card_footer</code></td><td>Card Footer</td><td><code>layout</code></td><td><code>resources/views/admin/blocks/types/card_footer.blade.php</code></td><td>None</td><td><code>settings.layout_name</code></td><td>Parent <code>card</code> relationship only</td><td>Container; may only be placed under <code>card</code>; card-region children are not allowed</td><td><code>resources/views/pages/partials/blocks/card_footer.blade.php</code></td><td>Owns its <code>&lt;div class=&quot;wb-card-footer&quot;&gt;</code> root</td><td><code>clear</code></td><td><code>PublicEditorialBlocksRenderingTest</code>, <code>PageBuilderExperienceTest</code>, <code>BlockTypePhaseThreeContractsTest</code></td><td>Composable Card region block for footer or action content.</td></tr><tr><td><code>stat-card</code></td><td>Stat Card</td><td><code>content</code></td><td><code>resources/views/admin/blocks/types/stat-card.blade.php</code></td><td><code>title</code>, <code>subtitle</code>, <code>content</code> via text translation rows</td><td>Canonical <code>url</code> remains shared on the block row</td><td>None</td><td>Not a container</td><td><code>resources/views/pages/partials/blocks/stat-card.blade.php</code></td><td>Owns its stat-card root</td><td><code>clear</code></td><td><code>StatCardTest</code>, <code>BlockTranslationIntegrityTest</code></td><td>Phase 3 keeps the existing optional URL field and now renders a simple public link when present.</td></tr><tr><td><code>image</code></td><td>Image</td><td><code>content</code></td><td><code>resources/views/admin/blocks/types/image.blade.php</code></td><td><code>caption</code>, <code>alt text</code> via image translation rows</td><td>Shared <code>media_id</code>; shared canonical <code>url</code></td><td>Direct image media relation via <code>media_id</code></td><td>Not a container; historical child rows are preserved but new child placement is not supported</td><td><code>resources/views/pages/partials/blocks/image.blade.php</code></td><td>Owns its semantic <code>&lt;figure&gt;</code> root when media exists</td><td><code>clear</code></td><td><code>MediaVisualBlockContractsTest</code>, <code>PublicMediaBlocksTest</code>, <code>BlockTranslationIntegrityTest</code></td><td>Phase 3 aligns Image with the existing image-translation architecture so caption and alt text are locale-owned while selected media and optional link URL remain shared.</td></tr><tr><td><code>gallery</code></td><td>Gallery</td><td><code>content</code></td><td><code>resources/views/admin/blocks/types/gallery.blade.php</code> plus <code>resources/views/admin/blocks/types/partials/gallery-items-editor.blade.php</code></td><td>Per-gallery-item <code>alt_text</code>, <code>caption</code>, <code>overlay_title</code>, and <code>overlay_text</code> via <code>block_gallery_item_translations</code></td><td>Shared gallery presentation settings plus ordered <code>block_media</code> gallery-item relations</td><td>Ordered <code>block_media</code> rows with role <code>gallery_item</code>; locale-owned gallery item copy lives on <code>block_gallery_item_translations</code>; legacy stored block <code>title</code>/<code>subtitle</code> values may still exist but are ignored by public rendering</td><td>Not a container; historical child rows are preserved but new child placement is not supported</td><td><code>resources/views/pages/partials/blocks/gallery.blade.php</code></td><td>Owns its gallery wrapper root and registers one viewer modal under <code>#wb-overlay-root</code> when lightbox is enabled</td><td><code>clear</code></td><td><code>MediaVisualBlockContractsTest</code>, <code>PublicMediaBlocksTest</code>, <code>PageBuilderExperienceTest</code>, <code>SiteCloneServiceTest</code>, <code>SiteExportImportTest</code>, <code>SitePromotionTest</code>, <code>ReconstructionIntegrityTest</code>, <code>SharedSlotRevisionTest</code></td><td>Gallery is now a media-collection block. The normal admin form uses a compact list-row editor instead of the older selected-assets grid. Its nested <code>Add Gallery Items</code> picker stays on the shared admin <code>#wb-overlay-root</code> contract so WebBlocks UI owns the stacked modal lifecycle, and long compact result lists keep natural row height while the modal body remains the scroll container. Public variants are distinct: <code>grid</code> keeps equal cells, <code>masonry</code> uses CSS columns with natural image height, and <code>collage</code> keeps the featured-first composition. Legacy saved <code>masonary</code> values are still accepted and normalized to the canonical <code>masonry</code> path. Gallery no longer owns intro heading or paragraph output; editors should place <code>content_header</code> plus <code>plain_text</code> or <code>rich-text</code> before Gallery when section copy is needed. Legacy settings-based fallback items remain readable for older content.</td></tr><tr><td><code>download</code></td><td>Download</td><td><code>content</code></td><td><code>resources/views/admin/blocks/types/download.blade.php</code></td><td><code>title</code>, <code>subtitle</code> via text translation rows</td><td>Shared <code>media_id</code>; shared <code>variant</code></td><td>Direct download media relation via <code>media_id</code></td><td>Not a container; historical child rows are preserved but new child placement is not supported</td><td><code>resources/views/pages/partials/blocks/download.blade.php</code></td><td>Owns its CTA wrapper root when media exists</td><td><code>clear</code></td><td><code>MediaVisualBlockContractsTest</code>, <code>PublicMediaBlocksTest</code></td><td>Phase 3 keeps the selected download media and button variant shared while moving visible label and helper copy into the existing text-translation path.</td></tr><tr><td><code>file</code></td><td>File</td><td><code>content</code></td><td><code>resources/views/admin/blocks/types/file.blade.php</code></td><td><code>title</code>, <code>content</code> via text translation rows</td><td>Shared <code>media_id</code>; shared canonical <code>url</code></td><td>Direct media relation via <code>media_id</code> with external URL fallback</td><td>Not a container; historical child rows are preserved but new child placement is not supported</td><td><code>resources/views/pages/partials/blocks/file.blade.php</code></td><td>Owns its file-card root</td><td><code>clear</code></td><td><code>MediaVisualBlockContractsTest</code>, <code>PublicMediaBlocksTest</code></td><td>Phase 3 productizes the shipped File renderer with a dedicated admin form and keeps shared file-source ownership explicit between Media and external URL fallback.</td></tr><tr><td><code>video</code></td><td>Video</td><td><code>content</code></td><td><code>resources/views/admin/blocks/types/video.blade.php</code></td><td><code>title</code>, <code>content</code> via text translation rows</td><td>Shared <code>media_id</code>; shared canonical <code>url</code></td><td>Direct media relation via <code>media_id</code> with safe external URL fallback</td><td>Not a container; historical child rows are preserved but new child placement is not supported</td><td><code>resources/views/pages/partials/blocks/video.blade.php</code></td><td>Owns its video-card root</td><td><code>clear</code></td><td><code>MediaVisualBlockContractsTest</code>, <code>PublicMediaBlocksTest</code></td><td>Phase 3 adds the missing admin form and save path so uploaded video media, safe external provider URLs, translated visible copy, and public fallback behavior now describe the same contract.</td></tr><tr><td><code>audio</code></td><td>Audio</td><td><code>content</code></td><td><code>resources/views/admin/blocks/types/audio.blade.php</code></td><td><code>title</code>, <code>content</code> via text translation rows</td><td>Shared <code>media_id</code>; shared canonical <code>url</code></td><td>Direct media relation via <code>media_id</code> with external URL fallback</td><td>Not a container; historical child rows are preserved but new child placement is not supported</td><td><code>resources/views/pages/partials/blocks/audio.blade.php</code></td><td>Owns its audio-card root</td><td><code>clear</code></td><td><code>MediaVisualBlockContractsTest</code>, <code>PublicMediaBlocksTest</code></td><td>Phase 3 adds the missing admin form and save path so uploaded audio media, translated visible copy, and safe no-empty-controls rendering now match the documented contract.</td></tr><tr><td><code>table</code></td><td>Table</td><td><code>content</code></td><td><code>resources/views/admin/blocks/types/table.blade.php</code></td><td><code>title</code>, <code>content</code> via text translation rows</td><td>Shared <code>variant</code>; renderer also checks <code>settings.rows</code> fallback</td><td>None</td><td>Not a container; historical child rows are preserved but new child placement is not supported</td><td><code>resources/views/pages/partials/blocks/table.blade.php</code></td><td>Owns its table wrapper root</td><td><code>mostly clear</code></td><td><code>PublicRichContentTest</code>, <code>BlockTypePhaseThreeContractsTest</code>, <code>PageBuilderExperienceTest</code></td><td>Phase 3 aligns translated title and row-copy ownership with the existing text-translation architecture and now ignores arbitrary historical child trees in public output; the legacy <code>settings.rows</code> fallback path remains documented.</td></tr><tr><td><code>quote</code></td><td>Quote</td><td><code>content</code></td><td><code>resources/views/admin/blocks/types/quote.blade.php</code></td><td>None in the registry today</td><td>Canonical <code>title</code>, <code>subtitle</code>, <code>content</code>, <code>variant</code></td><td>None</td><td>Not a container; historical child rows are preserved but new child placement is not supported</td><td><code>resources/views/pages/partials/blocks/quote.blade.php</code></td><td>Owns its quote root</td><td><code>clear</code></td><td><code>PublicRichContentTest</code>, <code>PageBuilderExperienceTest</code></td><td>Phase 3 stops treating Quote as a layout/container wrapper in public output while preserving existing saved child rows in admin block trees.</td></tr><tr><td><code>hero</code></td><td>Hero</td><td><code>content</code></td><td><code>resources/views/admin/blocks/types/hero.blade.php</code></td><td><code>title</code>, <code>subtitle</code>, <code>content</code> via text translation rows</td><td>Shared <code>variant</code>; <code>settings.layout</code>; <code>settings.title_tag</code></td><td>Child <code>button</code> blocks for managed CTAs</td><td>Container; only <code>button</code> children</td><td><code>resources/views/pages/partials/blocks/hero.blade.php</code></td><td>Owns its promo <code>&lt;section&gt;</code> root</td><td><code>transitional</code></td><td><code>PublicHeroBlockRenderingTest</code>, <code>PageBuilderExperienceTest</code>, <code>BlockTypePhaseThreeContractsTest</code></td><td>Hero is now a published source-backed core contract. Intro copy is locale-owned, CTA labels are translated on child buttons, and CTA URLs remain shared. Legacy content fallbacks remain readable where canonical translated fields are empty.</td></tr><tr><td><code>columns</code></td><td>Columns</td><td><code>content</code></td><td><code>resources/views/admin/blocks/types/columns.blade.php</code></td><td><code>title</code>, <code>subtitle</code>, <code>content</code> via text translation rows</td><td>Shared <code>variant</code></td><td>Child <code>column_item</code> blocks</td><td>Container; only <code>column_item</code> children</td><td><code>resources/views/pages/partials/blocks/columns.blade.php</code></td><td>Owns its structured-content <code>&lt;section&gt;</code> root</td><td><code>clear</code></td><td><code>PublicColumnsRenderingTest</code>, <code>PageBuilderExperienceTest</code>, <code>BlockTypePhaseThreeContractsTest</code></td><td>Columns is now a published first-class structured content block. Intro copy is locale-owned while variant, child order, and child URLs stay shared.</td></tr><tr><td><code>column_item</code></td><td>Column Item</td><td><code>content</code></td><td><code>resources/views/admin/blocks/types/column_item.blade.php</code></td><td><code>title</code>, <code>subtitle</code>, <code>content</code> via text translation rows</td><td>Shared canonical <code>url</code></td><td>Parent <code>columns</code> relationship</td><td>Not a container</td><td><code>resources/views/pages/partials/blocks/column_item.blade.php</code></td><td>Parent-driven item root varies by Columns variant</td><td><code>clear</code></td><td><code>PublicColumnsRenderingTest</code>, <code>PageBuilderExperienceTest</code>, <code>BlockTypePhaseThreeContractsTest</code></td><td>Column Item is now a published supporting child contract for Columns. Locale edits update item copy without overwriting shared URLs.</td></tr><tr><td><code>cta</code></td><td>CTA</td><td><code>content</code></td><td><code>resources/views/admin/blocks/types/cta.blade.php</code></td><td><code>title</code>, <code>subtitle</code>, <code>content</code> via text translation rows</td><td>Shared <code>variant</code></td><td>Child <code>button</code> blocks for managed CTAs</td><td>Container; only <code>button</code> children</td><td><code>resources/views/pages/partials/blocks/cta.blade.php</code></td><td>Owns its promo <code>&lt;section&gt;</code> root</td><td><code>clear</code></td><td><code>PublicHeroBlockRenderingTest</code>, <code>PageBuilderExperienceTest</code>, <code>BlockTypePhaseThreeContractsTest</code></td><td>CTA is now a published promo contract with locale-owned copy and CTA labels, shared CTA URLs, and a root-owning public renderer.</td></tr><tr><td><code>feature-grid</code></td><td>Feature Grid</td><td><code>content</code></td><td><code>resources/views/admin/blocks/types/feature-grid.blade.php</code></td><td><code>title</code>, <code>subtitle</code>, <code>content</code> via text translation rows</td><td>None beyond shared child structure</td><td>Child <code>feature-item</code> blocks with legacy-compatible <code>column_item</code> support</td><td>Container; allowed children are <code>feature-item</code> and <code>column_item</code></td><td><code>resources/views/pages/partials/blocks/feature-grid.blade.php</code></td><td>Delegates to the Columns cards presentation and still uses the generic public wrapper</td><td><code>transitional</code></td><td><code>PublicHeroBlockRenderingTest</code>, <code>PageBuilderExperienceTest</code>, <code>BlockTypePhaseThreeContractsTest</code></td><td>Feature Grid is now a published compatibility alias because it has real shipped admin and render paths, but it still intentionally delegates to the Columns cards contract.</td></tr><tr><td><code>feature-item</code></td><td>Feature Item</td><td><code>content</code></td><td><code>resources/views/admin/blocks/types/feature-item.blade.php</code></td><td><code>title</code>, <code>content</code> via text translation rows</td><td>Shared canonical <code>url</code></td><td>Parent <code>feature-grid</code> relationship</td><td>Not a container</td><td><code>resources/views/pages/partials/blocks/feature-item.blade.php</code></td><td>Delegates to the Column Item cards presentation</td><td><code>transitional</code></td><td><code>PublicHeroBlockRenderingTest</code>, <code>PageBuilderExperienceTest</code>, <code>BlockTypePhaseThreeContractsTest</code></td><td>Feature Item is now a published supporting child contract for Feature Grid while still sharing the existing Column Item card shell.</td></tr></tbody></table>

Layout

<table><thead><tr><th>Slug</th><th>Label</th><th>Category</th><th>Admin form source</th><th>Translatable fields</th><th>Shared/settings fields</th><th>Media/relationship fields</th><th>Child/container behavior</th><th>Public renderer source</th><th>Renderer root contract</th><th>Current status</th><th>Tests / coverage</th><th>Known gaps / notes</th></tr></thead><tbody><tr><td><code>section</code></td><td>Section</td><td><code>layout</code></td><td><code>resources/views/admin/blocks/types/section.blade.php</code></td><td>None</td><td><code>settings.layout_name</code>; <code>settings.spacing</code></td><td>Child blocks only</td><td>Container; no explicit child whitelist</td><td><code>resources/views/pages/partials/blocks/section.blade.php</code></td><td>Owns its root <code>&lt;section&gt;</code></td><td><code>clear</code></td><td><code>PublicEditorialBlocksRenderingTest</code>, <code>PublicLayoutStructureTest</code>, <code>PageBuilderExperienceTest</code>, <code>BlockTypePhaseThreeContractsTest</code></td><td>Phase 3 keeps Section layout-only: shared layout settings stay canonical, it owns the semantic <code>&lt;section&gt;</code> root, and it does not move user-facing copy into arbitrary settings.</td></tr><tr><td><code>container</code></td><td>Container</td><td><code>layout</code></td><td><code>resources/views/admin/blocks/types/container.blade.php</code></td><td>None</td><td><code>settings.layout_name</code>; <code>settings.width</code>; <code>settings.flow</code></td><td>Child blocks only</td><td>Container; no explicit child whitelist</td><td><code>resources/views/pages/partials/blocks/container.blade.php</code></td><td>Owns its root container <code>&lt;div&gt;</code></td><td><code>clear</code></td><td><code>PublicEditorialBlocksRenderingTest</code>, <code>PublicLayoutStructureTest</code>, <code>PageBuilderExperienceTest</code>, <code>BlockTypePhaseThreeContractsTest</code></td><td>Legacy default still falls back to stacked flow when unset, while explicit <code>Flow: None</code> remains the layout-neutral composition path.</td></tr><tr><td><code>cluster</code></td><td>Cluster</td><td><code>layout</code></td><td><code>resources/views/admin/blocks/types/cluster.blade.php</code></td><td>None</td><td><code>settings.layout_name</code>; <code>settings.gap</code>; <code>settings.alignment</code>; <code>settings.items_alignment</code>; <code>settings.wrap</code>; <code>settings.width</code></td><td>Child blocks only</td><td>Container; no explicit child whitelist</td><td><code>resources/views/pages/partials/blocks/cluster.blade.php</code></td><td>Owns its root cluster <code>&lt;div&gt;</code></td><td><code>clear</code></td><td><code>PublicEditorialBlocksRenderingTest</code>, <code>PublicLayoutStructureTest</code>, <code>PageBuilderExperienceTest</code>, <code>BlockTypePhaseThreeContractsTest</code></td><td>Shared layout settings stay explicit and renderer-owned, including the existing navbar-compatible full-width, between, center, nowrap composition path without adding navbar-specific logic.</td></tr><tr><td><code>grid</code></td><td>Grid</td><td><code>layout</code></td><td><code>resources/views/admin/blocks/types/grid.blade.php</code></td><td>None</td><td><code>settings.layout_name</code>; <code>settings.columns</code>; <code>settings.gap</code></td><td>Child blocks only</td><td>Container; no explicit child whitelist</td><td><code>resources/views/pages/partials/blocks/grid.blade.php</code></td><td>Owns its root grid <code>&lt;div&gt;</code></td><td><code>clear</code></td><td><code>PublicEditorialBlocksRenderingTest</code>, <code>PublicLayoutStructureTest</code>, <code>PageBuilderExperienceTest</code>, <code>BlockTypePhaseThreeContractsTest</code></td><td>Shared grid settings stay canonical, and public output remains limited to shipped <code>wb-grid-<em></code> and supported <code>wb-gap-</em></code> class mappings.</td></tr></tbody></table>

Pattern

<table><thead><tr><th>Slug</th><th>Label</th><th>Category</th><th>Admin form source</th><th>Translatable fields</th><th>Shared/settings fields</th><th>Media/relationship fields</th><th>Child/container behavior</th><th>Public renderer source</th><th>Renderer root contract</th><th>Current status</th><th>Tests / coverage</th><th>Known gaps / notes</th></tr></thead><tbody><tr><td><code>content_header</code></td><td>Content Header</td><td><code>pattern</code></td><td><code>resources/views/admin/blocks/types/content_header.blade.php</code></td><td><code>title</code>, <code>subtitle</code>, <code>meta</code> via text translation rows</td><td><code>settings.alignment</code></td><td><code>meta</code> is stored as structured list content</td><td>Not a container</td><td><code>resources/views/pages/partials/blocks/content_header.blade.php</code></td><td>Owns its root <code>&lt;header&gt;</code></td><td><code>clear</code></td><td><code>PublicEditorialBlocksRenderingTest</code>, <code>PageBuilderExperienceTest</code>, <code>BlockTypePhaseThreeContractsTest</code>, <code>BlockTranslationIntegrityTest</code></td><td>Content Header is a content-shell pattern block. It keeps locale-owned title, intro, and meta copy, always renders its title as H1, ignores any legacy saved heading-level values at render time, and keeps only alignment shared.</td></tr><tr><td><code>alert</code></td><td>Alert</td><td><code>pattern</code></td><td><code>resources/views/admin/blocks/types/alert.blade.php</code></td><td><code>title</code>, <code>content</code> via text translation rows</td><td><code>settings.variant</code></td><td>None</td><td>Not a container</td><td><code>resources/views/pages/partials/blocks/alert.blade.php</code></td><td>Owns its alert root</td><td><code>clear</code></td><td><code>PublicEditorialBlocksRenderingTest</code>, <code>BlockTranslationIntegrityTest</code></td><td>Shared variant and translated copy line up cleanly.</td></tr></tbody></table>
<table><thead><tr><th>Slug</th><th>Label</th><th>Category</th><th>Admin form source</th><th>Translatable fields</th><th>Shared/settings fields</th><th>Media/relationship fields</th><th>Child/container behavior</th><th>Public renderer source</th><th>Renderer root contract</th><th>Current status</th><th>Tests / coverage</th><th>Known gaps / notes</th></tr></thead><tbody><tr><td><code>link-list</code></td><td>Link List</td><td><code>navigation</code></td><td><code>resources/views/admin/blocks/types/link-list.blade.php</code></td><td><code>title</code>, <code>subtitle</code>, <code>content</code> via text translation rows</td><td>None</td><td>Child <code>link-list-item</code> blocks</td><td>Container; only <code>link-list-item</code> children</td><td><code>resources/views/pages/partials/blocks/link-list.blade.php</code></td><td>Owns its <code>.wb-link-list</code> root when child rows exist</td><td><code>clear</code></td><td><code>LinkListBlockTest</code>, <code>PublicEditorialBlocksRenderingTest</code>, <code>BlockTranslationIntegrityTest</code>, <code>BlockTypePhaseThreeContractsTest</code></td><td>Phase 3 now renders the existing translated intro copy above the public link list without changing child item behavior.</td></tr><tr><td><code>link-list-item</code></td><td>Link List Item</td><td><code>navigation</code></td><td><code>resources/views/admin/blocks/types/link-list-item.blade.php</code></td><td>Required <code>title</code>, optional <code>subtitle</code>, optional <code>content</code> via text translation rows</td><td>Required shared <code>url</code></td><td>Parent <code>link-list</code> relationship</td><td>Not a container</td><td><code>resources/views/pages/partials/blocks/link-list-item.blade.php</code></td><td>Owns its row link root and omits the description element when content is blank</td><td><code>clear</code></td><td><code>LinkListBlockTest</code>, <code>PageBuilderExperienceTest</code>, <code>PublicEditorialBlocksRenderingTest</code>, <code>BlockTranslationIntegrityTest</code></td><td>Shared URL with translated row copy is consistent.</td></tr><tr><td><code>toc</code></td><td>TOC</td><td><code>navigation</code></td><td><code>resources/views/admin/blocks/types/toc.blade.php</code></td><td>None</td><td>Canonical <code>title</code> only</td><td>Same-page published <code>header</code> blocks with valid anchors</td><td>Not a container; historical child rows are preserved but new child placement is not supported</td><td><code>resources/views/pages/partials/blocks/toc.blade.php</code></td><td>Owns its generated TOC wrapper when headings exist</td><td><code>clear</code></td><td><code>PublicRichContentTest</code>, <code>PageBuilderExperienceTest</code></td><td>Phase 3 keeps TOC focused on discovered page headings only and no longer treats arbitrary child blocks as public TOC content.</td></tr><tr><td><code>breadcrumb</code></td><td>Breadcrumb</td><td><code>navigation</code></td><td><code>resources/views/admin/blocks/types/breadcrumb.blade.php</code></td><td>None</td><td>Shared <code>settings.home_label</code>; <code>settings.include_current</code></td><td>Current page, site, and locale breadcrumb context</td><td>Not a container</td><td><code>resources/views/pages/partials/blocks/breadcrumb.blade.php</code></td><td>Owns its <code>&lt;nav.wb-breadcrumb&gt;</code> root</td><td><code>clear</code></td><td><code>PublicEditorialBlocksRenderingTest</code>, <code>PageBuilderExperienceTest</code></td><td>Phase 3 preserves the exposed breadcrumb settings through the shared BlockRequest path so form behavior now matches persistence and rendering.</td></tr><tr><td><code>header-actions</code></td><td>Header Actions</td><td><code>navigation</code></td><td><code>resources/views/admin/blocks/types/header-actions.blade.php</code></td><td>None</td><td>Shared <code>settings.show_mode_toggle</code>; <code>settings.show_accent_toggle</code>; <code>settings.show_search</code></td><td>Search route plus client-side UI hooks</td><td>Not a container</td><td><code>resources/views/pages/partials/blocks/header-actions.blade.php</code></td><td>Owns its inner action cluster only</td><td><code>clear</code></td><td><code>PublicEditorialBlocksRenderingTest</code></td><td>System utility block; no translation ownership by design.</td></tr><tr><td><code>sticky-navbar</code></td><td>Navbar</td><td><code>navigation</code></td><td><code>resources/views/admin/blocks/types/sticky-navbar.blade.php</code></td><td>None</td><td><code>settings.layout_name</code>; <code>settings.sticky_mode</code></td><td>Nested navbar child blocks</td><td>Container; allowed children are <code>container</code>, <code>cluster</code>, <code>header</code>, <code>plain_text</code>, <code>rich-text</code>, <code>button_link</code>, <code>navbar-brand</code>, <code>navbar-navigation</code>, <code>header-actions</code>, <code>search-form</code></td><td><code>resources/views/pages/partials/blocks/sticky-navbar.blade.php</code></td><td>Public renderer owns the outer <code>&lt;nav.wb-navbar&gt;</code> root</td><td><code>clear</code></td><td><code>PublicEditorialBlocksRenderingTest</code>, <code>PublicLayoutStructureTest</code>, <code>BlockTypePhaseThreeContractsTest</code></td><td>Phase 3 aligns the persisted <code>sticky-navbar</code> slug with <code>Block::ownsPublicRoot()</code> so Navbar no longer receives an extra generic public block wrapper.</td></tr><tr><td><code>navbar-brand</code></td><td>Navbar Brand</td><td><code>navigation</code></td><td><code>resources/views/admin/blocks/types/navbar-brand.blade.php</code></td><td><code>title</code>, <code>subtitle</code> via text translation rows</td><td>Shared <code>settings.url</code>; <code>settings.target</code>; <code>settings.aria_label</code></td><td>Shared logo media via <code>media_id</code>; site home URL fallback</td><td>Not a container</td><td><code>resources/views/pages/partials/blocks/navbar-brand.blade.php</code></td><td>Owns the inner brand link only</td><td><code>clear</code></td><td><code>PublicEditorialBlocksRenderingTest</code>, <code>PageBuilderExperienceTest</code>, <code>BlockTranslationIntegrityTest</code>, <code>BlockTypePhaseThreeContractsTest</code></td><td>Phase 3 aligns admin save behavior with the shipped renderer contract: explicit saved URL wins, otherwise the current site home path is used when available, then <code>/</code> as the final safe fallback.</td></tr><tr><td><code>navbar-navigation</code></td><td>Navbar Navigation</td><td><code>navigation</code></td><td><code>resources/views/admin/blocks/types/navbar-navigation.blade.php</code></td><td>None</td><td>Canonical <code>title</code> as shared ARIA label; <code>settings.menu_key</code></td><td>Shared <code>NavigationItem</code> menu tree</td><td>Not a container</td><td><code>resources/views/pages/partials/blocks/navbar-navigation.blade.php</code></td><td>Owns the inner navigation wrapper only</td><td><code>clear</code></td><td><code>PublicEditorialBlocksRenderingTest</code></td><td>Shared menu binding and ARIA label are current product-owned behavior.</td></tr><tr><td><code>sidebar-brand</code></td><td>Sidebar Brand</td><td><code>navigation</code></td><td><code>resources/views/admin/blocks/types/sidebar-brand.blade.php</code></td><td><code>title</code>, <code>subtitle</code> via text translation rows</td><td>Shared <code>settings.url</code>; <code>settings.target</code>; <code>settings.aria_label</code></td><td>Shared logo media via <code>media_id</code>; site home URL fallback</td><td>Not a container</td><td><code>resources/views/pages/partials/blocks/sidebar-brand.blade.php</code></td><td>Owns the inner brand link only</td><td><code>clear</code></td><td><code>PublicEditorialBlocksRenderingTest</code>, <code>PageBuilderExperienceTest</code>, <code>BlockTranslationIntegrityTest</code>, <code>BlockTypePhaseThreeContractsTest</code></td><td>Phase 3 gives Sidebar Brand the same logo-only accessible-name fallback order as Navbar Brand and the same conservative shared URL fallback contract.</td></tr><tr><td><code>sidebar-navigation</code></td><td>Sidebar Navigation</td><td><code>navigation</code></td><td><code>resources/views/admin/blocks/types/sidebar-navigation.blade.php</code></td><td><code>title</code> via text translation rows</td><td>Shared <code>settings.menu_key</code>; <code>settings.layout_name</code>; <code>settings.show_icons</code>; <code>settings.active_matching</code></td><td>Either CMS <code>NavigationItem</code> tree or manual child blocks</td><td>Container; only <code>sidebar-nav-item</code> and <code>sidebar-nav-group</code> children</td><td><code>resources/views/pages/partials/blocks/sidebar-navigation.blade.php</code></td><td>Owns its <code>&lt;nav.wb-sidebar-nav&gt;</code> root</td><td><code>clear</code></td><td><code>PublicEditorialBlocksRenderingTest</code>, <code>BlockTranslationIntegrityTest</code></td><td>Shared menu-mode settings and manual child mode are both explicit.</td></tr><tr><td><code>sidebar-nav-item</code></td><td>Sidebar Nav Item</td><td><code>navigation</code></td><td><code>resources/views/admin/blocks/types/sidebar-nav-item.blade.php</code></td><td><code>title</code> via text translation rows</td><td>Shared <code>settings.url</code>; <code>settings.target</code>; <code>settings.icon</code>; <code>settings.active_mode</code>; <code>settings.manual_active</code></td><td>Shared icon catalog slug; parent sidebar relationship</td><td>Not a container</td><td><code>resources/views/pages/partials/blocks/sidebar-nav-item.blade.php</code></td><td>Owns its sidebar link root</td><td><code>clear</code></td><td><code>PublicEditorialBlocksRenderingTest</code>, <code>BlockTranslationIntegrityTest</code></td><td>Shared link behavior plus translated label line up well.</td></tr><tr><td><code>sidebar-nav-group</code></td><td>Sidebar Nav Group</td><td><code>navigation</code></td><td><code>resources/views/admin/blocks/types/sidebar-nav-group.blade.php</code></td><td><code>title</code> via text translation rows</td><td>Shared <code>settings.icon</code>; <code>settings.initially_open</code>; <code>settings.layout_name</code></td><td>Child <code>sidebar-nav-item</code> blocks; icon catalog slug</td><td>Container; only <code>sidebar-nav-item</code> children</td><td><code>resources/views/pages/partials/blocks/sidebar-nav-group.blade.php</code></td><td>Owns its <code>.wb-nav-group</code> root</td><td><code>clear</code></td><td><code>PublicEditorialBlocksRenderingTest</code>, <code>PageBuilderExperienceTest</code>, <code>BlockTranslationIntegrityTest</code>, <code>BlockTypePhaseThreeContractsTest</code></td><td>Phase 3 keeps the shipped WebBlocks UI nav-group wrapper contract while nested manual child links now reuse the same sidebar item semantics for href, target, icon, and active-state output.</td></tr><tr><td><code>search-form</code></td><td>Search Form</td><td><code>navigation</code></td><td><code>resources/views/admin/blocks/types/search-form.blade.php</code></td><td><code>title</code>, <code>subtitle</code>, <code>content</code> via text translation rows</td><td>Shared <code>variant</code>; <code>settings.show_button</code></td><td>Search route, site, and locale context</td><td>Not a container</td><td><code>resources/views/pages/partials/blocks/search-form.blade.php</code></td><td>Owns its <code>&lt;form role=&quot;search&quot;&gt;</code> root</td><td><code>clear</code></td><td><code>SearchFormTest</code>, <code>PublicEditorialBlocksRenderingTest</code>, <code>BlockTranslationIntegrityTest</code></td><td>Shared button-display settings and translated labels are explicit.</td></tr><tr><td><code>sidebar-footer</code></td><td>Sidebar Footer</td><td><code>navigation</code></td><td><code>resources/views/admin/blocks/types/sidebar-footer.blade.php</code></td><td><code>title</code>, <code>subtitle</code>, <code>content</code> via text translation rows</td><td>Shared <code>settings.variant</code></td><td>None</td><td>Not a container</td><td><code>resources/views/pages/partials/blocks/sidebar-footer.blade.php</code></td><td>Owns its inner footer block root</td><td><code>clear</code></td><td><code>PublicEditorialBlocksRenderingTest</code>, <code>BlockTranslationIntegrityTest</code></td><td>Shared variant with translated copy is straightforward.</td></tr></tbody></table>

Forms

<table><thead><tr><th>Slug</th><th>Label</th><th>Category</th><th>Admin form source</th><th>Translatable fields</th><th>Shared/settings fields</th><th>Media/relationship fields</th><th>Child/container behavior</th><th>Public renderer source</th><th>Renderer root contract</th><th>Current status</th><th>Tests / coverage</th><th>Known gaps / notes</th></tr></thead><tbody><tr><td><code>contact_form</code></td><td>Contact Form</td><td><code>form</code></td><td><code>resources/views/admin/blocks/types/contact_form.blade.php</code></td><td><code>title</code>, <code>content</code>, <code>submit_label</code>, <code>success_message</code> via contact form translation rows</td><td>Shared <code>settings.recipient_email</code>; <code>settings.send_email_notification</code>; <code>settings.store_submissions</code></td><td><code>contact_messages</code> submissions relate back to the block and page</td><td>Not a container</td><td><code>resources/views/pages/partials/blocks/contact_form.blade.php</code></td><td>Owns its <code>section.wb-card</code> root and emits a native CSRF-protected form posting to <code>/contact-messages</code></td><td><code>clear</code></td><td><code>ContactFormModuleTest</code>, <code>InternalContentApiTest</code>, <code>ContactMailDiagnoseCommandTest</code></td><td>The renderer emits a CMS-owned hidden generated anti-spam check field that is not normal visitor input and should not be created manually. Filled check-field submissions return generic success without storage or notification. Legitimate messages are stored before notification, scored spam is retained for admin review, recipient fallback is block, site, <code>CONTACT_RECIPIENT_EMAIL</code>, then <code>MAIL_FROM_ADDRESS</code>, and Contact pages should not use Trusted HTML/raw forms/<code>mailto:</code> as substitutes.</td></tr></tbody></table>

Advanced

<table><thead><tr><th>Slug</th><th>Label</th><th>Category</th><th>Admin form source</th><th>Translatable fields</th><th>Shared/settings fields</th><th>Media/relationship fields</th><th>Child/container behavior</th><th>Public renderer source</th><th>Renderer root contract</th><th>Current status</th><th>Tests / coverage</th><th>Known gaps / notes</th></tr></thead><tbody><tr><td><code>html</code></td><td>HTML (Trusted)</td><td><code>advanced</code></td><td><code>resources/views/admin/blocks/types/html.blade.php</code></td><td>None</td><td>Canonical trusted HTML <code>content</code></td><td>Public overlay and body-end registries can receive extracted fragments</td><td>Not a container; historical child rows are preserved but new child placement is not supported</td><td><code>resources/views/pages/partials/blocks/html.blade.php</code></td><td>Owns a wrapper <code>&lt;div&gt;</code> around trusted markup and can also emit out-of-band overlay or body-end content</td><td><code>mostly clear</code></td><td><code>PublicEditorialBlocksRenderingTest</code>, <code>PublicRichContentTest</code>, <code>PageBuilderExperienceTest</code></td><td>Phase 3 stops treating Trusted HTML as a public child-container wrapper. Trusted markup can still affect shared overlay or body-end output beyond the visible root.</td></tr></tbody></table>

Validation And Persistence Overview

Published block types do not each have their own dedicated request class today.

  • App\Http\Requests\Admin\BlockRequest is the shared edit request path
  • slug-specific branches inside that request normalize fields into current canonical storage
  • App\Support\Blocks\BlockPayloadWriter persists the normalized block payload
  • App\Support\Blocks\BlockTranslationWriter moves locale-owned fields into translation rows for registered families
  • App\Support\Blocks\BlockTranslationResolver resolves translated or fallback values back onto a renderable block instance

That shared request path is one reason Phase 1 documents contracts first before any schema-driven edit work.

Gaps And Backlog

Current remaining high-signal gaps after the current Phase 3 fixes:

  • gallery still preserves a legacy settings-based fallback item path when canonical ordered block_media rows are missing
  • table renderer still supports a legacy settings.rows fallback path even though the core admin form writes translated row copy
  • hero still preserves legacy field fallbacks when canonical translated intro fields are empty
  • feature-grid and feature-item are now published because they are source-backed, but they remain intentionally transitional delegate contracts over the shared Columns or Column Item presentation paths
  • tabs, slider, menu, and faq-list still exist as legacy draft catalog rows with compatibility forms or renderers, but they are not published core contracts and should continue to fail safely in the contract modal and audit output
  • showcase-list and contact-info still exist only as public-render compatibility paths rather than shipped published core catalog blocks; their settings-driven links now follow the same safe public URL rules as other block renderers
  • published and draft catalogs coexist, so future admin surfacing must stay explicit about published core contracts versus draft or install-specific rows

Recommended later standardization work for block groups:

  • define stable product-owned block groups such as layout, content, navigation, pattern, and advanced in one source of truth
  • align picker groupings, docs groupings, and Block Types admin groupings to that same source
  • decide which currently draft or transitional blocks should become supported, archived, or explicitly legacy
  • standardize which contracts are translation-backed versus intentionally shared-only before any schema-driven form work starts

Phase 1 Summary

Phase 1 establishes the current published contract inventory without changing block editing behavior, block storage, or public rendering.

That was the intended stopping point for the Phase 1 release.

Phase 2 Summary

Phase 2 makes the documented contract visible in the Block Types admin as read-only information while keeping block edit behavior, storage, renderers, and picker behavior unchanged.

Phase 3 Summary

Phase 3 starts resolving low-risk documented contract gaps without adding a schema editor, dynamic form builder, or DB-driven block form system.

  • code now follows the existing text-translation path for title, label, and snippet body while keeping syntax language shared
  • table now follows the existing text-translation path for title and row copy while keeping table style shared
  • breadcrumb now preserves the exposed shared settings on save
  • stat-card now uses the existing optional URL in the public renderer with a simple safe link
  • link-list now renders the existing translated intro copy above the child item list
  • sticky-navbar now aligns persisted Navbar root ownership with Block::ownsPublicRoot() so the public shell does not add an extra generic wrapper
  • image now follows the existing image-translation path for caption and alt text while keeping selected media and optional link URL shared
  • gallery now uses locale-owned block_gallery_item_translations for per-item alt text, caption, overlay title, and overlay text while keeping ordered gallery media and presentation settings shared, preserving legacy fallback items for old content, and excluding legacy gallery title/description from normal editing and public output
  • download now follows the existing text-translation path for visible label and helper copy while keeping selected media and button variant shared
  • file, video, and audio now have first-class admin forms and request normalization so translated visible copy and shared media or URL sources round-trip through the same contract they already render publicly
  • image, gallery, download, file, video, and audio no longer accept new arbitrary child placement and no longer render arbitrary historical child trees publicly, while existing child rows remain preserved in admin block trees
  • code, table, quote, toc, and html no longer accept new normal child placement and no longer render arbitrary historical child trees publicly, while existing child rows remain preserved in admin block trees
  • navbar-brand and sidebar-brand now share a conservative saved-URL or site-home fallback contract, and both preserve safe accessible naming for logo-only output without forcing visible text
  • sidebar-nav-group now reuses the same manual sidebar item output semantics as sidebar-nav-item for nested child links while preserving the existing WebBlocks UI nav-group wrapper contract
  • section, container, grid, cluster, card, and content_header now use the same shipped contract source across the registry, read-only admin contract modal, audit output, docs, and focused regression coverage
  • hero, columns, column_item, cta, feature-grid, and feature-item are now published in the shipped core catalog, documented in the shared contract registry, and covered as source-backed marketing or structured content contracts instead of remaining draft-only or underdocumented
  • locale-only edits for managed CTA buttons and structured child items now preserve shared URLs while still updating translated labels or copy
  • layout primitives keep shared layout settings and child structure on canonical block storage instead of translation rows or arbitrary copy fields
  • card now uses a composable shell contract: the parent Card owns the wb-card root, Card regions own wb-card-header, wb-card-body, and wb-card-footer, region blocks are only valid under Card, and legacy saved Card copy is preserved only through a minimal no-region fallback renderer path
  • content_header keeps translated title, intro, and meta copy, always renders its title as H1, safely ignores legacy saved heading-level values, and keeps its semantic <header class="wb-content-header"> root renderer-owned without a generic wrapper
  • testimonial and stats remain documented honestly as alias-only behavior that delegates to existing Quote or Columns render paths rather than as standalone published core contracts
  • tabs, slider, menu, and faq-list remain draft or alias-era compatibility slugs rather than published core contracts, while showcase-list and contact-info remain public-only compatibility renderers with no shipped core contract entry