Abdulkader Safi

Six pages into blocks, and the rule was nobody notices

Contact and five listing pages moved from Blade views and controllers to Atelier blocks. The site was already live and indexed, so the constraint wasn't building a page builder, it was making sure nobody could tell it had happened.

3 min read

Share
A page builder editor sidebar listing selectable blocks next to a live preview of a listing page.

Filament v5 is in. The routing collision is fixed. Now the actual work: converting real pages on a live, indexed site to Atelier blocks, without anybody noticing.

Six pages first: Contact, and the five listing pages, projects, services, blog, devlogs, resources. Home stays on its own controller for now, it's the most structurally different page on the site and gets its own pass later. The constraint on all six: identical rendered output. Same URLs, same routes, no reindexing, no redirect rules. Where a section repeats across multiple pages, it's one block I can select from a list, not five copies of the same markup with different data wired in.

ListingBlock renders projects, services, blog posts, devlogs or resources from one block, dispatching to the right card component by a source attribute. The first version used Blade's <x-dynamic-component :attributes="[...]"> to pass the current item through.

Every card rendered. Every card linked to #.

<x-dynamic-component> doesn't spread an array through :attributes the way I expected. It only forwards attributes written literally on the tag itself. The array reached the component as one opaque prop, not as named attributes the component's @props could see, so $project inside the card was always null. Replaced the dynamic dispatch with an explicit @switch($source) calling each <x-ui.project-card>, <x-ui.blog-card> and so on directly. Less clever, correctly wired.

A block used for the wrong shape

The Resources page ends with a closing paragraph, full width. I'd wrapped it in TwoColBlock, since a two-column grid block already existed and one column just meant leaving the second empty. It rendered at half width instead, because TwoColBlock is a two-column grid whether or not both columns get used.

The fix wasn't a CSS override, it was admitting TwoColBlock was the wrong tool: a genuine two-column layout and a single block of prose are different shapes, not the same shape used differently. Built ProseBlock for the second case and moved the Resources page onto it.

Click-to-select broke twice, for two different reasons

Every block needs to be clickable in the editor's live preview, so clicking a section on the canvas opens its settings in the sidebar. It stopped working twice.

First: none of this site's <x-ui.*> components echo {{ $attributes }}, so the data-atelier-block id Atelier needs on a section's root element never reached the DOM. Fixed by wrapping every block's output in its own <div {{ $shared }}>, independent of whatever the inner component does with its own attributes.

Second, after the first fix: still nothing happened on click. The custom layout this site uses, adapted from the existing pages/layout.blade.php, never carried over the preview-mode script that listens for clicks on [data-atelier-block] and posts a message back to the editor iframe. Copied it in from the package's own default layout, gated behind the $preview flag so it never ships to a real visitor.

Where it stands

Six routes removed from web.php, six controllers' index() actions gone, six orphaned Blade views deleted. Every route() call pointing at one of those six names, in every template that used them, swept to url() instead. Sixteen blocks in the editor's picker. Every one of the six pages, screenshot-compared against what was live before, matches. The visitor sees nothing. That was the whole point.

Last updated 28 Aug 2026 · filed under Laravel, blade, filamentphp, plugin, tools, ui, ux

Every entry on this project

28 build notes, in order

Including the ones where nothing worked. You are on part 16.

Read the series →

FAQ

Frequently asked questions

Why doesn't <x-dynamic-component :attributes="[...]"> pass an array as named props to the target component?

Blade's dynamic component tag only forwards attributes written literally on the tag itself into the target component's attribute bag. A PHP array bound through :attributes arrives as a single prop named attributes, not as the individually named props the target component's @props array expects. The fix is to dispatch explicitly, a @switch over the discriminating value calling each concrete component by name, rather than relying on the dynamic tag to spread a data structure it was never built to spread.

How do you tell whether a block is the wrong shape for content, rather than just needing a style override?

Ask what the block fundamentally is, not what it can be coerced into rendering. A two-column grid block used with one column empty still is a two-column grid, so its layout math assumes two columns exist, and no CSS override changes that assumption cleanly. If the actual content is single-column prose, the honest fix is a dedicated single-column block, not a two-column block with one side left blank.

What two separate things does click-to-select in a page-builder editor actually need?

A DOM hook and a listener. Every block's rendered root element needs a stable identifier, a data-atelier-block attribute carrying the block's id, so the editor knows which block on the canvas maps to which entry in the sidebar. Separately, the page or its layout needs a script, active only in preview mode, that listens for clicks on elements carrying that attribute and posts a message to the parent editor frame naming which block was clicked. Missing either one looks identical from the outside: nothing happens when you click a section.

Written by

Abdulkader Safi

Software Engineer

Lead engineer at DSRPT, from Lebanon and based in Kuwait. I write about the tools and bugs from real client work, with the numbers I measured.

About me → GitHub LinkedIn

Need this kind of work done on your project?

Start a project →

Keep reading

More from Filament Atelier

All 28 entries →
  • A version number changing from 0.5.0 to 1.0.0 beside a list of deferred items.
    Filament Atelier

    · part 28 of 28

    Tagging 1.0.0 with four features missing, on purpose

    The gate list had thirteen items. Six went in, seven did not, and the tag went out anyway. What the number promises is that the API stops moving, not that the feature list is finished, and conflating those two is how packages sit at 0.x for three years.

  • The same block of JavaScript appearing in three different Blade layout files.
    Filament Atelier

    · part 26 of 28

    A script in three layouts is a contract nobody signed

    The editor's preview needed a few lines of JavaScript in the page it renders. I put them in the shipped layout, then copied them into two more. Anyone writing their own layout had to copy them too, and missing them broke half the editor with no error at all.