Skip to content

Filament Atelier

Services needed their own sidebar entry, and Filament had already solved it

13 September 2026 Updated 13 September 2026 4 min read
A Filament sidebar showing Pages, Services and Products as separate entries, each with its own icon.

The ask was simple: services and products should sit in the sidebar next to Pages, not hide behind a dropdown on a page form. The design I approved cost a Filament resource subclass per type in every app that installs the package. Reading the framework source before writing it found the mechanism that removes the subclass entirely.

v0.5.0 adds page types. A service, a product or a case study becomes its own entry in the panel sidebar, with its own fields and its own starter sections, on the same table and the same editor as every other page.

The ask

Atelier already had a dropdown on the page settings screen labelled "Page type". It set the schema.org type in the JSON-LD and it told the panel nothing. A page that was a Service in search results was a row under Pages in the sidebar, next to the privacy policy.

What was actually wanted: Services in the sidebar, with its own icon, listing only services, each with the handful of extra facts a service carries. Products the same. Pages keeps everything else.

What a type turned out to be

Once the questions were answered, a page type is six things a developer declares in code:

  • the custom properties, as a plain Filament schema
  • which of them are translated
  • the block tree a new one opens with
  • the blocks its picker offers
  • a slug prefix, and a schema.org type
  • the Blade partial a listing renders it through

None of that needs a new table. A typed page is still a row in atelier_pages, so it inherits slugs, redirects, revisions, drafts, preview, SEO and the sitemap without a line of work. Two columns carry the difference: type, defaulting to page, and data for whatever the type declared.

The cost I was about to accept

A Filament resource is routed by class name. That is the whole problem. /admin/services and /admin/pages are two routes, two navigation items and two breadcrumb trails, and Filament resolves each to a class, so one class cannot wear two hats.

So the approved design had every consumer writing a subclass per type:

class ServiceResource extends PageTypeResource
{
    protected static string $pageType = 'service';
}

Five lines each, times every type, in every client project. Annoying rather than fatal, and I had already argued myself into it.

What version 5 already had

Before writing it I went reading vendor/filament/filament/src/Resources to check how routes get their names. Two files down there is ResourceConfiguration, a small class holding a resource class, a key and a slug. Panel::resources() accepts instances of it as well as class strings. Resource::getSlug() prefers the configuration's slug over the class-derived one.

Which means the framework already supports registering one resource class several times, each under its own key and URL. The panel builds routes for each configuration and stamps them with a middleware:

$route?->middleware("resource-configuration:{$configuration->getKey()}");

That middleware sets the current key, and it is registered as a Livewire persistent middleware, so the key survives every Livewire update after the first load. Table sorting, pagination and a save all still know which type they are looking at.

The registration became a loop:

$resources = [PageResource::class];

foreach (app(PageTypeRegistry::class)->all() as $key => $type) {
    $resources[] = PageResource::make($key)->slug($type::slug());
}

One resource class, one plain registration for Pages and one configuration per type. Everything inside the resource asks which hat it is wearing:

public static function typeKey(): string
{
    return static::getConfiguration()?->getKey() ?? PageTypeRegistry::DEFAULT;
}

Query scope, label, icon, navigation sort, the extra form section and the defaults on create all hang off that one method.

What it bought

A page type is now one plain PHP class that knows nothing about Filament resources. That fixed a wart I had flagged when presenting the design and could not remove: cardView(), a front-end concern, was going to live on a resource class because the resource was the only class per type. With the configuration approach the type is not a resource, so the method sits where it belongs.

It also removed a file per type from every project that will ever install this.

The one thing to know before copying it

The current configuration key lives on Filament's manager singleton and nothing resets it per request. That is correct for a normal PHP request, where the container is thrown away at the end. It is not correct inside a test, where two get() calls share one container: hit /admin/services and then /admin/pages in the same test and Pages lists services, because the key from the first request is still set.

The fix in tests is one line clearing it between requests, and it is worth knowing if you run Octane, where containers are also reused.

Where it stands

Shipped in v0.5.0. Two columns, one registry, no resource class per type, and Pages still lists anything carrying a type nobody registered, so deleting a type class never hides a live page from the panel.

Reading the framework source for twenty minutes deleted a file from every future project. I nearly wrote the subclass version instead, because I had already argued for it in front of someone and the design was approved.