visualbuilder / filament-screenshot-catalogue
Capture every page of a Filament v5 panel at multiple viewports and modes, upload to S3, generate a browsable index. Drop-in tooling for visual QA, design reviews, marketing assets, and AI-driven visual regression.
Package info
github.com/visualbuilder/filament-screenshot-catalogue
pkg:composer/visualbuilder/filament-screenshot-catalogue
Requires
- php: ^8.2
- filament/filament: ^5.0
- illuminate/bus: ^11.0 || ^12.0
- illuminate/console: ^11.0 || ^12.0
- illuminate/queue: ^11.0 || ^12.0
- illuminate/support: ^11.0 || ^12.0
- league/flysystem-aws-s3-v3: ^3.0
- spatie/laravel-package-tools: ^1.16
- symfony/process: ^7.0
Requires (Dev)
- orchestra/testbench: ^9.0 || ^10.0
- pestphp/pest: ^3.0 || ^4.0
- pestphp/pest-plugin-laravel: ^3.0 || ^4.0
README
Capture every page of a Filament v5 panel — at desktop, tablet, and mobile breakpoints, in light and dark mode — upload to S3, and publish a browsable, link-shareable index. Designed as the data source for visual QA, design reviews, marketing assets, and AI-driven visual regression workflows.
What you get
php artisan panel:sitemap --panel=admin # discover every URL php artisan screenshot:dispatch --panel=admin --tag=latest # queue per-page capture jobs php artisan screenshot:capture --panel=admin --page=dashboard --tag=latest # one-off sync run php artisan screenshot:rebuild-index --panel=admin --tag=latest # rebuild index.html only
The pipeline is:
- Sitemap generator walks the panel's resources, custom pages and auth screens, picks a representative record for
view/editURLs, and saves a JSON manifest tostorage/app/sitemap-{panel}.json. - Capture jobs drive Playwright through every entry × every viewport × every mode, save PNGs locally, and upload to your configured S3 disk.
- Index builder lists the run's S3 prefix and renders
index.htmlwith a sticky sidebar nav, scrollspy, lightbox, and one section per page. - Output lives at
s3://{disk}/{prefix}/{env}/{panel}/{version}/with a top-levelindex.htmland{slug}/{viewport}-{mode}.pngper shot.
Install
This is a development tool — most consumers will want it as a require-dev dependency so it never ships to production:
composer require --dev visualbuilder/filament-screenshot-catalogue
(If you genuinely need the artisan commands available in production — e.g. you run captures from a CI job that mounts production-like infrastructure — drop the --dev flag.)
The package's commands auto-register. Then make sure both Composer and Node prerequisites are in place:
| Dependency | Why it's needed | How to install |
|---|---|---|
aws/aws-sdk-php + an S3 disk |
Uploads + serves the catalogue | Already pulled in transitively. Configure an S3 disk in config/filesystems.php (default expected name: s3_public). |
| Node 18+ | Runs the Playwright runner | System-level (apt install nodejs / brew install node / etc.). |
| Playwright | Drives the headless browser | npm install playwright && npx playwright install chromium in the host project root. |
| A canonical "screenshot user" per panel | The runner logs in as this user and the sitemap generator uses them to find sample records | Seeded by the host (see Panel registration below). |
Optionally publish the config, the Node runner, or the bundled Claude Code skill:
php artisan vendor:publish --tag=filament-screenshot-catalogue-config # override defaults php artisan vendor:publish --tag=filament-screenshot-catalogue-js # custom Playwright runner php artisan vendor:publish --tag=filament-screenshot-catalogue-claude-skills # /screenshot-catalogue slash command
The Claude skill installs into the host's .claude/commands/screenshot-catalogue.md — gives Claude the full command surface, troubleshooting tips, and the PanelDescriptor registration pattern as context.
Panel registration
Hosts tell the package which panels to capture by registering a PanelDescriptor per panel — typically inside a small service provider:
namespace App\Providers; use Filament\Facades\Filament; use Illuminate\Support\ServiceProvider; use Visualbuilder\FilamentScreenshotCatalogue\PanelDescriptor; use Visualbuilder\FilamentScreenshotCatalogue\PanelRegistry; class ScreenshotCatalogueServiceProvider extends ServiceProvider { public function boot(): void { PanelRegistry::register(new PanelDescriptor( key: 'admin', panelId: 'admin', domain: env('ADMIN_DOMAIN'), loginHeading: 'Admin Login', email: env('SCREENSHOT_ADMIN_EMAIL'), password: env('SCREENSHOT_ADMIN_PASSWORD'), authenticator: static function (): void { $user = \App\Models\User::where('email', config('catalogue.admin_email'))->first(); if ($user !== null) auth('web')->setUser($user); }, )); } }
Add the provider to bootstrap/providers.php. If you installed the package as require-dev (the default suggestion above), wrap the registration in a class_exists check so production — where the package isn't installed — silently skips it instead of crashing on a missing class import:
// bootstrap/providers.php $providers = [ // ...your usual providers... ]; if (class_exists(\Visualbuilder\FilamentScreenshotCatalogue\PanelRegistry::class)) { $providers[] = App\Providers\ScreenshotCatalogueServiceProvider::class; } return $providers;
If you installed as a regular require dependency, register the provider unconditionally.
Tenanted panels (Filament's tenancy enabled): set the active tenant inside authenticator:
authenticator: static function (): void { $user = \App\Models\OrganisationUser::where('email', 'sample@example.com')->first(); if ($user !== null) { auth('organisation_user')->setUser($user); Filament::setTenant($user->organisation); } },
Configuration
Override defaults by publishing config/screenshot-catalogue.php. Common knobs:
return [ // S3 disk (must be configured in config/filesystems.php). 'disk' => 's3_public', // Top-level S3 prefix. 'path_prefix' => 'screenshots', // Capture viewports — name → { name, width, height }. Override to // standardise on your own breakpoints. 'viewports' => [ 'desktop' => ['name' => 'desktop', 'width' => 1280, 'height' => 800], 'tablet' => ['name' => 'tablet', 'width' => 768, 'height' => 1024], 'mobile' => ['name' => 'mobile', 'width' => 375, 'height' => 812], ], // CSS injected into every page just before the screenshot fires — // use this to lock host theme animations into a stable state. Empty // by default. // // Example for a theme that animates its topbar on root scroll: // // 'capture_time_css' => ' // .fi-topbar { padding-block: 1rem !important; } // .fi-main { padding-top: 7rem !important; } // ', 'capture_time_css' => '', // Sitemap entries to omit from the catalogue. Match the `slug` field // in `storage/app/sitemap-{panel}.json` — Filament's `{resource}.{page}` // form (e.g. `users.index`, `orders.edit`) for resource pages, or the // page class slug for custom pages. NOT URL slugs. 'excluded_slugs' => [], // Brand assets uploaded next to index.html so the catalogue's heading // shows your wordmark + favicon. Both are optional. 'brand' => [ 'name' => 'Panel Screenshots', 'logo' => public_path('media/your_logo_dark_mode.svg'), 'favicon' => public_path('media/your_favicon.svg'), ], ];
How the catalogue is laid out on S3
{prefix}/{env}/{panel}/{version}/ (env omitted in production)
├── index.html
├── favicon.svg
├── logo.svg
├── dashboard/
│ ├── desktop-light.png
│ ├── desktop-dark.png
│ ├── tablet-light.png
│ ├── tablet-dark.png
│ ├── mobile-light.png
│ └── mobile-dark.png
├── orders.index/
│ └── …
└── …
Tag-versioning: --tag=latest overwrites in place (continuous capture), any other tag (e.g. --tag=v2.5.0) is treated as immutable.
Commands
| Command | Use it for |
|---|---|
panel:sitemap --panel=KEY |
Generate sitemap-{panelId}.json for the panel. Re-run when routes change. |
screenshot:dispatch --panel=KEY --tag=latest |
Queue one CapturePageScreenshotsJob per sitemap entry plus a final RebuildScreenshotIndexJob. Use a queue worker. |
screenshot:capture --panel=KEY --page=SLUG --tag=latest |
Synchronous run, single page or a few. Useful for debug. |
screenshot:rebuild-index --panel=KEY --tag=latest |
Re-render index.html from the existing PNGs without re-capturing. |
All four accept --panel=KEY where KEY is the friendly CLI name registered in the descriptor (e.g. admin). The package translates to Filament's internal panel ID for URL/route resolution.
What's not in the package (host responsibilities)
- The seeded sample user(s) per panel — the package needs a real user to log in as, but creating one is host territory.
- Theme-specific tweaks beyond
capture_time_css(e.g. permanent CSS overrides for a custom design system) — keep those in your theme stylesheet. - Authentication wiring (tenants, magic links, 2FA dismissal) — the descriptor's
authenticatorclosure is the seam.
Production safety
The package refuses to run screenshot:capture and screenshot:dispatch when APP_ENV=production. The dispatch flow respects APP_DEBUG=true too — it bails out so the Laravel debug bar doesn't pollute every shot.
License
GPL-2.0-or-later.