daddl3 / seo-tool-bundle
All in One Symfony Seo Bundle
Package info
gitlab.com/daddl3/SeoToolBundle
Type:symfony-bundle
pkg:composer/daddl3/seo-tool-bundle
Requires
- php: ^8.5
- ext-dom: *
- doctrine/persistence: ^4.0
- symfony/cache: ^8.0
- symfony/config: ^8.0
- symfony/console: ^8.0
- symfony/dependency-injection: ^8.0
- symfony/event-dispatcher: ^8.0
- symfony/http-kernel: ^6.0 | ^7.0 || ^8.0
- symfony/messenger: ^8.0
- symfony/routing: ^8.0
- twig/twig: ^3.0
Requires (Dev)
- phpunit/phpunit: ^12.1
- symfony/framework-bundle: ^8.0
- symfony/profiler-pack: ^1.0
- symfony/serializer: ^8.0
- symfony/test-pack: ^1.0
- symfony/yaml: ^8.0
This package is auto-updated.
Last update: 2026-04-01 00:14:04 UTC
README
All-in-One Symfony SEO Bundle — Meta Tags, Open Graph, Schema.org, Breadcrumbs, Google Tag Manager, Meta Pixel, and full Profiler support.
Requirements: PHP 8.5+, Symfony 8.x
Installation
composer require daddl3/seo-tool-bundle
Symfony Flex registers the bundle automatically. If not:
// config/bundles.php
return [
Daddl3\SeoToolBundle\SeoBundle::class => ['all' => true],
];
Configuration
All options in config/packages/seo.yaml:
seo:
# Schema.org — inject JSON-LD into <head>
schema:
enabled: true
cache_ttl: 604800 # Cache in seconds (default: 1 week)
# Meta Tags — global default values
meta_tags:
title: 'My Website'
description: 'Default description'
keywords: ['symfony', 'seo']
# Open Graph — global default values
open_graph:
title: 'My Website'
description: 'Default description'
sitename: 'My Website'
url: 'https://example.com'
type: 'website'
# Google Tag Manager
google_tag_manager:
enabled: true
tag_manager_id: 'GTM-XXXXXX'
# Meta Pixel (Facebook)
meta_pixel:
enabled: true
pixel_id: '1234567890'
Features
Meta Tags
In the Controller
use Daddl3\SeoToolBundle\Metas\MetaTagsManagerInterface;
class ProductController extends AbstractController
{
public function show(MetaTagsManagerInterface $meta): Response
{
$meta
->setTitle('Product Name')
->setDescription('Short product description')
->setKeywords(['product', 'shop', 'symfony'])
->setCanonical('https://example.com/product')
->setRobots(['index', 'follow'])
->setAuthor('John Doe')
->setViewPort('width=device-width, initial-scale=1')
->setCharacterEncoding('UTF-8')
->setCopyright('2025 Example Inc.')
->setContentType('text/html; charset=UTF-8')
->setContentSecurityPolicy("default-src 'self'")
->setSubject('E-Commerce')
->setXUACompatible()
->setCustomMetaTag('theme-color', '#ffffff')
;
return $this->render('product/show.html.twig');
}
}
In a Twig Template
{{ meta_tags(
title: 'Product Name',
description: 'Short description',
keywords: ['product', 'shop'],
canonical: 'https://example.com/product',
robots: ['index', 'follow']
) }}
Values from the controller and the template are merged. Template values override controller values.
Include in the Base Template
<head>
{{ meta_tags() }}
</head>
Output
<meta charset="UTF-8" />
<title>Product Name</title>
<meta name="description" content="Short product description" />
<meta name="keywords" content="product, shop, symfony" />
<meta name="robots" content="index, follow" />
<link rel="canonical" href="https://example.com/product" />
<meta name="author" content="John Doe" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
All Available Methods
| Method | Description |
|---|---|
setTitle(string) | Page title (<title>) |
setDescription(string) | Meta description |
setKeywords(array) | Meta keywords |
setCanonical(string) | Canonical URL |
setRobots(array) | Robots directives (index, follow, noindex, …) |
setAuthor(string) | Author |
setViewPort(string) | Viewport |
setCharacterEncoding(string) | Character encoding (default: UTF-8) |
setCopyright(string) | Copyright notice |
setSubject(string) | Page subject |
setContentType(string) | Content type |
setContentSecurityPolicy(string) | CSP header |
setAlternate(string, string) | Alternate link (href, media) |
setXUACompatible() | IE compatibility mode (IE=edge) |
setDefaultStyle(string) | Default stylesheet |
setCustomMetaTag(string, string) | Custom meta tag (name, content) |
Open Graph
In the Controller
use Daddl3\SeoToolBundle\OpenGraph\OpenGraphManagerInterface;
class ArticleController extends AbstractController
{
public function show(OpenGraphManagerInterface $og): Response
{
$og
->setTitle('Article Title')
->setDescription('Description for social media')
->setImage('https://example.com/image.jpg')
->setImageAltText('Image description')
->setUrl('https://example.com/article')
->setType('article')
->setLocale('en_US')
->setAlternateLocale('de_DE')
->setSiteName('My Website')
;
return $this->render('article/show.html.twig');
}
}
In a Twig Template
{{ open_graph(
title: 'Article Title',
description: 'Description for social media',
imageUrl: 'https://example.com/image.jpg',
url: 'https://example.com/article',
type: 'article'
) }}
Include in the Base Template
<head>
{{ open_graph() }}
</head>
Output
<meta property="og:title" content="Article Title" />
<meta property="og:description" content="Description for social media" />
<meta property="og:image" content="https://example.com/image.jpg" />
<meta property="og:url" content="https://example.com/article" />
<meta property="og:type" content="article" />
<meta property="og:site_name" content="My Website" />
OG Image
Extended image metadata with og:image:* tags.
Controller:
use Daddl3\SeoToolBundle\OpenGraph\OGImageManagerInterface;
public function show(OGImageManagerInterface $ogImage): Response
{
$ogImage
->setUrl('https://example.com/og.jpg')
->setSecureUrl('https://example.com/og.jpg')
->setType('image/jpeg')
->setWidth('1200')
->setHeight('630')
->setAlt('Image description')
;
}
Twig:
{{ og_image(
url: 'https://example.com/og.jpg',
type: 'image/jpeg',
width: '1200',
height: '630',
alt: 'Image description'
) }}
Output:
<meta property="og:image" content="https://example.com/og.jpg" />
<meta property="og:image:secure_url" content="https://example.com/og.jpg" />
<meta property="og:image:type" content="image/jpeg" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="Image description" />
OG Video
Controller:
use Daddl3\SeoToolBundle\OpenGraph\OGVideoManagerInterface;
public function show(OGVideoManagerInterface $ogVideo): Response
{
$ogVideo
->setUrl('https://example.com/video.mp4')
->setSecureUrl('https://example.com/video.mp4')
->setType('video/mp4')
->setWidth('1280')
->setHeight('720')
;
}
Twig:
{{ og_video(url: 'https://example.com/video.mp4', type: 'video/mp4', width: '1280', height: '720') }}
Output:
<meta property="og:video" content="https://example.com/video.mp4" />
<meta property="og:video:secure_url" content="https://example.com/video.mp4" />
<meta property="og:video:type" content="video/mp4" />
<meta property="og:video:width" content="1280" />
<meta property="og:video:height" content="720" />
OG Audio
Controller:
use Daddl3\SeoToolBundle\OpenGraph\OGAudioManagerInterface;
public function show(OGAudioManagerInterface $ogAudio): Response
{
$ogAudio
->setUrl('https://example.com/audio.mp3')
->setSecureUrl('https://example.com/audio.mp3')
->setType('audio/mpeg')
;
}
Twig:
{{ og_audio(url: 'https://example.com/audio.mp3', type: 'audio/mpeg') }}
Output:
<meta property="og:audio" content="https://example.com/audio.mp3" />
<meta property="og:audio:secure_url" content="https://example.com/audio.mp3" />
<meta property="og:audio:type" content="audio/mpeg" />
OG Article
Controller:
use Daddl3\SeoToolBundle\OpenGraph\OGArticleManagerInterface;
public function show(OGArticleManagerInterface $ogArticle): Response
{
$ogArticle
->setPublishedTime(new \DateTime('2025-01-15'))
->setModifiedTime(new \DateTime('2025-06-01'))
->setAuthor('John Doe')
->setSection('Technology')
->setTags(['symfony', 'php', 'seo'])
;
}
Twig:
{{ og_article(
author: 'John Doe',
section: 'Technology',
tags: ['symfony', 'php', 'seo']
) }}
Output:
<meta property="article:published_time" content="..." />
<meta property="article:modified_time" content="..." />
<meta property="article:author" content="John Doe" />
<meta property="article:section" content="Technology" />
<meta property="article:tag" content="symfony" />
<meta property="article:tag" content="php" />
<meta property="article:tag" content="seo" />
Twitter Cards
Twitter Cards are added via the OpenGraphManager:
$og->addTwitterCardProperty('card', 'summary_large_image');
$og->addTwitterCardProperty('site', '@myWebsite');
$og->addTwitterCardProperty('creator', '@author');
$og->addTwitterCardProperty('title', 'Article Title');
$og->addTwitterCardProperty('description', 'Description for Twitter');
Output:
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:site" content="@myWebsite" />
<meta property="twitter:creator" content="@author" />
Structured Properties
For og:video:*, og:audio:*, and og:image:* inline in the OG block:
$og->addStructuredProperty('video', 'secure_url', 'https://example.com/video.mp4');
$og->addStructuredProperty('image', 'width', '1200');
Output:
<meta property="og:video:secure_url" content="https://example.com/video.mp4" />
<meta property="og:image:width" content="1200" />
For music:
$og->addMusicProperty('musician', 'https://example.com/artist');
Schema.org (Rich Results)
First enable it in the configuration:
seo:
schema:
enabled: true
cache_ttl: 604800
The schema is automatically injected as <script type="application/ld+json"> into the <head>.
In the Controller
use Daddl3\SeoToolBundle\Schema\SchemaInterface;
class ArticleController extends AbstractController
{
public function show(SchemaInterface $schema): Response
{
$article = $schema->article()
->name('My Article')
->setDescription('Article description')
->url('https://example.com/article')
;
$schema->render($article);
return $this->render('article/show.html.twig');
}
}
Output
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"name": "My Article",
"description": "Article description",
"url": "https://example.com/article"
}
</script>
Nested Objects
$person = $schema->person()
->name('John Doe')
->url('https://example.com/john')
;
$org = $schema->organization()
->name('Example Inc.')
->url('https://example.com')
;
$schema->render(
$schema->article()
->name('My Article')
->setDescription('...')
);
Supported Schema Types
| Category | Types |
|---|---|
| Thing | thing(), person(), organization(), place(), event(), action() |
| CreativeWork | creativeWork(), article(), blogPosting(), socialMediaPosting(), blog(), course(), thesis(), website(), mediaObject(), imageObject(), textObject() |
| WebPage | webPage(), aboutPage(), contactPage(), faqPage(), itemPage(), checkoutPage(), profilePage(), searchResultPage(), collectionPage(), mediaGallery(), imageGallery(), webPageElement() |
| Organization | organization(), localBusiness(), library(), radioStation(), travelAgency() |
| Place | place(), administrativeArea(), country() |
| Intangible | jobPosting(), occupation(), breadcrumbList(), listItem(), itemList(), definedTerm(), specialty(), speakableSpecification(), monetaryAmount(), propertyValue(), offer() |
| Other | service(), audience(), postalAddress(), educationOccupationalCredential() |
Breadcrumb
In the Controller
use Daddl3\SeoToolBundle\Breadcrumb\BreadcrumbManagerInterface;
class BlogController extends AbstractController
{
public function show(BreadcrumbManagerInterface $breadcrumb): Response
{
$breadcrumb
->addItem('Home', 'https://example.com')
->addItem('Blog', 'https://example.com/blog')
->addItem('My Article') // last item without URL = active
;
return $this->render('blog/show.html.twig');
}
}
Render in the Template
{{ seo_breadcrumb() }}
Output (Bootstrap)
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="https://example.com">Home</a></li>
<li class="breadcrumb-item"><a href="https://example.com/blog">Blog</a></li>
<li class="breadcrumb-item active">My Article</li>
</ol>
Customize CSS Classes
$breadcrumb->setOptions([
'list_class' => 'breadcrumb',
'list_item_class' => 'breadcrumb-item',
'active_item' => 'active',
]);
Google Tag Manager
Configuration (automatically on all pages)
seo:
google_tag_manager:
enabled: true
tag_manager_id: 'GTM-XXXXXX'
Automatically injects the GTM code into <head> and <body>.
Programmatically per Request
use Daddl3\SeoToolBundle\Seo\GoogleTagManager\TagManagerInterface;
public function index(TagManagerInterface $tagManager): Response
{
$tagManager->enableGoogleTagManager('GTM-XXXXXX');
return $this->render('index.html.twig');
}
Meta Pixel (Facebook)
Configuration (automatically on all pages)
seo:
meta_pixel:
enabled: true
pixel_id: '1234567890'
Programmatically per Request
use Daddl3\SeoToolBundle\Seo\MetaPixel\MetaPixelInterface;
public function index(MetaPixelInterface $metaPixel): Response
{
$metaPixel->enableMetaPixel('1234567890');
return $this->render('index.html.twig');
}
Symfony Profiler
A full SEO profiler is available in dev mode.
Toolbar shows the status of all features at a glance:
| Feature | Green | Yellow | Red |
|---|---|---|---|
| Meta Tags | set | — | not set |
| Open Graph | set | — | not set |
| Twitter Cards | set | not set | — |
| OG Image / Video / Audio / Article | set | not set | — |
| Schema.org | type name | not set | — |
| Breadcrumb | number of items | 0 items | — |
| Google GTM | GTM ID | not configured | — |
| Meta Pixel | Pixel ID | not configured | — |
Profiler panel shows for each page:
- Overview of all features with status
- All set meta tags in detail
- Open Graph tags (base, Twitter Cards, Structured Properties, Music)
- Separate panels for OG Image, Video, Audio, Article Manager
- Schema.org JSON-LD preview
- Breadcrumb items as a table
- Google Tag Manager ID
- Meta Pixel ID