Filament SEO Pro

Developed by Nomanur Rahman

The definitive SEO toolkit for Filament v4. Bring a complete, Yoast-like content analysis system, Google searches, social media preview cards, Schema markup, and automated metrics into your Filament Admin panels.

v0.1.0 PHP 8.2+ Laravel 10 / 11 / 12 Filament v3 / v4 / v5 MIT License

Prerequisites

This package integrates deep SEO checks directly inside your panel provider. You will need a working Laravel application running Filament. Active support is optimized for Filament v4 panels.

Key Features

Filament SEO Pro is designed from the ground up to provide a world-class editor experience. No external API requests are performed during editing, ensuring zero latency and privacy compliance.

🔍
Live SEO Analysis
13 checks testing focus keywords, content lengths, title tags, metadata density, and internal link configurations in real time.
📖
Readability Analyzer
Engine computes Flesch-Kincaid-inspired indices, highlighting transition word usage, passive voice percentage, and paragraph structures.
💻
SERP Live Previews
Visualizes real-time Google search snippet rendering. Support includes both mobile and desktop view configurations.
🌐
Social Cards Previews
Visual preview cards for Open Graph (Facebook, LinkedIn) and Twitter/X, with custom image, title, and description options.
📊
SEO Management Page
A centralized, bulk administration panel mapping all configured Models, listing SEO scores, filter structures, and CSV exports.
Performance Cache
Scores and analysis states can be queued asynchronously and cached locally to prevent database load spikes.

Installation

Bring the package into your repository via composer. Follow these steps to configure databases and assets.

1
Download Package

Pull the package from Packagist into your project dependencies:

composer require nomanur/filament-seo-pro
2
Publish Frontend Assets

Assets are managed locally via Filament's asset manager. Publish them into your public directory:

php artisan filament:assets
3
Run Polymorphic Migrations

Publish and run the migrations to create the database table `seo_meta` that stores all records polymorphically:

php artisan vendor:publish --tag="filament-seo-pro-migrations"
php artisan migrate
4
Publish Configuration File (Optional)

Create a local copy of the global configuration files to customize checking criteria:

php artisan vendor:publish --tag="filament-seo-pro-config"

Initial Setup

Register the plugin directly inside your panel provider class. This binds all page builders and widget registers.

Edit your panel provider (typically app/Providers/Filament/AdminPanelProvider.php):

<?php

namespace App\Providers\Filament;

use Filament\Panel;
use Filament\PanelProvider;
use Nomanur\FilamentSeoPro\SeoPlugin;

class AdminPanelProvider extends PanelProvider
{
    public function panel(Panel $panel): Panel
    {
        return $panel
            ->default()
            ->id('admin')
            ->path('admin')
            // ...other settings
            ->plugins([
                SeoPlugin::make()
                    ->defaultContentField('body')
                    ->defaultTitleField('name')
                    ->models([
                        \App\Models\Post::class,
                        \App\Models\Page::class,
                    ]),
            ]);
    }
}

Model Configuration

To associate SEO attributes with a model, apply the HasSeo trait. This creates a polymorphic relationship to the SeoMeta model.

Example using a `Post` model (app/Models/Post.php):

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Nomanur\FilamentSeoPro\Traits\HasSeo;

class Post extends Model
{
    use HasSeo;
    
    // Optional: Provide custom getUrl() method for the live URL previewer
    public function getUrl(): string
    {
        return route('posts.show', $this->slug);
    }
}

Available Trait Helper Methods

The HasSeo trait provides several helpful runtime methods:

Method Return Type Description
$model->seo() MorphOne Get the Eloquent polymorphic relationship instance.
$model->getOrCreateSeo() SeoMeta Get the existing SEO metadata model or initialize a blank one with index directives.
$model->hasSeoMeta() bool Checks if the model has a non-null custom SEO title or description.
$model->updateSeoScore(int $score) void Atomically writes a calculated SEO score to the DB.
$model->getSeoAnalysisData() array Compiles content, slugs, URLs, titles, and keys for the engine analyzer.

Automatic Cascade Deletion

When you call $model->delete(), the trait boots a deleting listener that automatically deletes the associated SeoMeta record from the database to prevent orphaned records.

Form Integration

You can choose between two form components depending on your resource layout: Tabs or Sections.

Option A: Drop-in Tab (Recommended)

If your form uses Filament's `Tabs` component, simply register the tab. Place this in your resource form method:

use Filament\Forms\Components\Tabs;
use Nomanur\FilamentSeoPro\Forms\SeoTab;

public static function form(Form $form): Form
{
    return $form
        ->schema([
            Tabs::make('Main Layout')
                ->tabs([
                    Tabs\Tab::make('Content')
                        ->schema([
                            // Your standard fields...
                        ]),
                    SeoTab::make(), // Automatically injects the full SEO interface
                ])
        ]);
}

Option B: Inline Section

If your form does not use tabs, you can use the collapsible section component instead:

use Nomanur\FilamentSeoPro\Forms\SeoSection;

public static function form(Form $form): Form
{
    return $form
        ->schema([
            // Your standard fields...
            
            SeoSection::make(), // Automatically collapsible section at the bottom
        ]);
}

Overriding Defaults on the Fly

By default, the plugin queries global configs to fetch fields. You can override these mapping rules dynamically on specific resource forms:

SeoTab::make()
    ->contentField('post_body')  // Bind to custom body field name
    ->titleField('post_title')   // Bind to custom title field name
    ->slugField('post_slug')     // Bind to custom slug field name

Interactive Plugin Builder

Use this utility to configure your panel integration options. Tweak the inputs below, and copy the instantly updated PHP code for your PanelProvider registration.

