Fields and morph
The SEO plugin stores per-record metadata on a single polymorphic table and renders it into the document head. Three pieces do the work: the HasSeo trait, the SeoFields form schema, and the SeoTags renderer.
The morph
SEO lives in one seo_meta table shared by every content model. The migration uses $table->morphs('seoable'), producing seoable_type and seoable_id, with a unique pair so each model owns at most one row.
Schema::create('seo_meta', function (Blueprint $table) {
$table->id();
$table->morphs('seoable');
$table->string('title')->nullable();
$table->string('description', 512)->nullable();
$table->string('canonical_url')->nullable();
$table->string('og_image_url')->nullable();
$table->timestamps();
$table->unique(['seoable_type', 'seoable_id']);
});
Any model that wants SEO just uses the trait. The relation is morphOne:
class Post extends Model
{
use HasSeo;
}
HasSeo exposes $model->seo, a MorphOne to SeoMeta. The inverse on SeoMeta is seoable() (MorphTo).
SeoFields
SeoFields::make() returns a Filament Section bound to the seo relationship, so saving the form writes the seo_meta row automatically. The schema is four fields:
| Field | Type | Limit |
|---|---|---|
title |
TextInput | 70 |
description |
Textarea (3 rows) | 160 |
canonical_url |
URL | 2048 |
og_image_url |
URL | 2048 |
The 70/160 limits are the form-side enforcement (the column is wider). There is no separate og_title/og_description field — Open Graph title and description reuse title and description. JSON-LD is not a form field; it is generated at render time.
SeoTags
SeoTags::for($model) reads the morph row and falls back to the model's title attribute and config('app.name'). toHtml() renders the filament-seo::tags Blade view, emitting <title>, meta description, <link rel="canonical">, og:* and twitter:* tags, and a <script type="application/ld+json"> block (a WebPage schema built from the same fields).
<head>
{!! \Mamenein\FilamentSeo\SeoTags::for($post)->toHtml() !!}
</head>
If the model implements HasSocialImage (or exposes seoThumbnailUrl()), that URL is used as og:image when og_image_url is empty.
flowchart LR
M[Model uses HasSeo] -->|morphOne| R[(seo_meta row)]
R --> F[SeoFields form schema]
R --> T[SeoTags renderer]
T --> H[<head>: title, meta, canonical, og, JSON-LD]