Skip to content

Filament Atelier

Installing my own plugin on my own site broke the homepage first

28 August 2026 2 min read

Atelier alone, before converting a single page, silently took over `/` and `/sitemap.xml` on a site that already had both. `route('home')` threw `RouteNotFoundException` even though the route was still sitting in `web.php`. The docs said the opposite of what actually happens.

The first commit that installs Atelier on this site doesn't touch a single page. It configures one locale, registers a layout, wraps this site's existing components as blocks so the visual language stays exactly what it was. Nothing about the live site was supposed to change yet.

The homepage broke anyway.

route('home') doesn't exist, except it does

routes/web.php still had Route::get('/', [HomeController::class, 'index'])->name('home'). I hadn't removed it, hadn't touched it. Calling route('home') in a test threw RouteNotFoundException. php artisan route:list still showed the route, named, registered, present.

Laravel's RouteCollection keys routes by method and URI. For two routes registered at the exact same literal path, the last one registered wins outright, replacing the first entirely. Not "first match wins," which is what I'd assumed, and what I'd written in this package's own install docs. That rule only holds for genuinely different patterns, /projects against /{locale}/{slug?}, where Laravel matches in registration order and a specific route can win regardless of position. For an identical literal string, there's no matching contest. The second registration overwrites the first.

Atelier registers its own / and /sitemap.xml from a booted() callback, timed deliberately to run after routes/web.php loads, so a host app's own catch-all doesn't need to guess at load order. That timing is exactly why it won: this site's / was registered when web.php loaded, Atelier's / was registered after, in the very next boot phase, and the later one took the slot.

The fix, and the test that has to exist now

Re-registered / and /sitemap.xml from an even later booted() callback, timed to run after Atelier's own. Same trick Atelier itself uses, aimed back at it.

app()->booted(function () {
    Route::get('/', [Pages\HomeController::class, 'index'])->name('home');

    Route::get('/sitemap.xml', function () {
        return response()->file(public_path('sitemap.xml'));
    });
});

Added a regression test that asserts the route named home actually resolves to /, and the route named sitemap.xml doesn't resolve to atelier.sitemap. Cheap insurance against a future version of this package changing its boot timing again.

The collateral damage

Fixing the collision broke a second, unrelated test. A hardening test dynamically registered a GET route to probe how the app handles a malformed request. That probe was now shadowed by Atelier's own GET-only /{locale}/{slug?} catch-all, which had just become active for real. Switched the probe to POST, since Atelier registers no POST routes, and it stopped competing with anything.

Where it stands

Zero pages converted, one route collision found and fixed, one regression test added, one unrelated test quietly broken and repaired. This is what dogfooding actually catches: not whether the block system renders correctly, but whether installing this thing on a site that already has routes of its own does what the docs promise. It didn't. The docs are getting fixed to match.