SeoPlugin::make()
    ->defaultContentField('content')
    ->defaultTitleField('title')
    ->defaultSlugField('slug')
    ->enableDashboardWidget(true)
    ->enableManagementPage(true)
    ->translatable(false)
    ->models([
        \App\Models\Post::class,
        \App\Models\Page::class,
    ])

Configuration Reference

Publishing the configuration files creates two separate files in your config directory. Learn when and how to modify them below.

1. Panel Settings: config/filament-seo-pro.php

Controls global field mapping fallbacks, translatable supports, cache lifetimes, and dashboard lists.

Option Key Default Value Usage Description
default_content_field 'content' Fallback field checked to fetch content details for real-time analyses.
default_title_field 'title' Fallback field checked to fetch title details for Google SERP analysis.
default_slug_field 'slug' Fallback field checked to fetch the slug structure for preview links.
translatable false Set true if you use Spatie's multi-language translation traits in models.
cache_ttl 3600 Time (in seconds) to cache SEO grades. Set to 0 to disable database caching.
queue_analysis false Offload heavy calculations to background queues instead of computing on form input triggers.

2. Engine Tuning: config/config.php

Fine-tune target reading indices, minimum word limits, passive voice indicators, and individual check point weights.

// Snippet from config/config.php
'readability' => [
    'target_sentence_length' => [
        'min' => 15,
        'max' => 20,
    ],
    'target_paragraph_sentences' => [
        'min' => 2,
        'max' => 4,
    ],
    'max_passive_voice_percentage' => 10.0,
    'min_transition_words' => 3,
],

'weights' => [
    'title_exists' => 10,
    'title_length' => 10,
    'keyword_in_title' => 10,
    'description_exists' => 10,
    'content_length' => 10,
    'internal_links' => 5,
    'external_links' => 5,
],

Frontend Rendering

To render the configured metadata tags, Open Graph, and Twitter Cards in your public HTML template, query the polymorphic relation directly inside your layout file.

Insert this standard boilerplate inside the <head> tag of your global Blade layout (e.g., resources/views/layouts/app.blade.php):

<head>
    <!-- Basic Meta Tags -->
    <title>{{ $model->seo?->title ?? $model->title }}</title>
    <meta name="description" content="{{ $model->seo?->description ?? Str::limit(strip_tags($model->content), 150) }}">
    @if($model->seo?->keywords)
        <meta name="keywords" content="{{ $model->seo->keywords }}">
    @endif
    <meta name="robots" content="{{ $model->seo?->robots ?? 'index, follow' }}">
    <link rel="canonical" href="{{ $model->seo?->canonical_url ?? request()->url() }}">

    <!-- Open Graph (Facebook / LinkedIn) -->
    <meta property="og:type" content="website">
    <meta property="og:url" content="{{ request()->url() }}">
    <meta property="og:title" content="{{ $model->seo?->og_title ?? $model->seo?->title ?? $model->title }}">
    <meta property="og:description" content="{{ $model->seo?->og_description ?? $model->seo?->description ?? '' }}">
    @if($model->seo?->og_image)
        <meta property="og:image" content="{{ Storage::disk(config('filament.default_filesystem_disk', 'public'))->url($model->seo->og_image) }}">
    @endif

    <!-- Twitter Cards -->
    <meta name="twitter:card" content="summary_large_image">
    <meta name="twitter:url" content="{{ request()->url() }}">
    <meta name="twitter:title" content="{{ $model->seo?->twitter_title ?? $model->seo?->title ?? $model->title }}">
    <meta name="twitter:description" content="{{ $model->seo?->twitter_description ?? $model->seo?->description ?? '' }}">
    @if($model->seo?->twitter_image)
        <meta name="twitter:image" content="{{ Storage::disk(config('filament.default_filesystem_disk', 'public'))->url($model->seo->twitter_image) }}">
    @endif

    <!-- Schema.org JSON-LD Markup -->
    @if($model->seo?->schema_type)
        <script type="application/ld+json">
        {
            "@context": "https://schema.org",
            "@type": "{{ $model->seo->schema_type }}",
            "mainEntityOfPage": {
                "@type": "WebPage",
                "@id": "{{ request()->url() }}"
            },
            "headline": "{{ $model->seo?->title ?? $model->title }}",
            "description": "{{ $model->seo?->description ?? '' }}"
        }
        </script>
    @endif
</head>

Eager Loading Tip

To avoid N+1 query problems in lists or pages executing metadata reviews, eager load the SEO relationship in your controllers: $posts = Post::with('seo')->get();

Testing & CI

Ensure that all local code contributions pass automated checks. The package contains a comprehensive suite of Pest tests and strict Larastan analysis rules.

To execute the tests locally, run the following script:

composer test

To run static analysis checks via PHPStan/Larastan:

composer analyse

To automatically format codebase code structures following Pint guidelines:

composer format

Troubleshooting

Solutions to common issues encountered during setup or local package operations.

1. Visual elements are blank or styles are broken

If the Google Search Preview, readability panels, or SEO checklist fail to display styling inside your admin panel, re-publish the frontend assets. This moves the compiled package CSS files into Laravel's public asset storage:

php artisan filament:assets

2. Model validation errors: SEO fields cannot accept null states

If you encounter database level exceptions trying to save model forms, ensure you have executed migrations. The polymorphic relationship queries the seo_meta table, which creates non-null defaults for columns like robots and seo_score.

3. Spatie Translatable support is failing

If you translate your content field (e.g. content is translated), make sure you enable Spatie support within your configuration or inside your panel provider registration by adding ->translatable(true).