Symfony Open Graph Tags: Add OG Meta to Your Symfony App
How to add og:title, og:image, og:description, and Twitter card meta tags to Symfony applications using Twig templates, dynamic data from controllers, and sonata-project/seo-bundle.
OG tags in Twig base layout
The cleanest approach is to add OG meta tags to your Twig base layout with defaults, then override them per page using Twig blocks:
{# templates/base.html.twig #}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>{% block title %}My App{% endblock %}</title>
{# Define OG blocks with defaults — child templates override these #}
{% set og_title = block('og_title') is defined ? block('og_title') : block('title') %}
{% set og_description = block('og_description') is defined ? block('og_description') : 'Default site description.' %}
{% set og_image = block('og_image') is defined ? block('og_image') : absolute_url(asset('images/default-og.png')) %}
{% set og_url = block('og_url') is defined ? block('og_url') : app.request.uri %}
{% set og_type = block('og_type') is defined ? block('og_type') : 'website' %}
<!-- Open Graph -->
<meta property="og:title" content="{{ og_title }}" />
<meta property="og:description" content="{{ og_description }}" />
<meta property="og:image" content="{{ og_image }}" />
<meta property="og:url" content="{{ og_url }}" />
<meta property="og:type" content="{{ og_type }}" />
<meta property="og:site_name" content="My App" />
<!-- Twitter / X -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="{{ og_title }}" />
<meta name="twitter:description" content="{{ og_description }}" />
<meta name="twitter:image" content="{{ og_image }}" />
<link rel="canonical" href="{{ og_url }}" />
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>Override OG blocks in child templates
{# templates/blog/show.html.twig #}
{% extends 'base.html.twig' %}
{% block title %}{{ post.title }}{% endblock %}
{% block og_title %}{{ post.title }}{% endblock %}
{% block og_description %}{{ post.excerpt|slice(0, 160) }}{% endblock %}
{% block og_image %}{{ absolute_url(asset('images/og/' ~ post.slug ~ '.png')) }}{% endblock %}
{% block og_url %}{{ url('blog_show', { slug: post.slug }) }}{% endblock %}
{% block og_type %}article{% endblock %}
{% block body %}
<article>
<h1>{{ post.title }}</h1>
{{ post.body|raw }}
</article>
{% endblock %}Passing OG data from the controller
Alternatively, pass OG data directly from your Symfony controller as a render variable:
// src/Controller/BlogController.php
<?php
namespace App\Controller;
use App\Repository\PostRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class BlogController extends AbstractController
{
#[Route('/blog/{slug}', name: 'blog_show')]
public function show(string $slug, PostRepository $postRepository): Response
{
$post = $postRepository->findOneBySlug($slug);
if (!$post) {
throw $this->createNotFoundException('Post not found');
}
return $this->render('blog/show.html.twig', [
'post' => $post,
'og_title' => $post->getTitle(),
'og_description' => mb_substr(strip_tags($post->getExcerpt()), 0, 160),
'og_image' => 'https://myapp.com/og/' . $post->getSlug() . '.png',
'og_url' => $this->generateUrl('blog_show', ['slug' => $post->getSlug()], true),
'og_type' => 'article',
]);
}
}{# templates/blog/show.html.twig #}
{% extends 'base.html.twig' %}
{# Use variables passed from the controller #}
{% block og_title %}{{ og_title }}{% endblock %}
{% block og_description %}{{ og_description }}{% endblock %}
{% block og_image %}{{ og_image }}{% endblock %}
{% block og_url %}{{ og_url }}{% endblock %}
{% block og_type %}{{ og_type }}{% endblock %}Using sonata-project/seo-bundle
For larger Symfony apps, sonata-project/seo-bundle provides a service-based OG tag management system:
composer require sonata-project/seo-bundle
// In your controller or event subscriber
use Sonata\SeoBundle\Seo\SeoPageInterface;
class BlogController extends AbstractController
{
public function show(string $slug, PostRepository $repo, SeoPageInterface $seoPage): Response
{
$post = $repo->findOneBySlug($slug);
$seoPage
->setTitle($post->getTitle())
->addMeta('property', 'og:title', $post->getTitle())
->addMeta('property', 'og:description', substr($post->getExcerpt(), 0, 160))
->addMeta('property', 'og:image', 'https://myapp.com/og/' . $post->getSlug() . '.png')
->addMeta('property', 'og:url', $this->generateUrl('blog_show', ['slug' => $slug], true))
->addMeta('property', 'og:type', 'article')
->addMeta('name', 'twitter:card', 'summary_large_image');
return $this->render('blog/show.html.twig', ['post' => $post]);
}
}Common Symfony OG mistakes
- Relative image URLs — always use
absolute_url()in Twig or generate absolute URLs in the controller. Scrapers don't resolve relative paths. - Missing Twig block override — if you forget to override the OG block in a child template, the default title/image from the base layout renders for all pages.
- Wrong image size — use 1200×630px (1.91:1 ratio).
Verify your Symfony OG tags
After deploying, paste your URL into OGFixer to see exactly how your Symfony app's link previews appear on Twitter, LinkedIn, Slack, and Discord — and catch missing or broken tags before they go live.