34ml/filament-seo
Filament SEO adds SEO fields and management to Filament resources, letting you edit meta titles, descriptions, Open Graph data, and canonical URLs directly from the admin panel. Laravel-friendly and configurable, it keeps SEO tasks close to your content workflow.
This package wires romanlazko/laravel-seo (the polymorphic SEO meta model) into Filament, so a resource's edit form gains a ready-made SEO block (meta title, description, canonical, robots, OpenGraph, etc.) without hand-rolling the relation.
Minimal steps for v3:
composer require 34ml/filament-seo:^3.0 — this release requires Filament v5. If the panel is still on v3/v4, stay on 2.3.x.laravel-seo migration (the seo table) if you haven't already; then republish this package's assets/config per the README.HasSEO (from laravel-seo) to the model you want to describe — e.g. Post, Page, Product.form() schema, typically at the end of the main column or inside a dedicated Tabs tab.First use case to prove it out: take one existing resource (PageResource), add the SEO fieldset to form(), open an edit page, fill title/description, save, and confirm a row lands in seo with the correct model_type/model_id. Then render it in Blade ($page->seo->title) before doing the same rollout across other resources.
Read the README first for the exact component class name/namespace in v3, then the laravel-seo docs for how the meta is consumed on the front end — that split (Filament side vs. Eloquent side) is the main mental model you need.
Standard resource wiring. Keep the SEO block in a separate tab (Tabs\Tab::make('SEO')) so editors don't trip over it on every save. The component binds to the model's seo relation; you generally don't need to add ->relationship('seo') yourself, but if you extract a shared schema closure make sure the owning record exists before the fields hydrate (it does on edit pages, not on create).
Shared schema across resources. Put the SEO fields in a static method on a small helper class (SeoForm::schema()) and call it from every resource that has HasSEO. This keeps the block consistent when you upgrade the package — one place to change when a field is renamed.
Localisation. laravel-seo stores translations per locale in the meta payload. If you run a multi-locale panel, wrap the SEO tab in Filament's locale switcher workflow or render one SEO block per locale explicitly; don't assume the fieldset follows the panel locale automatically — verify what the saved seo row looks like per locale before shipping.
Programmatic filling. Seed defaults on create (->default(fn ($record) => $record?->title)), or in an observer/afterCreate hook, so a record that never gets touched by an editor still has a sane meta title. Good pattern: a SeoDefaults service that generates title/description from the model's own fields and only applies when the meta field is empty.
Table columns and N+1. Showing a "has SEO" badge or meta title in a table() column means a polymorphic seo relation per row. Eager load it (->modifyQueryUsing(fn ($q) => $q->with('seo'))) or use an aggregate/exists query. Without this, a 50-row index fires 50 extra queries.
Front-end integration. Do the rendering outside Filament — a Blade component or an SEO manager that reads $model->seo and falls back to model defaults. Keep that fallback logic in one place so the Filament form and the rendered <head> never disagree.
Permissions. The SEO tab is content, not configuration — but if marketing owns it, gate it with ->visible(fn () => auth()->user()->can('manageSeo', $record)) rather than hiding fields inside the schema.
laravel-seo version bump, expect Filament's own v4→v5 breaking changes in how form components, actions, and asset registration are declared. Budget a real upgrade slot: bump Filament first, get the panel booting, then add filament-seo:^3.0. Upgrading both at once makes failures ambiguous.laravel-seo bump can mean migration drift. Diff the published seo table migration against what's in your DB before running migrate in production; a changed column type or new index on a large table can lock.BadMethodCallException, the model is missing HasSEO or the relation name doesn't match what the package expects. Check the trait before debugging the form.seo row. Your front-end fallback logic must tolerate $model->seo === null.delete() that skips model events leaves seo rows behind. Add a deleting observer on SEO-enabled models, or clean up periodically.saved on SEO-enabled models to flush whatever cache key your front end uses.dd($record->seo()->first()) immediately after save in a tinker session. If the row is written there, the bug is in your front-end read; if not, it's the form's relationship wiring.How can I help you explore Laravel packages today?