bambamboole / filament-pages
A Filament plugin for managing hierarchical, block-based content pages with a drag-and-drop page tree, SEO integration, and live preview
Fund package maintenance!
Requires
- php: ^8.4
- bambamboole/filament-menu: ^0.2
- filament/filament: ^5.0
- filament/spatie-laravel-media-library-plugin: ^5.3
- league/commonmark: ^2.7
- nette/php-generator: ^4.2
- pboivin/filament-peek: ^4.0
- ralphjsmit/laravel-filament-seo: ^2.1
- ralphjsmit/laravel-seo: ^1.7
- spatie/laravel-package-tools: ^1.15.0
- spatie/laravel-responsecache: ^8.0
- spatie/php-attribute-reader: ^1.1
- spatie/php-structure-discoverer: ^2.4
Requires (Dev)
- larastan/larastan: ^3.2
- laravel/pint: ^1.0
- orchestra/testbench: ^9.0|^10.0
- pestphp/pest: ^3.7|^4.0
- pestphp/pest-plugin-arch: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- pestphp/pest-plugin-livewire: ^3.0|^4.0
- rector/rector: ^2.3
- spatie/browsershot: ^5.2
- spatie/laravel-ray: ^1.26
Suggests
- ryangjchandler/commonmark-blade-block: Required for Blade block support in markdown (^1.1)
- spatie/browsershot: Required for automatic OG image generation (^5.0)
- torchlight/torchlight-commonmark: Required for Torchlight syntax highlighting in markdown (^0.6)
README
A Filament plugin for managing hierarchical, block-based content pages. Features a drag-and-drop page tree, extensible block system (Markdown, Image out of the box), nested pages with automatic slug path computation, multi-locale support, SEO integration, and live preview.
Features
- Page Tree — Interactive drag-and-drop tree for reordering and nesting pages
- Block Builder — Flexible content composition with an extensible block system
- Markdown Block — Rich editor with table of contents, heading permalinks, GFM support, and optional Torchlight syntax highlighting
- Image Block — Spatie Media Library integration with responsive images and an image editor
- Custom Blocks & Layouts — Artisan generators for scaffolding your own blocks and layouts
- Multi-Locale — Locale-scoped page trees with automatic route prefixing and browser language detection
- SEO — Built-in SEO metadata tab with extensible fields and optional OG image generation via Browsershot
- Live Preview — Instant page preview powered by Filament Peek
- Publishing Workflow — Draft, published, and scheduled states with
published_atdatetime - Authorization — Laravel policy support for
create,update,delete, andreorderabilities - Frontend Routing — Catch-all route registration that resolves pages by slug path and locale
- Hierarchical Slugs — Automatic slug path computation based on the page tree hierarchy
Screenshots
| Page Editor | SEO Tab |
|---|---|
![]() |
![]() |
Installation
composer require bambamboole/filament-pages
Publish and run the migrations:
php artisan vendor:publish --tag="filament-pages-migrations"
php artisan migrate
Optionally publish the config:
php artisan vendor:publish --tag="filament-pages-config"
Register the plugin in your Filament panel provider:
use Bambamboole\FilamentPages\FilamentPagesPlugin; public function panel(Panel $panel): Panel { return $panel ->plugins([ FilamentPagesPlugin::make(), ]); }
Add the plugin views to your Tailwind CSS content paths:
@source '../../../../vendor/bambamboole/filament-pages/resources/**/*.blade.php';
Configuration
The plugin is configured through the fluent API on FilamentPagesPlugin and the published config file.
FilamentPagesPlugin::make() ->seoForm(fn () => [ TextInput::make('canonical_url'), ]) ->treeItemActions(fn (ManagePages $page) => [ Action::make('duplicate')->action(fn (array $arguments) => /* ... */), ]) ->previewView('my-custom-preview-view')
Blocks and layouts are auto-discovered using PHP attributes. Add your application's directories to the discovery paths in config/filament-pages.php:
// config/filament-pages.php 'block_discovery_paths' => [ app_path('Blocks'), ], 'layout_discovery_paths' => [ app_path('Layouts'), ],
Blocks
Blocks are classes annotated with #[IsBlock] that extend AbstractBlock. They are auto-discovered from the package's built-in Blocks/ directory and any directories listed in block_discovery_paths.
Two blocks ship out of the box:
- MarkdownBlock — Rich markdown editor with optional table of contents (top/left/right positioning), front matter parsing, and Torchlight syntax highlighting.
- ImageBlock — Spatie Media Library file upload with responsive images and an image editor.
Layouts
Layouts control how pages render on the frontend. They are classes annotated with #[IsLayout] that extend AbstractLayout. They are auto-discovered from the package's built-in Layouts/ directory and any directories listed in layout_discovery_paths.
Multi-Locale
Enable multi-language content by configuring locales in the config file:
// config/filament-pages.php 'routing' => [ 'prefix' => '', 'locales' => ['en' => 'English', 'de' => 'Deutsch'], ],
When locales are enabled, the page tree filters by locale and frontend routes include a {locale} prefix.
SEO
SEO is always enabled. Every page form includes an SEO tab (powered by ralphjsmit/laravel-filament-seo).
Extend the SEO form with custom fields:
->seoForm(fn () => [ TextInput::make('canonical_url'), ])
Response Caching
Enable HTTP response caching for frontend pages using spatie/laravel-responsecache:
// config/filament-pages.php 'cache' => [ 'enabled' => env('FILAMENT_PAGES_CACHE_ENABLED', false), 'lifetime' => 60 * 60, // 1 hour fresh 'grace' => 60 * 15, // 15 min stale-while-revalidate ],
When enabled, cached responses are served instantly and refreshed in the background after expiry. The cache is automatically invalidated when a page is saved or deleted.
Preview
Live preview is always enabled (powered by pboivin/filament-peek). To use a custom preview view:
->previewView('my-custom-preview-view')
Creating Custom Blocks
Generate a block stub with the Artisan command:
php artisan filament-pages:make-block MyCustomBlock
A custom block uses the #[IsBlock] attribute for metadata, extends AbstractBlock, and defines a build() method to configure the Filament form schema:
use Bambamboole\FilamentPages\Blocks\AbstractBlock; use Bambamboole\FilamentPages\Blocks\IsBlock; use Filament\Forms\Components\Builder\Block; use Filament\Forms\Components\TextInput; #[IsBlock(type: 'call-to-action', label: 'Call to Action')] class CallToActionBlock extends AbstractBlock { protected string $view = 'blocks.call-to-action'; public function build(Block $block): Block { return $block->schema([ TextInput::make('heading')->required(), TextInput::make('button_text')->required(), TextInput::make('button_url')->url()->required(), ]); } }
The #[IsBlock] attribute accepts type (unique identifier matching the block type stored in the database), label (display name), and an optional translateLabel flag. If label is omitted, it is derived from the type automatically.
Block Schema (Structured Data)
Blocks can contribute JSON-LD structured data by overriding the registerSchema() method. The page model collects schema entries from all blocks and passes them to the SEO layer automatically.
use Bambamboole\FilamentPages\Blocks\AbstractBlock; use Bambamboole\FilamentPages\Blocks\IsBlock; use Bambamboole\FilamentPages\Models\Page; use Filament\Forms\Components\Builder\Block; use Filament\Forms\Components\Repeater; use Filament\Forms\Components\TextInput; use RalphJSmit\Laravel\SEO\SchemaCollection; #[IsBlock(type: 'faq', label: 'FAQ')] class FaqBlock extends AbstractBlock { protected string $view = 'blocks.faq'; public function build(Block $block): Block { return $block->schema([ Repeater::make('questions')->schema([ TextInput::make('question')->required(), TextInput::make('answer')->required(), ]), ]); } public function registerSchema(SchemaCollection $schema, array $data, Page $page): SchemaCollection { return $schema->addFaqPage(function ($faqSchema) use ($data) { foreach ($data['questions'] ?? [] as $item) { $faqSchema->addQuestion($item['question'], $item['answer']); } return $faqSchema; }); } }
The SchemaCollection supports addFaqPage(), addArticle(), and addBreadcrumbs() out of the box. Blocks that don't override registerSchema() contribute nothing — no empty <script> tags are generated.
Block Assets
Blocks can declare CSS and JavaScript assets by overriding the assets() method. Assets are deduplicated per request and rendered via Blade directives in your layout.
use Bambamboole\FilamentPages\Blocks\BlockAsset; public function assets(): array { return [ 'css' => [ BlockAsset::url(asset('css/cta.css')), BlockAsset::inline('.cta { background: var(--primary); }'), ], 'js' => [ BlockAsset::url('https://cdn.example.com/lib.js'), BlockAsset::inline('console.log("CTA loaded");'), ], ]; }
BlockAsset::url() creates a <link>/<script src> tag while BlockAsset::inline() creates an inline <style>/<script> tag. CSP nonces are applied automatically when Vite::cspNonce() is set.
Include the Blade directives in your layout to render block assets:
<head> @filamentPagesStyles @filamentPagesBlockStyles </head> <body> {!! $page->renderBlocks() !!} @filamentPagesBlockScripts </body>
Blocks that don't override assets() contribute nothing.
Creating Custom Layouts
Generate a layout stub:
php artisan filament-pages:make-layout LandingPage
A layout uses the #[IsLayout] attribute for metadata, extends AbstractLayout, and provides a render() method:
use Bambamboole\FilamentPages\Layouts\AbstractLayout; use Bambamboole\FilamentPages\Layouts\IsLayout; use Bambamboole\FilamentPages\Models\Page; use Illuminate\Http\Request; use Illuminate\View\View; #[IsLayout(key: 'landing-page', label: 'Landing Page')] class LandingPageLayout extends AbstractLayout { protected string $view = 'layouts.landing-page'; public function render(Request $request, Page $page): View { return view($this->view, ['page' => $page]); } }
The #[IsLayout] attribute accepts key (unique identifier used in the database), label (display name), and an optional translateLabel flag. If label is omitted, it is derived from the key automatically.
Include the asset directives in your layout to load the frontend CSS and any block-specific assets:
<head> @filamentPagesStyles @filamentPagesBlockStyles </head> <body> {!! $page->renderBlocks() !!} @filamentPagesBlockScripts </body>
Route Registration
Register the frontend page routes in your routes/web.php:
use Bambamboole\FilamentPages\Facades\FilamentPages; FilamentPages::routes();
When locales are configured, routes are automatically prefixed with {locale}.
Frontend Rendering
Pages are rendered through their assigned layout using {!! $page->renderBlocks() !!}.
You can also render blocks programmatically:
$page = Page::where('slug_path', '/about')->first(); echo $page->renderBlocks();
Import & Export
Pages can be exported to and imported from YAML files. Both commands are idempotent — running them multiple times produces the same result.
Export
php artisan filament-pages:export --path=resources/pages --locale=en --type=page
Exports pages as YAML files in a hierarchical directory structure. Media files are exported alongside their page. Parent pages with children use _index.yaml, and filenames are prefixed with their sort order (e.g. 01-about/).
Import
php artisan filament-pages:import --path=resources/pages --locale=en --type=page --prune --dry-run
| Option | Description |
|---|---|
--path |
Source directory (default: resources/pages) |
--locale |
Locale for imported pages |
--type |
Page type (default: page) |
--prune |
Soft-delete database pages not found in source |
--dry-run |
Preview changes without modifying the database |
The importer creates, updates, or skips pages based on whether changes are detected. Soft-deleted pages are restored if they reappear in the source files. Media referenced in YAML blocks are imported automatically.
Page Tree
The plugin provides an interactive page tree at /pages in your Filament panel. You can:
- Drag and drop to reorder and nest pages
- Create, edit, and delete pages via slide-over modals
- Publish/unpublish with datetime scheduling
- Switch between locales
- Preview pages before publishing
Custom actions can be added to tree items:
FilamentPagesPlugin::make() ->treeItemActions(fn (ManagePages $page) => [ Action::make('duplicate')->action(fn (array $arguments) => /* ... */), ])
Authorization
The package supports Laravel policies for access control. Create a policy for your page model to restrict actions:
php artisan make:policy PagePolicy --model=Page
Supported abilities: create, update, delete, reorder.
The package is permissive by default — when no policy is registered, all actions are allowed. Once a policy exists, only explicitly allowed abilities are permitted.
Laravel Boost Skill
This package ships with a Laravel Boost skill that teaches AI agents how to create custom blocks and layouts. The skill is auto-installed when users run php artisan boost:install and provides detailed guidance on the block/layout architecture, asset system, and testing patterns.
Testing
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Credits
License
The MIT License (MIT). Please see License File for more information.

