kreetancraft / laravel-seo
SEO for Laravel — per-model meta, live content analysis, JSON-LD and a sitemap feed. Livewire 4 + Flux UI.
Requires
- php: ^8.2
- laravel/framework: ^12.0|^13.0
- livewire/flux: ^2.0
- livewire/livewire: ^4.0
- lorisleiva/laravel-actions: ^2.7
Requires (Dev)
- laravel/pint: ^1.27
- orchestra/testbench: ^10.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- spatie/laravel-permission: ^6.0|^7.0|^8.0
Suggests
- kreetancraft/laravel-media-manager: Resolves og:image from a media library, via seo.image_resolver.
- kreetancraft/laravel-user-management: Discovers SeoPolicy and creates its permissions automatically.
Provides
None
Conflicts
None
Replaces
None
README
Per-model SEO metadata, live content analysis, Google and social previews, JSON-LD helpers and a sitemap feed. Livewire 4 + Flux UI.
Ships no models of its own beyond SeoMeta, no layout, no CSS and no image handling — it renders
into your application and knows nothing about your content types until you tell it.
Design decisions worth knowing before you install
It knows nothing about your content types. The bulk screen and seo:analyze read a registry
that ships empty. A package that owns a content type contributes to it through a container tag, so
neither package names the other.
It names no permission of its own. The screens ask the ordinary authorization question and
SeoPolicy answers it. Until permissions exist anywhere in the app it is open, so it works on a
bare install rather than failing closed.
It ships no image handling. og:image resolves through seo.image_resolver when one is
configured, and falls back to the model's own default otherwise.
Flux is a hard dependency. The views use <flux:*> throughout.
Installation
composer require kreetancraft/laravel-seo
php artisan migrate
Let Tailwind see this package
Required. Tailwind v4 generates only the classes it finds by scanning files, and it does not scan
vendor/. In resources/css/app.css:
@source '../../vendor/kreetancraft/laravel-seo/resources/views';
Skipping it fails confusingly rather than loudly — classes shared with your own views still work and only the ones unique to this package go missing.
Making a model SEO-aware
use Kreetancraft\Seo\Concerns\HasSeo; class Article extends Model { use HasSeo; public function seoDefaults(): array { return [ 'title' => $this->title, 'description' => $this->excerpt, 'path' => '/articles/'.$this->slug, 'image' => null, ]; } }
Then register it so it appears on the bulk screen — from a package, through the tag:
$this->app->bind('acme.seo.models', fn () => [ Article::class => [ 'label' => 'Article', 'title' => 'title', 'edit_route' => 'admin.articles.edit', 'route_param' => true, ], ]); $this->app->tag('acme.seo.models', \Kreetancraft\Seo\Support\SeoModelRegistry::TAG);
Or from a host application, in config/seo.php under models.
The editor
Add the SEO card to any Livewire form:
use Kreetancraft\Seo\Livewire\Concerns\InteractsWithSeoForm; class EditArticle extends Component { use InteractsWithSeoForm; }
@include('seo::livewire.partials.seo-fields', ['seoAnalysis' => $this->seoAnalysis()])
That gives you meta fields, Open Graph and X/Twitter overrides, noindex/nofollow, live Google
and social previews, and the analysis panel. Pass ['compact' => true] to drop the previews and
analysis for a modal.
What gets analysed
Thirteen content checks — meta title present and length, meta description present and length, focus keyword in title, description, slug, content and headings, content length, headings present, image alt text, links — plus four readability checks: sentence length, paragraph length, passive voice and transition words.
Add your own by tagging it:
$this->app->tag([MyCheck::class], 'seo.checks.content');
Site health
<livewire:seo.health />
Average score, how much content has no meta title or description, and how much is unscored — counted across every registered model, not just rows that already have meta.
The sitemap feed
GET /api/v1/sitemap returns every public URL with lastmod. Contribute to it by implementing
ProvidesSitemapUrls and tagging the class:
$this->app->bind('acme.sitemap', fn () => ArticleSitemapProvider::class); $this->app->tag('acme.sitemap', \Kreetancraft\Seo\Support\SitemapProviderRegistry::TAG);
Permissions
SeoPolicy declares PERMISSION_SUBJECT = 'seo', so with
kreetancraft/laravel-user-management
installed one command creates view-seo, create-seo, update-seo and delete-seo:
php artisan user-management:sync-permissions
A SEO link also appears in that package's sidebar, with nothing declared on either side.
Images
Install kreetancraft/laravel-media-manager and point two values at it:
// config/seo.php 'image_resolver' => \Kreetancraft\Media\Support\MediaImageResolver::class, 'og_picker_view' => 'media::picker-field',
The resolver turns a model plus a collection name into URLs; the field renders the tiles, a
Choose button and the picker modal. Name your own view instead if you prefer — it receives
$items (already resolved), $group and $multiple.
Leave both null and og:image falls back to whatever seoDefaults()['image'] returns, with no
picker rendered.
Requirements
PHP 8.2+, Laravel 12 or 13, Livewire 4, Flux 2.
License
MIT.