rmzamora/seo-bundle
A Laravel SEO bundle that gathers common optimization tools into one package. Manage meta tags, Open Graph data, and other search-friendly markup for your models and views, with straightforward configuration and Blade-friendly integration. Useful for consistent SEO defaults across pages.
First, the reality check: this is not a Laravel package. rmzamora/seo-bundle is a fork of SonataSeoBundle, a Symfony bundle that depends on symfony/framework-bundle, sonata-project/admin-bundle, and sonata-project/block-bundle. It ships no ServiceProvider, no config/*.php, no facade, no Blade views, and no Laravel-facing API. There are 0 dependents, 0 stars, and the repo is archived, so treat it as reference material rather than a dependency you can composer require and use.
Minimal steps a Laravel dev should take before touching it:
composer.json first. Confirm the require block: you will see symfony/* and sonata-project/* constraints. If those don't resolve against your app's PHP/Symfony polyfills, stop — do not force it in.SonataSeoBundle / SonataSeoBundle::class) and DependencyInjection/SonataSeoExtension.php. This tells you the config tree (sonata_seo:), the default page metadata, and which services are registered.Twig/Extension/* (e.g. SeoTwigExtension) and the templates under Resources/views/ are where <title>, meta description, canonical, OpenGraph, and Twitter card tags are actually emitted. This is the part you'll want to imitate.Controller/. These are the only pieces with obvious Laravel analogues (spatie/laravel-sitemap).<head> block in layouts/app.blade.php with dynamic per-page title/description/OG tags driven by a model, plus a generated /sitemap.xml and /robots.txt.If your goal is "Laravel app needs SEO meta + sitemap," skip this package and evaluate artesaos/seotools, ralphjsmit/laravel-seo, and spatie/laravel-sitemap instead. Read SonataSeoBundle only for its metadata-modeling ideas (a SeoPage object holding title/description/metas/OG/Twitter, exposed to templates).
Since you can't run this bundle in Laravel as-is, the useful patterns are the ones you transcribe, plus a note on what a forced integration would look like.
Metadata object pattern (the core idea to steal). SonataSeoBundle centers on a mutable SeoPage/SeoPageInterface service holding title, description, keywords, metas[], oGP/twitter bags, and a lang. In Laravel you mirror this with a singleton bound in a service provider:
// AppServiceProvider::register()
$this->app->singleton(SeoPage::class, fn () => new SeoPage());
Controllers, middleware, or a HasSeo trait on models set fields; a Blade component renders them. This gives you the same "set anywhere, render once" ergonomics the Symfony bundle gives via its container service.
Twig extension → Blade component/directive. The bundle exposes helpers like sonata_seo_title(), sonata_seo_metadatas(), sonata_seo_html_attributes(), sonata_seo_head_attributes(). Map each to a Blade component (<x-seo.meta />) or an @seo directive registered in a provider. Keep the output HTML-escaping identical (the bundle escapes attributes via Twig autoescaping; Blade's {{ }} does the same, so don't switch to {!! !!}).
Sonata Admin block → Filament/Nova field. In Symfony you'd attach SEO fields to entities through Sonata Admin form mappers. In Laravel the equivalent is a Filament Section/custom field or a Nova field that writes to a seo_meta JSON column. The bundle's Block\ classes show the field set worth copying: title, description, keywords, robots directives, canonical URL, OG image.
Event listener → middleware / model events. The bundle hooks Symfony kernel/request events to inject defaults and per-request overrides. The Laravel equivalent is a SeoDefaults middleware that seeds the singleton from route/model data, or model->saved listeners that regenerate sitemap entries.
Sitemap & robots controllers → routes + commands. The bundle's SitemapController/RobotsController become Laravel routes returning XML/text, ideally cached. Generate the sitemap in a queued job/artisan command (like spatie/laravel-sitemap) rather than per-request.
Config tree → config/seo.php. The sonata_seo: config (defaults for title, metadatas, encoding, page defaults, sitemap settings) maps cleanly to a publishable Laravel config file. Keep defaults and per-context overrides (web, admin) as nested arrays.
If you truly must run the Symfony bundle (e.g., a Laravel + Symfony hybrid or a Sonata admin alongside), you'd register the bundle through a Symfony kernel bridge and let it boot its own container — this is fragile, pulls in Sonata Admin + BlockBundle, and is not something to do "day-to-day." Don't.
sonata-project/admin-bundle and sonata-project/block-bundle transitively. That's a full admin panel you probably don't want in a Laravel app.composer require alone does nothing — there is no extra.laravel.providers. You must manually wire services or bridge the Symfony container..html.twig) and depend on Symfony form/block helpers. They will not render under Blade; you must rewrite head/meta output by hand.sonata_seo config uses page defaults merged with runtime SeoPage mutations — order matters. If you port it, decide explicitly whether defaults are applied before or after controller-set values (the bundle applies defaults first, then request/model overrides win). Getting this backwards is the #1 source of "my title won't change" bugs.{{ }} for everything in <head> — never {!! !!} for user-supplied descriptions or titles.?page=2) with rel=prev/next or noindex explicitly — the bundle does not do this for you, and neither will a naive port.SeoPage (title, metas, canonical) in a middleware in local to catch empty/duplicate tags before deploy.SeoPageInterface for swappable implementations, a seo.render/seo.meta event or hook so packages can inject tags, and a config-driven defaults layer. Those three are the bundle's real value.artesaos/seotools (metadata singleton + Blade helpers) or ralphjsmit/laravel-seo (model-attached SEO) for meta tags, and spatie/laravel-sitemap for sitemaps. If you liked SonataSeoBundle's SeoPage design, those libraries already implement the same pattern in idiomatic Laravel.How can I help you explore Laravel packages today?