Installation
Add the bundle to your composer.json:
composer require symfony-cmf/seo-bundle
Enable it in config/bundles.php:
return [
// ...
SymfonyCmf\Bundle\SeoBundle\SymfonyCmfSeoBundle::class => ['all' => true],
];
Configuration Publish the default config:
php artisan vendor:publish --tag=seo-bundle-config
Update config/seo.php with your preferred settings (e.g., canonical URLs, meta tags).
First Use Case
Generate SEO metadata for a route (e.g., App\PageController@show):
use SymfonyCmf\Bundle\SeoBundle\Seo\SeoMetadataFactoryInterface;
class PageController extends Controller
{
public function show(SeoMetadataFactoryInterface $seoFactory, Page $page)
{
$metadata = $seoFactory->createMetadata($page);
return $this->render('page/show.html.twig', [
'metadata' => $metadata,
]);
}
}
Twig Integration In your template, render metadata:
{{ metadata.title }}
{{ metadata.description }}
{{ metadata.canonicalUrl }}
Dynamic Metadata Generation Extend the default metadata factory to customize behavior:
use SymfonyCmf\Bundle\SeoBundle\Seo\Metadata\MetadataFactory;
use SymfonyCmf\Bundle\SeoBundle\Seo\Metadata\MetadataFactoryInterface;
class CustomMetadataFactory extends MetadataFactory implements MetadataFactoryInterface
{
public function createMetadata($content, $routeParameters = [])
{
$metadata = parent::createMetadata($content, $routeParameters);
$metadata->setTitle($metadata->getTitle() . ' | Custom Suffix');
return $metadata;
}
}
Register the service in config/services.yaml:
SymfonyCmf\Bundle\SeoBundle\Seo\SeoMetadataFactory:
class: App\Seo\CustomMetadataFactory
Route-Based SEO
Use the SeoMetadataFactory in route controllers or event subscribers:
use Symfony\Component\HttpKernel\Event\ResponseEvent;
class SeoSubscriber implements EventSubscriberInterface
{
public function onKernelResponse(ResponseEvent $event)
{
$metadata = $this->seoFactory->createMetadata($event->getController()[1]);
$event->getResponse()->headers->set('X-Seo-Title', $metadata->getTitle());
}
}
Content-Type-Specific Rules
Define metadata rules per content type (e.g., Page, BlogPost) using YAML:
# config/seo/rules.yml
App\Entity\BlogPost:
title: "%post.title% - %site.name%"
description: "%post.excerpt%"
robots: "index,follow"
Canonical URLs Ensure canonical URLs are set dynamically:
$metadata->setCanonicalUrl($this->generateUrl('canonical_route', ['id' => $page->getId()]));
OpenGraph/Social Media Tags Extend metadata for social sharing:
$metadata->setProperty('og:type', 'article');
$metadata->setProperty('og:image', $page->getFeaturedImageUrl());
Symfony Event Dispatcher
Listen to kernel.response to inject SEO metadata globally:
$event->getResponse()->setContent(
$this->renderView('seo/metadata.html.twig', ['metadata' => $metadata])
);
Doctrine Lifecycle Callbacks Update SEO metadata when entities change:
use Doctrine\ORM\Event\LifecycleEventArgs;
class Page
{
public function preUpdate(LifecycleEventArgs $args)
{
$em = $args->getEntityManager();
$em->getEventManager()->dispatchEvent(
'seo.update',
new SeoUpdateEvent($this)
);
}
}
API Responses Serialize metadata for API consumers:
return $this->json([
'data' => $page,
'seo' => [
'title' => $metadata->getTitle(),
'canonical_url' => $metadata->getCanonicalUrl(),
],
]);
Caching Cache metadata for performance:
$cacheKey = 'seo:metadata:' . $page->getId();
$metadata = $this->cache->get($cacheKey);
if (!$metadata) {
$metadata = $this->seoFactory->createMetadata($page);
$this->cache->set($cacheKey, $metadata, 3600);
}
Deprecated Bundle
FOS\SeoBundle (active maintenance).Knp\Bundle\SeoBundle (modern, Symfony 5+).Configuration Overrides
config/seo/rules.yml may conflict with your templates. Explicitly override in your config:
seo:
rules:
App\Entity\Page:
title: "%page.title%" # Override default
Route Parameter Mismatches
$routeParameters passed to createMetadata() match the route definition. Example:
// Correct: Parameters must align with route {id}
$metadata = $seoFactory->createMetadata($page, ['id' => $page->getId()]);
Twig Auto-escaping
og:image) may break if Twig escapes URLs. Use |raw filter:
<meta property="og:image" content="{{ metadata.properties.og_image|raw }}" />
Canonical URL Conflicts
if ($metadata->getCanonicalUrl() !== $request->getUri()) {
$metadata->setCanonicalUrl($request->getUri());
}
Dump Metadata Inspect generated metadata in a controller:
dump($metadata->getTitle());
dump($metadata->getProperties());
Check Rules Loading Verify YAML rules are loaded:
$this->container->get('seo.rules')->getRules();
Event Debugging Listen for SEO events to trace execution:
$dispatcher->addListener('seo.metadata', function ($event) {
error_log('SEO Event: ' . print_r($event->getMetadata(), true));
});
HTTP Headers
Validate metadata in browser dev tools or curl:
curl -I http://your-site.com/page
Custom Metadata Factories
Implement MetadataFactoryInterface for content-specific logic:
class BlogPostMetadataFactory implements MetadataFactoryInterface
{
public function createMetadata($content, array $routeParameters = [])
{
$metadata = new Metadata();
$metadata->setTitle(sprintf('Post: %s', $content->getTitle()));
return $metadata;
}
}
Metadata Normalizers
Extend MetadataNormalizerInterface to transform metadata for APIs:
class ApiMetadataNormalizer implements MetadataNormalizerInterface
{
public function normalize(Metadata $metadata)
{
return [
'title' => $metadata->getTitle(),
'canonical_url' => $metadata->getCanonicalUrl(),
'properties' => $metadata->getProperties(),
];
}
}
Dynamic Rule Providers Fetch SEO rules from a database or external service:
class DatabaseRuleProvider implements RuleProviderInterface
{
public function getRules()
{
return $this->entityManager->getRepository(Rule::class)->findAll();
}
}
Event Subscribers Extend SEO behavior via events:
// src/EventListener/SeoListener.php
class SeoListener implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
'seo.metadata' => 'onSeoMetadata',
];
}
public function onSeoMetadata(
How can I help you explore Laravel packages today?