Skip to content

Filament Atelier

A generated /services page shadowed a 301 I shipped three releases ago

13 September 2026 Updated 13 September 2026 3 min read
A terminal showing a failing test: expected a 301 redirect, received 200.

A page type can serve a listing at its own prefix without anyone building a page. That route has to sit ahead of the catch-all to exist at all, which means it sits ahead of everything the catch-all was handling. The test suite failed on a redirect written in v0.2.0, and it was right to.

Register a Service type with a prefix of services and an index view, and /services lists your services. Nobody builds that page. It appears because the type exists.

That is a small feature with a large blast radius, and the blast radius is route ordering.

Why the route has to be early

Atelier's public route is a catch-all:

Route::get('/{locale}/{slug?}', PageController::class)->where('slug', '.*');

It is registered last on purpose, from a booted() callback, so a host application's own routes always win. Laravel matches in registration order, so anything after it is unreachable.

Which means a type's index route must be registered before it. And once it is, /services belongs to the type, not to the catch-all, and the catch-all is what was handling three things at that URL.

What it took over

A real page. The obvious one, handled before writing it. A client who builds a page at services, with copy above the list, expects that page. So the controller looks for a published page at the slug first and hands off to PageController when it finds one. A page you built always beats a page the code generated.

A draft at the same slug. Deliberately not a handoff. If a half-written draft parked at services took the URL, the listing would vanish the moment someone started writing the page that was meant to replace it. So only a published page wins, and the listing keeps serving until the replacement is live.

A redirect. This one I missed, and the test suite caught it in the same run that registered the type in the example app.

Expected response status code [301] but received 200.

RedirectTest has asserted since v0.2.0 that renaming a page 301s its old URL. The fixture renames a page whose slug is services, which is now a route belonging to a page type. So the old URL rendered a service listing with a 200 instead of redirecting, and every inbound link to the renamed page silently stopped pointing anywhere useful.

The fix is one more lookup in the same controller: hand off to PageController when a redirect exists at that slug too, and let the machinery that already handles 301s do its job.

$existing = PageSlug::where('locale', $locale)->where('slug', $prefix)->first();
$redirected = PageRedirect::where('locale', $locale)->where('from_slug', $prefix)->exists();

if ($existing?->page?->isPublished() || $redirected) {
    return ($this->pages)($locale, $prefix);
}

Why the test existed to catch it

Nothing about that test is related to page types. It was written eighteen releases ago for a feature about renaming pages, and it uses services as a fixture slug for no reason other than it being a plausible page name.

The lesson is not "write more tests". It is that a test suite earns most of its value on the change nobody thought was related. I would not have written a page-types test that renamed a page and checked for a 301, because in my head those were separate features. They share a URL space, and the URL space is the thing that does not know about my mental model.

The second one, found the same way

The other failure in that run was the sitemap saying it contained no URLs when it should have been empty. A registered type was emitting its index URL whether or not anything had been published under it, so an empty site advertised /services to crawlers.

An index with nothing on it is not a page worth crawling. Now it is listed only once the type has something published, and a real page at the same slug wins there too, because the page URLs are concatenated first and the list is deduped on the URL.

Where it stands

Shipped in v0.5.0. Type index routes at their prefix in every locale, yielding to a published page or a redirect, listed in the sitemap only when they have something to list.

Two bugs, both found by tests written for other features, both about a URL being claimed by two things at once. That is the recurring shape of bugs in this project, and the reason the catch-all comment in routes/web.php is now four paragraphs long.