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[&lt;head&gt;: title, meta, canonical, og, JSON-LD]

See also

Built by Qcentic