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.
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.
Installation
Bring the package into your repository via composer. Follow these steps to configure databases and assets.
Pull the package from Packagist into your project dependencies:
composer require nomanur/filament-seo-pro
Assets are managed locally via Filament's asset manager. Publish them into your public directory:
php artisan filament:assets
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
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).