gurtok / seo-bundle
Flexible and modern SEO management bundle for Symfony applications
Installs: 12
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 1
Forks: 0
Open Issues: 0
Type:symfony-bundle
Requires
- php: >=8.1
- symfony/framework-bundle: ^6.0|^7.0
- symfony/twig-bundle: ^6.0|^7.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.75
- phpstan/phpstan: ^2.1
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-symfony: ^2.0
- phpunit/phpunit: ^10
- roave/backward-compatibility-check: 8.*
- roave/security-advisories: dev-latest
- symfony/browser-kit: ^6.4
- symfony/css-selector: ^6.4
- symfony/http-client: ^6.4
- symfony/translation: ^6.4
- symfony/yaml: ^6.4
README
Modern and flexible SEO Bundle for Symfony 6/7 projects. Easily manage meta tags, OpenGraph, Twitter cards, canonical URLs, hreflangs, and verification tags.
🧭 Roadmap (Post-2.0)
🟥 Priority 1: Full SEO "out-of-the-box"
- 🧩 Support
JsonLd
+JsonLdMulti
rendering helpers - 🧠 Add schema.org presets (Article, Product, Event, etc.)
- 🌐 Integrate with sitemap generator (e.g., via Symfony event or SitemapProviderInterface)
- 🧷 Provide Google Tag Manager (GTM) support
- 🧲 Provide Meta Pixel support (Facebook)
🟧 Priority 2: Enhanced social sharing support
- 🖼️
og:image
object support:url
,width
,height
,alt
,type
- 🎥
og:video
support with full metadata - 🧵 Multiple
og:image
/og:video
entries - 🎯
og:type
configuration (article, product, etc.) - 📌 Pinterest meta tags (
pinterest:description
,pinterest:image
) - 💼 LinkedIn-specific sharing optimization
🟨 Priority 3: Developer Experience & Flexibility
- 🧰 Symfony UX Component for real-time preview of tags in dev mode
- 🧪 PHPUnit constraint:
assertSeoRenderedContains($tag)
- 🧬 Full extensibility via custom
SeoRenderStrategyInterface
- 📦 Export configuration to JSON/LD, XML for third-party SEO analysis
- 📖 Recipe / packagist recipe for easy install with Flex
✅ Done
- Rewrite listeners for flexibility and testability
- Add support for localized defaults (en/uk/…) and translatable attributes
- Add
SeoMetaRenderOptions
(include_title, skip_empty, etc.) - Add
SeoTagHtmlBuilder
for rendering - Add new SeoMeta flags (noIndex, disableDefaults, etc.)
- Add canonical generator with query filtering
- Add granular disabling via config/environment
📦 Installation
Require the bundle via Composer:
composer require gurtok/seo-bundle
If you're using Symfony Flex, the bundle will be registered automatically.
Otherwise, manually add to config/bundles.php
:
return [ Gurtok\SeoBundle\GurtokSeoBundle::class => ['all' => true], ];
⚙️ Configuration
Create a config file config/packages/gurtok_seo.yaml
:
gurtok_seo: allow_custom_meta: false # Allow setting custom meta tags (true/false) auto_inject_response: true # Auto-insert SEO meta into <head> if not manually called excluded_paths: - '/admin' - '/api' canonical_excluded_query_keys: - 'utm_source' - 'utm_medium' - 'utm_campaign' - 'utm_term' - 'utm_content' - 'ref' - 'fbclid' - 'page' defaults: title: { en: 'Default Title', uk: 'Типовий заголовок' } # Default title for the site title_separator: ' | ' description: { en: 'Default description', uk: 'Типовий опис' } auto_canonical: true # Automatically generate canonical URL meta: robots: 'index, follow' og: type: 'website' twitter: card: 'summary_large_image' verifications: google-site-verification: 'abc123' no_index: false # Set to true if the site should not be indexed by search engines is_adult_content: false # Set to true if the content is adult-oriented and will be marked as such
Note:
The default locale is automatically taken from %kernel.default_locale%
(usually configured in framework.yaml
).
Example framework.yaml
:
framework: default_locale: 'en'
🚀 Usage
Using the Attribute on Controllers
You can define SEO metadata directly via PHP 8+ attributes:
use Gurtok\SeoBundle\Attribute\SeoMeta; #[SeoMeta( title: 'Homepage', titlePrefix: 'MySite', titleSeparator: ' ~ ', description: 'Welcome to our amazing website!', canonical: 'https://example.com', meta: ['robots' => 'index, follow'], og: [ 'title' => 'Homepage OG', 'image' => 'https://example.com/image.jpg', ], twitter: ['card' => 'summary_large_image'], verifications: ['google-site-verification' => 'your-verification-code'], hreflangs: [ 'en' => 'https://example.com', 'uk' => 'https://example.com/uk' ], noIndex: false, isAdultContent: false, disableDefaults: false )] public function __invoke() { // ... }
You can place the attribute either on the controller method or on the class — method attribute has priority.
Localized Titles and Descriptions
You can specify translations via arrays:
#[SeoMeta( title: [ 'en' => 'Homepage', 'uk' => 'Головна сторінка' ], description: [ 'en' => 'Welcome!', 'uk' => 'Ласкаво просимо!' ] )]
The bundle automatically detects the current request locale (RequestStack
) and uses the localized value.
Fallback is the default locale from %kernel.default_locale%
, or the first available value.
🎨 Twig integration
In your Twig layout (base.html.twig
):
<head> {{ seo() }} </head>
This will render:
<title>
tag<meta>
description<meta>
robots- OpenGraph tags
- Twitter card tags
- Canonical link
- Hreflang alternate links
- Verification meta tags
🔍 Twig Functions
Twig Function | Description |
---|---|
seo() |
Renders everything (title + meta + OG + Twitter + etc) |
seo_title() |
Renders only <title> tag |
seo_meta() |
Renders only <meta> tags |
seo_og() |
Renders OpenGraph tags |
seo_open_graph() |
Alias for seo_og() |
seo_twitter() |
Renders Twitter card tags |
seo_hreflangs() |
Renders hreflang links |
seo_verification() |
Renders verification tags |
⚙️ Twig Function Options
Functions seo()
and seo_meta()
accept an optional options array:
{{ seo({ include_title: true, skip_empty: true }) }}
include_title
(bool, defaulttrue
) — whether to include the<title>
tag.skip_empty
(bool, defaulttrue
) — skip rendering empty meta fields.
🔥 Auto Injection (Optional)
If you forget to call {{ seo() }}
manually in Twig,
and auto_inject_response
is enabled (default true
),
SeoBundle will automatically inject SEO meta before </head>
during the HTTP Response phase.
📘 Full Example
# config/packages/gurtok_seo.yaml gurtok_seo: allow_custom_meta: true auto_inject_response: true excluded_paths: - '/admin' - '/api'
Controller:
use Gurtok\SeoBundle\Attribute\SeoMeta; #[SeoMeta( title: 'Blog', titleSeparator: ' * ', description: 'Latest articles and news.', og: ['title' => 'Blog OG', 'type' => 'website'], twitter: ['card' => 'summary'], )] class BlogController { public function __invoke(SeoManager $seoManager) { // get post data, from database or API $seoManager->setTitlePrfix('Post Name'); // ... } }
Twig:
<head> {{ seo() }} </head>
Result in HTML:
<title>Post * Blog</title> <meta name="description" content="Latest articles and news."> <meta property="og:title" content="Blog OG"> <meta property="og:type" content="website"> <meta name="twitter:card" content="summary">
📄 License
SeoBundle is open-sourced software licensed under the MIT license.