Skip to content

feat(NB-3148): email block composer, block registry and block library - #76

Merged
cannycookie merged 5 commits into
5.xfrom
feature/NB-3148-email-block-composer
Oct 2, 2026
Merged

cannycookie merged 5 commits into
5.xfrom
feature/NB-3148-email-block-composer

Conversation

@cannycookie

@cannycookie cannycookie commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Part 1 of NB-3148 (modular email templates): a block composer in the email-templates package. A template can switch its body from the single TinyMCE content field to an ordered list of typed blocks (layout). Templates with no layout send exactly as before.

  • Block contract and registry. Blocks\Contracts\EmailBlockDefinition, Blocks\AbstractEmailBlock (token replacement, public image URLs, rendering through view($mode)), Blocks\BlockRenderContext, Enums\BlockRenderMode (Email only). Blocks\EmailBlockRegistry (singleton) lists block types from config('filament-email-templates.block_types') or DEFAULT_TYPES, plus register() at runtime; a later class with the same name() replaces the earlier one in place.
  • Seven block types (src/Blocks/Types): Text, Hero, Two columns, Button, Image, Divider / spacer, Saved block. Saved blocks render the library block inline, cannot nest, and are skipped (with a warning) when inactive, deleted or missing.
  • Renderer. Layout\EmailLayoutRenderer renders a layout in order; unknown types and malformed items are skipped. The send path, template preview, block library preview and Builder block previews all use it.
  • Views. email/layout.blade.php and layout_preview.blade.php (header, hero title, blocks, support block, footer, unsubscribe), email/blocks/*.blade.php, shared row partials _block_open / _block_close / _block_button, _support_block_standalone, _unsubscribe, forms/block-preview.blade.php.
  • Data model. layout JSON column on templates (fillable, array cast), EmailBlock model + vb_email_blocks table; migration stubs plus sqlite test mirrors; EmailTemplate::usesLayout() / renderViewPath().
  • Template form. "Build the body from blocks" toggle (hidden on quote templates; switching on copies the current body into a first Text block), the Builder, and read-only header/footer text. Saving with the toggle off stores layout = null and keeps content.
  • Block library. EmailBlockResource (list, create, edit, soft delete/restore, trashed filter, active filter, preview slide-over), registered by the plugin.
  • Send path. The package BuildGenericEmail renders the layout and passes unsubscribeUrl when the mailable has one (CampaignMail does), so filament-campaigns needs no change.
  • README "Email blocks" section and CHANGELOG ## 5.7.0 (unreleased).

Security fix (review cycle)

  • Builder block previews now render inside a sandboxed srcdoc iframe (sandbox with no allow-scripts / allow-same-origin, escaped attribute, title="Email block preview", fixed 320px height) instead of inline markup in the admin page. Stored rich text is not sanitised server-side, so inline previews let an editor's <script> or onerror run in other admins' sessions. Sent email output is unchanged. Filament's edit overlay sits over the preview, so click-to-edit still works; a block taller than 320px is cut off in the preview (an auto height would need scripts in the frame).
  • Block image uploads accept PNG, JPEG, GIF and WebP only (no SVG). The README tells hosts to register a policy for Visualbuilder\EmailTemplates\Models\EmailBlock.

Decisions (from the approved plan, revision 2)

  • D1 - Option (b), block composer. Opt-in per template; layout = null keeps the content body. Each block is one full-width table row with fixed markup.
  • D2 - Block library lives in Content Management next to Email Templates (cluster placement is done in neurobox, NB-3208; the resource honours a $cluster set by a subclass or navigation.blocks.cluster).
  • D3 - Client overrides stay whole-field replacements; an override's optional layout replaces the template's layout (neurobox, NB-3208).
  • D4 - Fixed parts. Logo header, hero title, support block and footer always render around the blocks; campaign sends add an unsubscribe line that cannot be removed. The composer shows them as read-only text.
  • D5 - Block classes and a registry; statikbe not installed. Adding a block type is one class plus one email view.
  • Quote templates are excluded from block mode.

Deviations from the plan

  • _unsubscribe renders its own full-width <table> rather than a bare <tr>: both the package and neurobox _footer close the outer table, so a <tr> after it would sit outside any table.
  • Images carry a pixel width attribute (content width - 60, or the column width) with style="width:100%;max-width:...px" instead of width="100%": Outlook desktop ignores percentage image widths and would render the image at its natural size.
  • Two columns: each column holds (content_width - 80) / 2 of content as planned, but the column wrapper (div max-width and MSO ghost td) is that plus 20px, with 10px side padding inside each column. This gives a 20px gutter side by side and keeps the 30px edge when stacked; with the literal plan numbers the gutter would be uneven.
  • Divider line colour is body_color mixed at 20% over content_bg_color and output as a solid hex (email clients do not reliably apply opacity to borders); #e5e7eb when a colour is missing.
  • Unsubscribe text colour uses the theme's footer_color (present in neurobox themes), falling back to body_color.
  • The template list's screenshot actions (single and bulk) now render through renderViewPath() so screenshots of block templates show the blocks.
  • The delete bulk action on the block library also carries the "send without it" modal description.
  • Composer field labels (for example "Image description (alt text)") were not specified in the plan and were chosen here. No helper text is used.

Test plan

  • vendor/bin/pest in the package: 167 passed (517 assertions) after the security fix (159 passed, 479 assertions at first review), run twice in random order. New: EmailBlockRegistryTest, EmailLayoutRendererTest, EmailLayoutBuilderTest, LayoutMailableTest, EmailBlockResourceTest, plus template-form additions in ResourcesTest.
  • Manual rendering QA (below) after NB-3208 wires neurobox.

Manual Outlook/Gmail rendering checklist

For one template with every block type, one with a saved block, one client override with its own layout, and one campaign send, check Outlook desktop (Windows), Outlook.com, Gmail web, Gmail iOS, Apple Mail (macOS and iOS), each in light and dark mode:

  • Blocks appear in the right order
  • Images are not stretched and have alt text
  • Buttons are clickable with the theme colours (VML button in Outlook desktop)
  • Two columns stack on mobile
  • No white gaps between rows
  • Text is readable in dark mode
  • Support block, footer and (campaign only) unsubscribe link are present
  • Tokens are replaced

Notes

  • Neurobox wiring is NB-3208 (migrations, trait/override/preview wiring, cluster placement, policy, neurobox tests). Nothing in the neurobox repo is changed here.
  • Release: after merge, tag 5.7.0 on 5.x (human step; no tag created here).
  • Package .php.stub migrations are only published, not loaded by a host's migrator, so hosts need their own migrations (neurobox adds them in NB-3208; the README covers upgrades).

Pre-existing observations

  • composer.lock is listed in .gitignore but tracked, and is out of sync with composer.json (visualbuilder/filament-tinyeditor locked at 4.x-dev against ^5.0|^6.0), so composer install fails. I ran composer update locally to run the suite and did not commit the lockfile change.

YouTrack

Part of NB-3148 (Part 1). Neurobox follow-up: NB-3208.

Tier: coding_complex

🤖 Generated with Claude Code

Dev Agent Bot and others added 3 commits October 2, 2026 03:17
- EmailBlockDefinition contract, AbstractEmailBlock, seven block types
- EmailBlockRegistry (config + runtime registration) and EmailLayoutRenderer
- layout/layout_preview views, block row views, unsubscribe row
- EmailBlock model and block library resource
- block mode toggle and Builder on the template form; package trait renders layouts
- registry and renderer tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- layout mailable: block order, support block, footer, unsubscribe row, classic regression
- builder: one block per registry entry, labels with summaries, previews via renderer
- template form: quote toggle hidden, block mode seeding, null layout when off, validation
- block library resource: list, create, unique name, edit, delete/restore, preview

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- README section on block mode, the block library, image disk and adding a block type
- CHANGELOG 5.7.0 entry (unreleased; tag pending)
- Pint (laravel preset) on the new files

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cannycookie cannycookie changed the title feat(NB-3148): email block composer [WIP] feat(NB-3148): email block composer, block registry and block library Oct 2, 2026
Dev Agent Bot and others added 2 commits October 2, 2026 03:26
…tion

Adds search/sort/filter coverage for the new EmailBlockResource table,
plus the token-URL regex happy path and hero requiredWith pairing that
the composer validation test only exercised the failure case for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Render Builder block previews inside a sandboxed srcdoc iframe (no
  scripts, opaque origin) instead of inline markup in the admin page
- Restrict block image uploads to PNG, JPEG, GIF and WebP
- README: register a policy for the EmailBlock model
- Tests: escaped srcdoc, sandbox attribute, raster-only uploads

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cannycookie
cannycookie marked this pull request as ready for review October 2, 2026 03:34
@cannycookie
cannycookie merged commit 351d351 into 5.x Oct 2, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant