beier/filament-pages
A Filament plugin for creating and managing pages in your admin panel. Build page resources with flexible content blocks, control publishing and visibility, and edit everything through a clean, native Filament interface. Ideal for adding simple CMS-style page management to Laravel applications power...
This package is an extracted Page CMS for Filament: it ships a Page model + migration, a PageResource for the admin panel, and a front-end catch-all route that resolves a slug to a rendered page. It was packaged against Filament v2 and last released in November 2022, so treat it as a reference implementation to copy from rather than a dependency you install and forget.
1. Install (on a Filament v2 app, or a scratch app to read the code):
composer require beier/filament-pages
php artisan vendor:publish --provider="Beier\FilamentPages\FilamentPagesServiceProvider"
php artisan migrate
Publish the config and views before migrating — the migration and the front-end view are the two things you will almost certainly want to edit.
2. Where to look first, in this order:
src/FilamentPagesServiceProvider.php — what gets registered (PageResource, route file, views, config, migrations).routes/web.php inside the package — the catch-all route definition and the middleware it runs under.src/Models/Page.php — the schema (slug, title, published flag, layout/view, content blocks) and any resolveRouteBinding override.PageResource + form schema — this is the actual contract of what fields exist.resources/views/ — the single Blade view every page renders through. This is your extension point for layouts.3. First use case (15 minutes):
Create a page in the admin panel (/admin/pages), give it a slug like about, fill the content field, mark it published, save — then hit /about in the browser. If that works, you understand 90% of the package. If it 404s, the problem is almost always the catch-all route being shadowed by another route or the published scope.
Let the package own the model, but override the resource. The fastest sustainable pattern with an archived package is: keep Beier\FilamentPages\Models\Page as-is, and extend PageResource in your own app/Filament/Resources/PageResource.php to add fields. Re-publishing views/migrations and editing in place works too, but you lose the upgrade path (there is none) and you pollute vendor/.
Custom front-end layouts via the view column. The common pattern is a layout / view column on the page that maps to a Blade view:
// routes or a Page::render() helper
return view($page->view ?? 'pages.default', ['page' => $page]);
Keep a whitelist in config and validate the layout field with ->in(config('filament-pages.layouts')). Never let an editor free-type a view name — arbitrary view resolution is a file-inclusion footgun.
Slug routing with a scoped fallback. The catch-all route (/{slug} or /{page:slug}) must be registered last. In routes/web.php, register all your explicit routes and Route::fallback(...) style page route after them, and exclude admin/auth prefixes via a regex constraint:
Route::get('/{page:slug}', PageController::class)
->where('page', '^(?!admin|api|login).*$')
->middleware('web');
Separate "draft preview" from public routing. Add a published_at (or boolean) check in the controller, and give authenticated admins a bypass:
$page = Page::where('slug', $slug)
->when(! auth()->user()?->can('view unpublished pages'), fn ($q) => $q->published())
->firstOrFail();
Treat content as structured, not one blob. If the package gives you a single rich-text column, keep it, but add SEO fields (title, description, OG image) as separate columns and pass them to the layout rather than embedding meta tags in the editor body. Filament's RichEditor output is HTML; render it with {!! !!} only after sanitizing (see Gotchas).
Integrate with navigation/menus. Pages are rarely standalone. Query the model for menu building:
$nav = Page::published()->where('in_menu', true)->orderBy('menu_order')->get(['title', 'slug']);
Cache that query — page content is a read-heavy workload and the catch-all route means every front request touches this table.
Test the route resolution, not the resource. A Pest/PHPUnit feature test asserting /about returns 200 with the page title is worth more than testing the Filament form; the form will break loudly, the route will silently 404.
Forms\Components namespacing, Filament\Resources\Resource signatures, and ->schema() vs ->form() changed. Read it as a blueprint and port the pieces./{slug} swallows /login, /admin, /sitemap.xml, livewire asset routes, and storage routes. Constrain the slug pattern ([a-z0-9-]+, no slashes) and register last. Debug with php artisan route:list --path=about before assuming the package is broken.unique:pages,slug validation rule plus a reserved-slug blocklist (admin, api, login, register, sitemap). Editing a published page's slug breaks inbound links — either freeze slugs after publish or add a redirect table.resolveRouteBinding, not only in the admin form. Also remember soft-deleted pages: firstOrFail on a trashed page throws only if the model uses SoftDeletes and the default scope applies.RichEditor/Tiptap output is arbitrary HTML. Sanitize on save (HtmlSanitizer, strip_tags with an allowlist) or on render — the package ships no sanitization. Especially important if multiple users can edit pages.FileUpload fields, verify disk() and directory() match your FILESYSTEM_DISK and that php artisan storage:link has been run. A published page referencing /storage/... that 404s is the most common "it worked in admin" bug.php artisan route:cache), a newly created page won't appear, and cached HTML survives edits. Add Page::saved/deleted observers that flush a tagged cache (Cache::tags('pages')->flush() — requires a tag-capable store; the database/array drivers don't support tags).$model. The package's migrations are already published, so your model can extend the original and add relationships (categories, translations) without a schema rewrite.resources/views/vendor/filament-pages/ (if the provider registers loadViewsFrom) and overriding the resource. Grep the service provider to confirm which publishes() keys actually exist before writing publish tags in your deploy scripts.migrate → storage:link → view:clear → confirm the catch-all route position. Missing view:clear after copying package views is a surprisingly common silent failure with cached compiled views.How can I help you explore Laravel packages today?