Abdulkader Safi

The homepage was last, and it found the bug that mattered

Converting home to blocks and handing `/` fully to Atelier surfaced a bug of my own making: the page whose slug is the special `home` one resolves correctly at `/`, but the URL builder used everywhere else, including the sitemap, builds `/home` instead.

4 min read

Share
A generated sitemap listing the site's homepage URL alongside every other public page.

Five pages were converted, a routing collision was fixed, sixteen blocks existed. The homepage was still on its own controller, deliberately last, because it's the most structurally different page on the site and the one most visitors actually land on.

Four new blocks, one extended one

Home needed a hero with a portrait image and two calls to action, a stack strip, a services preview pulling live data, and an about strip, none of which existed anywhere else on the site. Built all four, matching the current markup exactly rather than redesigning while I was in there.

The featured-work, writing, devlogs and resources sections on Home are the same shape ListingBlock already renders on their own index pages, just with an optional header instead of always requiring one. Extended ListingBlock with a collapsible section-header group in its schema rather than building four more blocks that would have been the same block with different copy.

defaults() doesn't do what the name suggests

The seeder for Home's content originally left several blocks with empty attributes, assuming each block's defaults() method would fill the gap at render time. It doesn't. Grepping the package's own source settled it: defaults() is called exactly once, by the page editor, to pre-fill a section the moment somebody adds it in the admin UI. Renderer never calls it. A block seeded with no attributes renders nothing, because nothing fills the hole for it later.

Rewrote the seeder to spell out every attribute explicitly, block by block, instead of leaning on a mechanism that only exists for the editor.

The stat strip that lost its border

After seeding, a screenshot comparison against the live homepage caught something a test wouldn't: the numbers strip, five packages published, ten plugins, fifty projects, thirty-six hundred downloads, was rendering full bleed with no top border and no dividers between the four numbers, instead of contained inside the same column as everything else.

The block had a bare toggle meaning "skip your own section wrapper, the caller already provides one," which is exactly what the old homepage's Blade markup did, wrapping the block in its own <section>. As a standalone Atelier block there is no caller left to provide that wrapper. bare: true just meant no wrapper existed at all.

Fixed by making the block always render its own correctly sized section, and removed the bare toggle from the editor schema entirely: there was no longer a context where turning it off made sense.

The sitemap, and the bug that actually mattered

With every page converted, the last piece was /sitemap.xml itself. This site had generated a static file with its own Artisan command since before Atelier existed. Atelier ships something better: a live sitemap built from published pages, plus a registry for URLs the package can't know about, blog posts, project pages, anything with its own model. Switched to it, registered the non-Atelier URLs, deleted the static-file command.

The sitemap came back with the homepage listed at /home.

PageController treats a page whose slug is literally home as the page served at /. That's the entire mechanism this site relies on for the homepage to work at all. Page::url(), the method that builds a page's public URL for exactly this kind of consumer, doesn't know about that convention. It builds /{slug} unconditionally, so the one page whose slug is the routing system's own special case gets the one URL that's wrong.

I wrote both halves of that inconsistency. PageController::__invoke() and Page::url() live in the same package, disagreeing about what home means, and nothing had called it until a sitemap needed both to agree.

Fixed it host side rather than editing the vendor package directly: bound a small subclass over Sitemap in the container that corrects the one URL after the parent class builds it, everything else untouched. That's the workaround for this site. The real fix belongs upstream, in Page::url() itself, and I'm the one who has to write it.

Where it stands

/ and /sitemap.xml both fully Atelier's now, HomeController deleted, the old re-registration workaround from the first install gone with it. Two hundred ninety four tests passing against the same baseline this project started with, zero new failures. One real bug found in my own package, by using it the way anyone else would.

Last updated 28 Aug 2026 · filed under Laravel, 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 17.

Read the series →

FAQ

Frequently asked questions

When is a block's defaults() method actually called?

Only by the page editor, at the moment a user adds a new section to a page in the admin UI, to pre-fill that section's fields with sensible starting values. The rendering path, Renderer, never touches defaults(); it reads whatever attributes were actually saved. A block seeded programmatically with an empty attributes array, assuming defaults() will fill it at render time, renders with nothing, because nothing calls that method outside the editor.

What does a bare toggle mean on a layout block, and when does it stop making sense?

bare: true typically means skip this block's own section wrapper, because whatever calls it already provides one, a reasonable option when a block might be nested inside a layout that supplies its own padding and max-width container. It stops making sense the moment the block becomes a standalone, top-level entry in a page builder's block list, since there is no longer any caller left to provide that wrapper: bare: true in that context just means no wrapper renders at all, dropping the layout rather than delegating it.

Why would a URL-building method and a routing controller disagree about the same page?

Because they're two separate pieces of logic maintained by the same person, encoding the same convention twice instead of once. PageController decides that a request for / should look up the page whose slug is literally home. Page::url() is asked, given a page, to build the URL a person should use to reach it, and defaults to /{slug} without checking for that same special case. Nothing forces the two to agree unless something exercises both directions, request to page and page to URL, for the same page, and a sitemap generator is one of the few things that actually does.

How do you fix a bug in a package you maintain, without waiting for your own next release?

Override the specific class at the container level in the app consuming it, rather than editing the vendor files directly, which get overwritten on the next composer update. Binding a small subclass that corrects just the one broken method, calling the parent implementation for everything else, is a workaround that ships immediately in the host app and can be deleted the moment the real fix lands upstream in the package itself.

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.