tomatophp/filament-cms
Full CMS builder for Filament: manage posts, categories, and content with built-in comments and ratings, multi-meta and SEO fields, plus import/export of integrations. Install with Composer, run filament-cms:install, then register the FilamentCMSPlugin in your admin panel.
Requirements first. Pick the package branch that matches your Filament major: v1–v3 for Filament v3-era apps, v4 for Filament v4, and v5 (released 2026-10-13) for Filament v5 + Laravel 12/13 on PHP 8.2+. The README's caution is serious: jumping v1 → v4 loses features that were split out into "integrated packages," so a legacy install should be treated as a migration project, not a composer update.
composer require tomatophp/filament-cms
php artisan filament-cms:install
The installer wires up config and the Spatie settings/translatable dependencies the package leans on (spatie/laravel-settings, lara-zeus/spatie-translatable in v5). Before you trust it, publish and inspect what it wrote:
php artisan vendor:publish --tag="filament-cms-config"
php artisan vendor:publish --tag="filament-cms-migrations"
php artisan migrate
Register the plugin in your panel provider — this is where day-to-day behaviour is decided, since Post and Category resources only appear if you ask for them:
->plugin(
\TomatoPHP\FilamentCms\FilamentCMSPlugin::make()
->useCategory()
->usePost()
->allowExport()
->allowImport()
)
Drop useCategory() if you're managing taxonomy elsewhere; drop allowImport()/allowExport() if content editors shouldn't move data around.
Where to look first. Open the Post resource in the panel and create one post end-to-end — title, body, SEO tab, category, author. That single pass shows you the shape of the data model (posts with multi-meta, categories, comments/ratings) and tells you quickly whether the default form matches your content model. If your content isn't a "post," don't fight the form — register a custom type in AppServiceProvider::boot():
use TomatoPHP\FilamentCms\Facades\FilamentCMS;
use TomatoPHP\FilamentCms\Services\Contracts\CmsType;
FilamentCMS::types()->register([
CmsType::make('building')
->label('Buildings')
->icon('heroicon-o-home')
->color('danger'),
]);
That's the first real use case: one CMS install, several editorial content types, each with its own label/icon/badge in the admin.
Frontend is yours. This is an admin-side CMS. There's no documented public routing or Blade rendering in the README, so plan on your own controller/route/resource layer (or a headless JSON endpoint) reading the Post model. Budget for that; don't assume the package ships a blog.
Model your authors before you seed content. The default author binding is the auth user model, but registration makes it explicit and multi-source:
use TomatoPHP\FilamentCms\Services\Contracts\CmsAuthor;
FilamentCMS::authors()->register([
CmsAuthor::make('Admin')->model(\App\Models\User::class),
]);
This is the pattern to copy whenever the package offers a register()/types() style registry: resolve it once in boot(), keep it declarative, and let the package own the wiring.
Extend resources through the registries rather than forking. Post and Category expose six extension points each — form, table columns, row actions, bulk actions, filters, infolist entries. A typical client request ("add a custom_field, show it in the table and the view page, filter on it") becomes three small registrations in one provider, no vendor edits:
PostForm::register([TextInput::make('custom_field')->label('Custom Field')->required()]);
PostTable::register([TextColumn::make('custom_field')->label('Custom Field')->sortable()->searchable()]);
PostInfoList::register([TextEntry::make('custom_field')->label('Custom Field')]);
PostFilters::register([
Filter::make('custom_field')
->form([TextInput::make('custom_field')])
->query(fn ($query, array $data) => $query->when(
$data['custom_field'],
fn ($q, $v) => $q->where('custom_field', 'like', "%{$v}%")
)),
]);
For anything reused across panels or packages, wrap it in a class extending Component / Column / Entry and register CustomFieldComponent::make(). That keeps the provider short and makes the component testable in isolation.
Keep side effects in the events, not in the form. PostCreated, PostUpdated, PostDeleted are the sanctioned hook for notifications, search indexing, cache busting, webhooks. Register listeners the normal Laravel way; pass $event->post (verify the property name against the source) into a queued job so editorial saves stay fast.
Import/export as an integration surface. allowImport()/allowExport() gives you the built-in content transfer, and FilamentCMS::registerImportAction(Action::make('import')) lets you bolt on your own source (a legacy WordPress dump, a partner feed, an internal API). Realistic workflow: export a CSV/JSON snapshot from staging, transform it offline, then import into production with your custom action so the mapping logic lives in your codebase rather than in someone's spreadsheet.
Comments and ratings are the "community" half of the feature list. Treat them as an admin moderation queue: filter by unapproved, bulk-action to approve/spam, and only then expose them publicly from your own controller.
TomatoPHP\FilamentCms\Facades\FilamentCMS and TomatoPHP\FilamentCms\Facade\FilamentCMS (singular, in the import-action snippet). The plural Facades is correct — the singular one will throw a class-not-found if you copy it verbatim. Grep the installed package's src/Facades directory to confirm.Filament\Tables\Actions\Action with Filament\Actions\Action, and mentions Filament\Schemas\Components\Component (v4+) alongside Filament\Forms\Components\Field. Resolve imports against your installed Filament version before blaming the package.v1.0.7 existed solely to fix Spatie\LaravelSettings\Migrations\SettingsMigration not found. If your install fails at migrate time, that's the settings package not being pulled in or its migrations not published — fix the dependency, don't hand-write the table.filament-cms:install → publish config/migrations → migrate → then open the panel. Opening the panel first on an unmigrated database gives confusing resource errors.boot(), and be aware of panel scope. FilamentCMS::types() and friends are global; FilamentCMSPlugin flags are per-panel. If you register types inside a panel provider's register(), they may not be available when the resource schema is built.lara-zeus/spatie-translatable, which assumes spatie/laravel-translatable conventions on the model, the *_translations columns, and a locale list. If your app has a custom locale switcher, verify it agrees with Filament's.author select and filter use the configured auth user model (v5 changelog) — if you've pointed CmsAuthor at a non-default model, re-check the author filter after upgrading; earlier versions hardcoded the assumption.composer.lock, read src/ when behaviour surprises you (the code is the documentation here), and avoid ^ ranges on the CMS package across Filament majors.php artisan migrate and check git diff on published migrations after every bump; a category that "disappeared" is usually a deleted_at column that arrived late.composer test / composer analyse setup so extension code stays green.register()/get() on PostForm) to confirm your provider ran after the package's. Panel providers and AppServiceProvider ordering is the usual culprit.How can I help you explore Laravel packages today?