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.