Abdulkader Safi

The config invited you to delete your own head tags

A social share image was not showing up. The render path turned out to be correct and the tag was never reaching the page, because the layout had been replaced. Every meta tag lived inside Atelier's own layout view, and a config key openly invited you to swap that view out.

4 min read

Share
Two rendered page sources side by side, one through the packaged layout and one through a host application layout, with identical head tags.

This one started as a bug report against myself: I set a social share image on a page and no og:image appeared.

The first thing I did was prove the code worked. I ran the form save in a test, watched the file land at atelier/og/<ulid>.jpg in the right column, rendered the public page, and read the head:

<meta property="og:image" content="http://localhost:8000/storage/atelier/og/share.jpg">
<meta name="twitter:card" content="summary_large_image">

Correct. Which meant the bug was somewhere I had not looked, and the useful question was which of my assumptions was false.

The layout was the answer

Every meta tag lived inside atelier::layouts.site, Atelier's own layout view.

The config has a key called atelier.layout whose entire purpose is to let a host app point at its own Blade view, so a client site gets its own navigation and footer. That is the normal thing to do. It is documented, and I wrote it.

Doing it deleted the entire head. Title, description, canonical, hreflang, Open Graph, Twitter. All of it.

The page still rendered perfectly. That is the part that makes it bad rather than merely wrong: nothing errors, nothing is missing on screen, and the failure is only visible to a crawler or a share card. A site could run like that for months.

Previews stopped being noindex too, which is the same bug with worse consequences, because it means unfinished drafts become indexable.

Two partials, not one

The head moved into atelier::partials.meta. Design tokens moved into atelier::partials.tokens.

Splitting them was not tidiness. They have different positional requirements and one file cannot express both:

<head>
    @include('atelier::partials.meta')

    @vite(['resources/css/app.css'])

    {{-- After your stylesheet, so the tokens win. --}}
    @include('atelier::partials.tokens')
</head>

Meta does not care where it sits. Tokens must come after your stylesheet or the custom properties lose to it. One combined include would have to pick a position and be wrong about half its contents.

The test that matters

Three tests cover this, and one of them is the reason I trust the fix.

It renders the same page twice, once through Atelier's stock layout and once through a host app's own layout, extracts every meta, title, canonical and alternate line from both, and asserts the two lists are identical.

That is the test that fails if somebody adds a tag to one place and forgets the other. Asserting that "a custom layout has a title" would pass forever while the two drift apart.

The other two cover the specific things that were silently lost: the share image tag, and a preview still carrying noindex when a host app supplies the layout.

The second bug in the same area

While I was there: the share image upload was missing ->visibility('public'), which the package's own media helper sets everywhere else.

On a local disk that changes nothing. On S3 it means the upload succeeds, the tag is emitted, the URL is correct, and the image 403s for every crawler that fetches it. Works in development, broken in production, no error either way.

What I changed about the documentation

The README was describing a different product. It promised GSAP animations, a sitemap, JSON-LD, per-block asset loading, and header, footer, contact form and raw HTML blocks. None of those existed. It also said Filament v4 while composer.json requires ^5.0, which is the one that would actually break an install.

It now has a "Not built yet" section, because a page builder is judged on what it does not do and finding out after install is worse than reading it first.

Documentation also moved to the wiki, for a reason I had missed: Docs/ is export-ignored, so nobody who installs the package can read it. It was never written for using Atelier anyway; it is the spec, the task breakdown and the research behind the decisions.

The trap this leaves for anyone upgrading

If you point atelier.layout at your own view, this release needs two lines from you. Nothing breaks without them, which is exactly the problem: add them, or your pages keep rendering with no head at all.

That is at the top of the release notes rather than in a footnote, because the entire bug was that a silent failure looks like success.

Where it stands

v0.1.4. The head survives a custom layout, share images work on S3, and the README describes what ships.

Animation is now formally dropped as a plugin feature. A block is already your PHP class and your Blade view, so it animates however you like, and the plugin ships no GSAP dependency and no preset contract. The cost, stated plainly in the docs, is that there is no animation dropdown for a client.

Last updated 18 Aug 2026 · filed under Laravel, blade, filamentphp, plugin, tools, web application, seo

Every entry on this project

28 build notes, in order

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

Read the series →

FAQ

Frequently asked questions

Why put meta tags in a partial rather than in a package's own layout?

Because any package that lets a host application replace its layout has made those tags optional by accident. The replacement is the normal thing to do, since a real site wants its own navigation and footer, and the moment somebody does it every tag living inside the packaged layout disappears. The page still renders and nothing errors, so the loss is invisible on screen and visible only to crawlers and share cards. Extracting the head into a partial the host includes turns an invisible total loss into two documented lines, and gives you something to point at in the upgrade notes.

Why split design tokens and meta tags into separate includes?

Because they have different positional requirements. Meta tags can sit anywhere in the head and behave identically. CSS custom properties have to come after the application stylesheet or the stylesheet wins, so their position is load bearing. A single combined include would have to choose one position and would be wrong for half its contents. Two includes also let the documentation explain each failure separately, which matters because they fail differently: without the meta the page has no head, and without the tokens every variable resolves to nothing and controls silently stop working.

How do you test that two layouts produce the same head?

Render the same page through both, extract every line that is a meta tag, a title, a canonical link or an alternate link, and assert the two lists are equal. Asserting that the custom layout merely contains a title would pass forever while the two implementations drifted apart. The equality assertion is what fails the moment somebody adds a tag in one place and forgets the other, which is the actual failure mode over the life of a package. Filter out anything genuinely owned by the layout, such as charset and viewport, so the comparison covers only what the shared partial is responsible for.

Why does a file upload work locally and fail on S3 with the same code?

Visibility. A local public disk serves whatever is in the directory once the storage symlink exists, so an uploaded file is readable regardless of what visibility was requested. Object storage applies an access control setting per object, so a file uploaded without explicitly setting public visibility is stored fine, returns a correct looking URL, and then returns 403 to anyone fetching it. The symptom is a broken image with a valid URL and no error in the application log, and it appears only after deploying. Set visibility explicitly on every upload field rather than relying on the disk default.

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.