PHPackages                             druidvav/page-metadata-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. druidvav/page-metadata-bundle

ActiveSymfony-bundle[Utility &amp; Helpers](/categories/utility)

druidvav/page-metadata-bundle
=============================

v2.8.3(1mo ago)06.0k↑133.3%MITPHPPHP &gt;=7.4.0

Since Jun 30Pushed 1mo ago2 watchersCompare

[ Source](https://github.com/druidvav/sf-page-metadata-bundle)[ Packagist](https://packagist.org/packages/druidvav/page-metadata-bundle)[ RSS](/packages/druidvav-page-metadata-bundle/feed)WikiDiscussions master Synced 1w ago

READMEChangelog (1)Dependencies (18)Versions (27)Used By (0)

Page Metadata Bundle
====================

[](#page-metadata-bundle)

Symfony bundle for managing page titles, meta tags, canonical URLs, Open Graph and Twitter metadata, breadcrumbs, and Schema.org JSON-LD from one request-scoped metadata object.

Requirements
------------

[](#requirements)

- PHP 7.4 or later
- Symfony 5, 6, or 7
- Twig 2.7 or later

Installation
------------

[](#installation)

```
composer require druidvav/page-metadata-bundle
```

When Symfony Flex is not available, register the bundle manually:

```
// config/bundles.php

return [
    // ...
    Druidvav\PageMetadataBundle\DvPageMetadataBundle::class => ['all' => true],
];
```

Render the metadata in the document head and breadcrumbs where they belong in the page body:

```

    {{ page_meta() }}

    {{ page_breadcrumbs() }}

```

`page_meta()` renders the title, standard meta tags, canonical link, Open Graph and Twitter tags, and structured data.

Configuration
-------------

[](#configuration)

`base_url` is required. All other sections are optional:

```
# config/packages/dv_page_metadata.yaml
dv_page_metadata:
    base_url: '%env(DEFAULT_URI)%'

    canonical:
        alternate_locales: [hy, ru, en]

    title:
        default: 'Example'
        delimiter: ' - '
        locale: null
        translation_domain: null

    meta:
        description: null
        keywords: null

    opengraph:
        site_name: 'Example'
        type: website
        image: '/default-cover.jpg'
        twitter_image: '/default-cover.jpg'
        twitter_site: '@example'
        twitter_card: summary

    breadcrumbs:
        listId: ''
        listClass: ''
        itemClass: ''
        linkRel: ''
        locale: null
        translation_domain: null
        viewTemplate: '@DvPageMetadata/breadcrumbs/bootstrap.html.twig'

    structured_data:
        enabled: true
        breadcrumbs: true
        nodes: { }
```

`base_url` is the canonical site root shared by metadata features that need to turn a path into an absolute URL. Values with and without a trailing slash are accepted; `PageMetadata::setBaseUrl()` strips trailing slashes before storing the value. A missing or empty value is rejected. Relative metadata paths such as `/cover.jpg` and structured-data identifiers such as `/#organization` are resolved against this URL; absolute URIs are left unchanged.

The `locale` and `translation_domain` configuration keys are retained by the bundle configuration. Set the active translation domain at runtime with `PageMetadata::setTransDomain()` when translated titles, descriptions, or breadcrumb labels are required.

Canonical URL, canonical language, Open Graph URL, and Twitter card type have no configuration defaults and are normally set per request through the PHP API. `canonical.alternate_locales` controls which locale variants are rendered as `rel="alternate"` links when canonical metadata is initialized from a request.

Using `PageMetadata`
--------------------

[](#using-pagemetadata)

Inject `PageMetadata` into a controller, event listener, or another service:

```
use Druidvav\PageMetadataBundle\PageMetadata;

final class ArticleController
{
    private PageMetadata $pageMetadata;

    public function __construct(PageMetadata $pageMetadata)
    {
        $this->pageMetadata = $pageMetadata;
    }

    public function __invoke(Article $article): Response
    {
        $this->pageMetadata
            ->setTitle($article->getTitle())
            ->setDescription($article->getDescription())
            ->setImage($article->getCoverUrl())
            ->setLinkCanonical($article->getCanonicalUrl())
            ->setOgUrl($article->getCanonicalUrl())
            ->setOgTwitterCard(PageMetadata::TWITTER_CARD_SUMMARY_LARGE_IMAGE);

        // Render the response.
    }
}
```

Setter methods return the same `PageMetadata` instance and can be chained, except for `setTransDomain()` and `setTitleDelimiter()`.

### Setter injection trait

[](#setter-injection-trait)

Autoconfigured Symfony services can use `PageMetadataAwareTrait` instead of constructor injection:

```
use Druidvav\PageMetadataBundle\PageMetadataAwareTrait;

final class ArticleController
{
    use PageMetadataAwareTrait;
}
```

Constructor injection is preferable when page metadata is a required dependency. The trait is useful for optional integration or existing classes where changing the constructor is inconvenient.

Titles
------

[](#titles)

`setTitle()` updates both the HTML page title and the Open Graph/Twitter title:

```
$pageMetadata->setTitle('Article title');
```

Titles can be composed using a configured delimiter:

```
use Druidvav\PageMetadataBundle\PageMetadata;

$pageMetadata
    ->setTitle('Example')
    ->addTitle('Air quality', [], PageMetadata::MODE_PREPEND);

// Air quality - Example
```

Available modes are:

- `PageMetadata::MODE_SET` — replace all title parts;
- `PageMetadata::MODE_PREPEND` — add a part before the existing title;
- `PageMetadata::MODE_APPEND` — add a part after the existing title.

Use `setPageTitle()` and `addPageTitle()` when only the HTML `` should change. Use `setOgTitle()` and `addOgTitle()` when only Open Graph and Twitter title tags should change.

Descriptions and keywords
-------------------------

[](#descriptions-and-keywords)

`setDescription()` updates both the standard meta description and the Open Graph/Twitter description:

```
$pageMetadata->setDescription('Current air quality and pollution measurements.');
```

They can also be controlled independently:

```
$pageMetadata->setMetaDescription('Search result description.');
$pageMetadata->setOgDescription('Social sharing description.');
```

`setMetaKeywords()` accepts a comma-separated string, trims individual values, and removes duplicates:

```
$pageMetadata->setMetaKeywords('air quality, Armenia, air quality');
// air quality, Armenia
```

Translation
-----------

[](#translation)

Set a translation domain before passing translation IDs:

```
$pageMetadata->setTransDomain('messages');

$pageMetadata->setTitle('article.page_title', [
    '%title%' => $article->getTitle(),
]);

$pageMetadata->addRouteItem('navigation.home', 'homepage');
```

The bundle treats lowercase identifiers containing letters, numbers, `_`, `-`, or `.` as translation IDs. Strings containing `%` are also passed through the translator. Other strings are used as provided.

An explicit translation domain can be passed to description setters:

```
$pageMetadata->setDescription('article.description', [], 'articles');
$pageMetadata->setMetaDescription('article.search_description', [], 'articles');
$pageMetadata->setOgDescription('article.share_description', [], 'articles');
```

Canonical metadata
------------------

[](#canonical-metadata)

```
$pageMetadata
    ->setLinkCanonical('https://example.com/en/articles/air-quality')
    ->setLinkCanonicalLang('en')
    ->setOgUrl('https://example.com/en/articles/air-quality');
```

The values are available in Twig through `page_link_canonical()`, `page_link_canonical_lang()`, and `page_og_url()`.

Canonical and alternate links can be generated from the same request. Route parameters are always retained; query parameters are excluded unless explicitly marked as canonical:

```
$pageMetadata
    ->setCanonicalFromRequest($request)
    ->addCanonicalParameter('page');
```

The request locale is used for the canonical link. Locales configured under `canonical.alternate_locales` are generated from the same route and canonical parameters.

When no explicit Open Graph URL is set, `og:url` uses the canonical URL. `setOgUrl()` remains available for pages whose Open Graph object intentionally has a different permanent URL.

Open Graph and Twitter
----------------------

[](#open-graph-and-twitter)

Convenience setters keep common values synchronized:

```
$pageMetadata->setTitle('Page title');       // HTML title and OG/Twitter title
$pageMetadata->setDescription('Summary');   // meta and OG/Twitter description
$pageMetadata->setImage($absoluteImageUrl); // OG and Twitter image
```

Individual fields can be changed separately:

```
$pageMetadata
    ->setOgType('article')
    ->setOgSiteName('Example')
    ->setOgTitle('Social title')
    ->setOgDescription('Social description')
    ->setOgImage('https://example.com/og-cover.jpg')
    ->setOgTwitterImage('https://example.com/twitter-cover.jpg')
    ->setOgTwitterSite('@example')
    ->setOgTwitterCard(PageMetadata::TWITTER_CARD_SUMMARY_LARGE_IMAGE);
```

Supported Twitter card constants are:

- `PageMetadata::TWITTER_CARD_SUMMARY`;
- `PageMetadata::TWITTER_CARD_SUMMARY_LARGE_IMAGE`.

Configure or set `og:site_name` when Open Graph output is used. The default metadata template renders `og:type` together with `og:site_name`.

Breadcrumbs
-----------

[](#breadcrumbs)

Add breadcrumb labels and routes in display order:

```
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

$pageMetadata
    ->addRouteItem('navigation.home', 'homepage')
    ->addRouteItem(
        'navigation.article',
        'article_show',
        ['slug' => $article->getSlug()],
        UrlGeneratorInterface::ABSOLUTE_PATH,
        ['%title%' => $article->getTitle()]
    );
```

Arguments of `addRouteItem()` are:

1. label or translation ID;
2. Symfony route name;
3. route parameters;
4. URL reference type, defaulting to `ABSOLUTE_PATH`;
5. translation parameters for the label.

Render the default breadcrumb namespace with:

```
{{ page_breadcrumbs() }}
```

Template options can be overridden for one rendering:

```
{{ page_breadcrumbs({
    listId: 'page-breadcrumbs',
    listClass: 'mb-4',
    itemClass: 'text-secondary',
    linkRel: 'nofollow'
}) }}
```

The default template contains Bootstrap 4/5-compatible markup. A Bootstrap 3 variant is available at `@DvPageMetadata/breadcrumbs/bootstrap3.html.twig`. A custom template receives the configured options and a `breadcrumbs` array containing `Breadcrumb` objects with `text` and `url` properties.

Breadcrumb namespaces are supported by `getNsBreadcrumbs()` and by the rendering function's `namespace` option. Adding items to a custom namespace requires extending `PageMetadata` and using its protected `addBreadcrumbToNs()` method. The public `addRouteItem()` method adds to the `default` namespace.

Structured data
---------------

[](#structured-data)

The bundle renders Schema.org data as one JSON-LD object containing a single `@context` and an `@graph` with all configured and dynamic nodes.

### Static nodes

[](#static-nodes)

Site-wide nodes can be configured once:

```
dv_page_metadata:
    base_url: '%env(DEFAULT_URI)%'

    structured_data:
        enabled: true
        breadcrumbs: true
        nodes:
            organization:
                '@type': Organization
                '@id': '/#organization'
                name: Example
                url: '/'
                logo: '/logo.jpg'

            website:
                '@type': WebSite
                '@id': '/#website'
                name: Example
                url: '/'
                publisher:
                    '@id': '/#organization'
                inLanguage: [en, fr]
```

The YAML keys `organization` and `website` are internal node names. They are not included in the generated JSON-LD.

### Dynamic nodes

[](#dynamic-nodes)

Add page-specific data through `PageMetadata`:

```
$pageMetadata->setStructuredData('article', [
    '@type' => 'BlogPosting',
    '@id' => $article->getCanonicalUrl() . '#article',
    'headline' => $article->getTitle(),
    'description' => $article->getDescription(),
    'datePublished' => $article->getPublishedAt(),
    'dateModified' => $article->getModifiedAt(),
    'publisher' => [
        '@id' => 'https://example.com/#organization',
    ],
]);
```

Calling `setStructuredData()` with the same internal name replaces the previous node without changing its graph position. This can be used to override a node loaded from configuration.

The node-level `@context` key is removed because the bundle owns the graph context. Other Schema.org properties remain generic, so the API can represent any current or future Schema.org type.

### Date and value normalization

[](#date-and-value-normalization)

Any `DateTimeInterface` value is converted recursively to `DATE_ATOM` ISO 8601 format:

```
$pageMetadata->setStructuredData('event', [
    '@type' => 'Event',
    'startDate' => new DateTimeImmutable('2026-08-01 10:00:00+04:00'),
    'subEvent' => [
        'startDate' => new DateTime('2026-08-01 12:00:00+04:00'),
    ],
]);
```

Structured values may contain strings, integers, floats, booleans, `null`, nested arrays, and `DateTimeInterface` instances. Strings starting with `/` or `#` are resolved against `base_url`; absolute URIs and other strings are preserved. Unsupported objects or resources cause an `InvalidArgumentException` whose message includes the property path.

### Removing and reading nodes

[](#removing-and-reading-nodes)

```
$pageMetadata->removeStructuredData('article');
$pageMetadata->clearStructuredData();

$namedNodes = $pageMetadata->getStructuredData();
$graph = $pageMetadata->getStructuredDataGraph();
$graphWithoutBreadcrumbs = $pageMetadata->getStructuredDataGraph(false);
```

`getStructuredData()` preserves internal names. `getStructuredDataGraph()` returns the sequential array used as `@graph` and optionally appends generated breadcrumb data.

### BreadcrumbList

[](#breadcrumblist)

When `structured_data.breadcrumbs` is enabled and the default namespace contains at least two breadcrumbs, the bundle appends a `BreadcrumbList` node automatically. Relative breadcrumb paths use the global `base_url`; the current request is not used to determine the host.

### Rendering and escaping

[](#rendering-and-escaping)

Structured data is included automatically by `page_meta()`:

```
{{ page_meta() }}
```

`page_structured_data()` is available for custom head templates that render individual metadata functions instead of calling `page_meta()`:

```
{{ page_title() }}
{{ page_structured_data() }}
```

Do not call both `page_meta()` and `page_structured_data()` in the same document, because `page_meta()` already includes the JSON-LD block.

JSON is encoded with Unicode and URLs left readable, while HTML-sensitive characters are escaped to prevent values such as `` from leaving the JSON-LD script element.

Autotext integration
--------------------

[](#autotext-integration)

The optional `meniam/autotext` package can generate title, description, and keyword variants:

```
composer require meniam/autotext
```

Available methods are:

```
$pageMetadata->setTitleAutotext($title, [], $stableId);
$pageMetadata->setMetaDescriptionAutotext($description, [], $stableId);
$pageMetadata->setMetaKeywordsAutotext($keywords, [], $stableId);
```

The optional stable ID controls deterministic variant selection according to the Autotext package behavior. When the optional package is not installed, calling any Autotext method throws a `LogicException` containing the installation command instead of causing a missing-class error.

Twig functions
--------------

[](#twig-functions)

FunctionResult`page_meta()`Complete metadata HTML, including structured data`page_breadcrumbs(options = {})`Rendered breadcrumb HTML`page_structured_data()`JSON-LD script for custom metadata templates`page_locale_url(request, locale)`Absolute request-route URL in another locale, preserving all query parameters`page_title()`Composed HTML page title`page_description(default = null)`Meta description or supplied default`page_keywords(default = null)`Meta keywords or supplied default`page_link_canonical()`Canonical URL`page_link_canonical_lang()`Canonical language value`page_link_canonical_alternates()`Locale-to-URL map generated from canonical request metadata`page_og_type()`Open Graph type`page_og_url()`Open Graph URL`page_og_site_name()`Open Graph site name`page_og_image()`Open Graph image`page_og_title()`Composed Open Graph/Twitter title`page_og_description()`Open Graph/Twitter description`page_twitter_image()`Twitter image`page_twitter_site()`Twitter account`page_twitter_card()`Twitter card typeCustom templates
----------------

[](#custom-templates)

The built-in templates are:

- `@DvPageMetadata/meta.html.twig` — complete metadata output;
- `@DvPageMetadata/structured_data.html.twig` — JSON-LD script wrapper;
- `@DvPageMetadata/breadcrumbs/bootstrap.html.twig` — Bootstrap 4/5 breadcrumbs;
- `@DvPageMetadata/breadcrumbs/bootstrap3.html.twig` — Bootstrap 3 breadcrumbs.

Configure `breadcrumbs.viewTemplate` to use an application template. Symfony's standard bundle template overriding mechanism can also be used to customize metadata output globally.

Testing
-------

[](#testing)

Install development dependencies and run:

```
composer install
composer test
```

###  Health Score

53

—

FairBetter than 96% of packages

Maintenance93

Actively maintained with recent releases

Popularity22

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity73

Established project with proven stability

 Bus Factor1

Top contributor holds 98% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~132 days

Recently: every ~0 days

Total

26

Last Release

34d ago

Major Versions

v1.3.0 → 3.4.x-dev2021-11-26

v1.4.1 → v2.02026-04-28

PHP version history (2 changes)v1.0PHP &gt;=5.6.0

v1.3.0PHP &gt;=7.4.0

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/1116122?v=4)[Anton Vlasov](/maintainers/druidvav)[@druidvav](https://github.com/druidvav)

---

Top Contributors

[![druidvav](https://avatars.githubusercontent.com/u/1116122?v=4)](https://github.com/druidvav "druidvav (48 commits)")[![meniam](https://avatars.githubusercontent.com/u/476450?v=4)](https://github.com/meniam "meniam (1 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisRector

### Embed Badge

![Health badge](/badges/druidvav-page-metadata-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/druidvav-page-metadata-bundle/health.svg)](https://phpackages.com/packages/druidvav-page-metadata-bundle)
```

###  Alternatives

[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k18.3M430](/packages/easycorp-easyadmin-bundle)[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.5k6.0M778](/packages/sylius-sylius)[pimcore/pimcore

Content &amp; Product Management Framework (CMS/PIM/E-Commerce)

3.8k3.9M535](/packages/pimcore-pimcore)[shopware/platform

The Shopware e-commerce core

3.4k1.5M3](/packages/shopware-platform)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

595.8M674](/packages/shopware-core)[sulu/sulu

Core framework that implements the functionality of the Sulu content management system

1.3k1.4M236](/packages/sulu-sulu)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
