Filament admin fields
The free rankbeam/laravel-seo-filament package adds a complete SEO section to any Filament resource form — two lines per resource. It supports Filament 4.x and 5.x (Livewire 3 and 4); the test suite passes unchanged on both majors.
Install
composer require rankbeam/laravel-seo-filamentThe model behind the resource must use the core HasSEO trait.
Add the section to a resource
use Rankbeam\Seo\Filament\Concerns\HasSEOFields;
class PostResource extends Resource
{
use HasSEOFields; // 1
public static function form(Schema $schema): Schema
{
return $schema->components([
TextInput::make('title'),
// ...
static::seoSection(), // 2
]);
}
}That renders a collapsible "SEO" section with:
- Title & description with live character counters — the 60/160 thresholds come from the core
SEOWarningEvaluator, so the admin UI and the audit layer can never disagree. - Focus keywords — a tags input. You type plain keywords; they persist in the core's structured
[{keyword, is_primary}]shape (the first is primary), sogetPrimaryKeyword()andSEODataread them unchanged. Enableseo.keywords.enabledto have theseo:auditcommand and the Pro scan flag pages that still lack a keyword (off by default — one gate, see Configuration). - Canonical URL (empty = automatic, query string stripped).
- Robots select (empty = site default).
- Social sharing image upload (og:image / twitter:image), stored on Filament's default disk under
seo/. - Search-snippet preview that mirrors the resolver's fallback chain live while you type.
- Source indicators — for each field, which resolver layer produced the effective value: Manual, Content fallback, Model-type default, Global default, Site config, or Derived from URL.
Limiting fields
static::seoSection(['title', 'description'])Accepts any subset of title, description, focus_keywords, canonical, robots, og_image.
Without the trait, SEOFields::make(?array $only) returns the same section directly.
How values persist
The section binds to a seo_meta state group and saves through the core seoMeta() relationship (update-or-create) — no columns on your own tables, and the values immediately become layer 6 (explicit) in the resolver.
Structured data (schema.org)
An optional Structured data section lets editors attach JSON-LD rich-result schema without touching code. Add it alongside the SEO section:
public static function form(Schema $schema): Schema
{
return $schema->components([
// ... your fields ...
static::seoSection(),
static::seoSchemaSection(), // optional
]);
}(or SEOSchemaFields::make() directly, without the trait).
It writes into the core seo_meta.schema_jsonld column — the same value the schema renderer emits — and is pure UI binding: every document is produced by a core schema builder and validated by the core SchemaValidator before it can be saved. It adds no schema logic of its own.
The section offers:
- Automatic breadcrumb — a single toggle, led with as the zero-config win. Derives a
BreadcrumbListfrom the record's parent chain viaBreadcrumbSchema::fromModelAncestors(). Nothing to fill in — it follows the model's ancestors. - Schema blocks — a repeater. Each block is either an FAQ (question / answer pairs →
FAQPage) or a Product (name, description, image, brand, SKU, price + currency, availability →Product), built by the coreFAQSchema/ProductSchemabuilders.
Validation
A block that would build malformed JSON-LD is rejected on save with the core validator's message — e.g. an FAQ entry with no answer, or a Product with no image or offer (Google requires an offer for Product rich results). Blocks left empty are simply ignored.
What it stores
schema_jsonld holds the built document(s): a single object when there is one, a JSON array when there are several (breadcrumb first, then your blocks). Both are valid JSON-LD and render unchanged through @seo / renderSchema().
Schema it doesn't manage
Schema you authored in code that this editor cannot represent — a hand-authored @graph, an exotic @type, or a Product carrying fields the form doesn't expose (reviews, ratings, GTIN/MPN) — is preserved verbatim. Opening and saving the form never clobbers it.
Testing note (testbench only)
If you boot Filament inside orchestra/testbench, register Filament's SupportServiceProvider before LivewireServiceProvider — Filament rebinds Livewire's DataStore, and the wrong order fails every Livewire test with ViewErrorBag::put(): ... null given. Real apps are unaffected (package discovery orders providers correctly).