setono/seo-bundle
Setono SEO Bundle brings SEO tooling to Symfony apps: manage meta tags, titles and descriptions, generate sitemaps, and control robots directives from one configurable package. It integrates with your entities and Twig templates so you can improve search visibility without custom boilerplate.
First, check the framework. Despite the framing, setono/seo-bundle is a Symfony bundle, not a Laravel package. There is no service provider, no config/seo.php, no facade. If you are in a Laravel codebase, stop here — you would be reimplementing the Twig/DI layer or writing a bridge, which is more work than the bundle saves. The realistic path in Laravel is to read the source for the approach (how it models canonical URLs and JSON-LD) and build it yourself.
If you are on Symfony, the minimum sequence is:
composer require setono/seo-bundle:0.1.* — pin the minor explicitly. Do not use ^0.1.src/ in config/bundles.php.bin/console debug:config <alias> to confirm the extension loaded. The alias is derived from the bundle name; grep src/DependencyInjection/Configuration.php for the root node rather than guessing.bin/console debug:twig and bin/console debug:container --tag=twig.extension to discover the actual helper names and service IDs. This is your documentation — there is no docs site, no README walkthrough, and v0.1.0's release note says only "work in progress."First use case: the least risky one — a base layout template rendering <title>, <meta name="description">, and <link rel="canonical"> from values your controller already computes. That exercises the meta-tag and canonical services without depending on any structured-data API, which is the part most likely to churn.
Read in this order: the bundle class → DependencyInjection/Configuration.php (what is actually configurable) → the Twig extension (what is actually callable) → tests, if any exist.
Keep SEO construction in a service, not the template. The bundle gives you primitives; the durable pattern is a per-entity builder (PostSeoData, ProductSeoData) that takes a domain object and returns a plain value object of title/description/canonical/JSON-LD. Templates then render that object through the bundle's helpers. When the bundle's API shifts — and at 0.1.x it will — you change one adapter class, not forty templates.
Wrap it behind your own interface. Define SeoPresenterInterface in your app, implement it with the bundle's services, and inject your interface into controllers and Twig. This is the single highest-value integration decision for a pre-1.0 dependency: it converts a breaking change from a migration into a one-file edit.
Decorate, don't fork. The bundle advertises extensible services; use Symfony's service decoration (decorates:) to add fields, change canonical normalisation, or swap JSON-LD shapes. Never patch vendor/.
Canonical URLs belong in a request listener, not in every controller. Derive the canonical from the resolved route plus normalised query parameters in one place, then expose it as a request attribute the template reads. Per-controller canonical logic is how duplicate-content bugs get shipped — one action forgets to strip ?utm_source, and you have split ranking signals.
Structured data: emit one graph per page. JSON-LD is easy to double-emit (an Organization block in the layout plus an Article block in the content template produces two competing <script> tags). Have a single collector service that templates append to, and render it once at the end of <body>.
Integration tip: put an assertion in your test suite that renders the layout and asserts the presence of exactly one canonical tag and one JSON-LD script. Functional coverage in your own repo is the only coverage you can count on here.
|raw on a pre-encoded string, or Symfony's json_encode filter. Meta descriptions containing quotes or emoji need the same care. This is the most common silent failure — the page renders, the tags are malformed, and nothing in the logs tells you./en/ vs /), paginated pages (each page needs a self-referential canonical, plus rel=prev/next if you use them), and tracking parameters. Decide the policy once, in one service.debug:container <service-id> --show-private, debug:config, and debug:twig are your entire toolchain. If a helper returns empty, first confirm the extension is tagged twig.extension, then confirm the service is public or injected — not that the logic is wrong.updatedAt. Stale titles in <title> are invisible in the UI and expensive in search results.How can I help you explore Laravel packages today?