Skip to content
Abdulkader Safi

Filament Atelier

One shell per site was a decision I never made on purpose

18 August 2026 Updated 18 August 2026 3 min read
The same page rendered twice, once in a marketing layout with a navbar and once in a documentation layout with a sidebar.

Marketing pages want a navbar and a footer, documentation wants a sidebar, a landing page often wants neither. The blocks are identical in all three. v0.2.0 makes the shell a per-page choice, and the test for it caught a bug where a custom layout worked live and crashed in the preview.

v0.2.0 lets a page pick its own layout. It is the first minor bump since the first release, because it adds a capability and a registration method rather than fixing something.

The gap

There was one layout, set in config, used by every page. That was never a decision, it was just where things ended up: a single atelier.layout key pointing at one Blade view.

Real sites are not one shell. Marketing pages want a navbar and a footer. Documentation wants a sidebar with a page list. A landing page often wants neither, because a nav bar is an exit and a landing page is trying not to have one.

The blocks are the same in all three cases. So the shell is a property of the page, not a different set of blocks.

A map, not a class

Blocks are classes in this project, and there was an obvious pull toward making layouts classes too. I did not.

A block earns a class because it carries a Filament schema, an icon, a category, translatable keys and a view. A layout carries a key, a label and a view name. A class holding three strings is ceremony.

AtelierPlugin::make()
    ->blocks(DefaultBlocks::all())
    ->layouts([
        'site' => ['label' => 'Navbar and footer', 'view' => 'layouts.site'],
        'docs' => ['label' => 'Sidebar', 'view' => 'layouts.docs'],
        'bare' => 'layouts.bare',
    ]);

The short form is just a view name and the label comes from the key. Registration sits next to ->blocks() so there is one idiom rather than two.

Three decisions inside a small feature

The select hides itself when no layouts are registered. A dropdown with one option is a question with one answer. Sites that never register a layout see no new control at all.

The choice is page-level, not per locale. A page that is a Service in English is a Service in Arabic, and the same logic applies to its shell: a layout is structure, and this project's whole model is that both locales share one structure. Putting it inside the locale tabs would let English get a sidebar and Arabic not, which is a bug you could ship without noticing.

An unknown key falls back rather than throwing. Delete a layout from the panel provider and pages still naming it keep rendering on the site-wide default. A page keeps its layout key after the code that defined it is gone, and every public page returning a 500 is a bad way to discover that.

The bug the test found

There is one test I wrote because the project has a rule: the preview and the public page must render through the same path. It asserts a preview renders through the same layout the public page will use.

It failed immediately, and not for the reason I expected.

PreviewController never passed $page to the layout. The public controller did. So a custom layout reading $page, for something as ordinary as highlighting the current item in a sidebar, worked perfectly on the live site and returned a 500 in the editor.

That is exactly the class of bug the shared render path rule exists to prevent, and it had been sitting there since the preview was built. It only surfaced because writing a second layout gave a reason to read $page in one.

Features find bugs that tests written for the feature would not, because the feature makes you use the code differently.

The failure modes I documented

Writing the guide for this forced me to list what breaks quietly when somebody writes their own layout:

  • Leave out atelier::partials.meta and the page has no title, description, canonical, hreflang or Open Graph tags, and previews stop being noindex.
  • Leave out atelier::partials.tokens and every design token resolves to nothing, so the section controls silently do nothing and Arabic loses its font stack.
  • Leave data-atelier-canvas off the element wrapping the blocks and the editor cannot swap the canvas on refresh.
  • Read $page without guarding it and anything rendering that layout outside the controllers breaks.

All four fail without an error. That list is now the middle of the layouts documentation rather than something a person discovers one at a time.

Where it stands

v0.2.0. Multiple layouts, picked per page, with the preview rendering through whichever one the page will actually use.

No migration: the layout column had existed since the first schema and nothing had ever read it. Which is its own small lesson about writing columns before writing the feature that needs them.