Meta Tags

Render description, Open Graph, Twitter Card, JSON-LD, robots, and canonical tags for a page from its fields, its page model, or global defaults.

The meta() page method renders the tags a page needs in its <head>. It renders no <title> – that stays in your template:

site/snippets/head.php
<?php $meta = $page->meta() ?>
<title><?= $page->title()->escape() ?></title>
<?= $meta->robots() ?>
<?= $meta->social() ?>
<?= $meta->jsonld() ?>

social() reads a description field and a thumbnail files field:

site/blueprints/pages/default.yml
fields:
  description:
    type: textarea
  thumbnail:
    type: files
    multiple: false

With both filled in, social() renders:

<meta
  content="An exhibition of street photography from the 1970s."
  name="description"
/>
<meta content="Kirby Gallery" property="og:site_name" />
<meta content="https://example.com/exhibitions/street" property="og:url" />
<meta content="website" property="og:type" />
<meta content="Street" property="og:title" />
<meta
  content="An exhibition of street photography from the 1970s."
  property="og:description"
/>
<meta
  content="https://example.com/media/pages/exhibitions/street/2f6c1e7a90-1791524069/poster-1200x.jpg"
  property="og:image"
/>
<meta content="1200" property="og:image:width" />
<meta content="800" property="og:image:height" />
<meta content="Two people crossing a street" property="og:image:alt" />
<meta content="summary_large_image" name="twitter:card" />
<meta content="Street" name="twitter:title" />
<meta
  content="An exhibition of street photography from the 1970s."
  name="twitter:description"
/>
<meta
  content="https://example.com/media/pages/exhibitions/street/2f6c1e7a90-1791524069/poster-1200x.jpg"
  name="twitter:image"
/>
<meta content="Two people crossing a street" name="twitter:image:alt" />

Where Values Come From

A value such as description, thumbnail, robots, or canonical is looked up in this order, and the first one set wins:

The Page Model's metadata()

An array returned by a metadata() method on the page model.

The meta.defaults Option

See Global Defaults.

The Page's Field

A field of the same name, if it is not empty.

The Site's Field

A field of the same name in the site content, if it is not empty.

Page Model

site/models/article.php
<?php

use Kirby\Cms\Page;

class ArticlePage extends Page
{
    public function metadata(): array
    {
        return [
            'description' => $this->text()->excerpt(160)->value(),
            'thumbnail' => fn (Page $page) => $page->cover(),
            'opengraph' => [
                'type' => 'article'
            ],
            'jsonld' => [
                'BlogPosting' => [
                    'headline' => $this->title()->value(),
                    'datePublished' => $this->date()->toDate('c')
                ]
            ]
        ];
    }
}

A value can be a closure that receives the page. thumbnail takes a file ID, a UUID, or a files field returned from a closure, as in the example above – a files field set directly, or a File object, throws once social() runs.

Global Defaults

meta.defaults takes an array, or a closure that receives $kirby, $site, and $page:

site/config/config.php
return [
    'johannschopplich.helpers' => [
        'meta' => [
            'defaults' => fn ($kirby, $site, $page) => [
                'jsonld' => [
                    'WebSite' => [
                        'url' => $site->url(),
                        'name' => $site->title()->value()
                    ]
                ]
            ],
            'twitter' => [
                'site' => '@kirbytools',
                'creator' => '@johannschopplich'
            ]
        ]
    ]
];

meta.twitter.site and meta.twitter.creator render as twitter:site and twitter:creator.

A key in meta.defaults beats the page field of that name on every page – only metadata() sits above it. For a site-wide fallback, use a field of that name in the site content instead. Write keys in lowercase; a camelCase key is never found.

The opengraph, twitter, meta, and jsonld arrays come only from metadata() and meta.defaults, merged key by key: a page model adds entries to the defaults and replaces only the keys it sets.

Generated Tags

social()

Every tag below can be set in the meta, opengraph, or twitter array instead, and a value you set wins:

  • description: the description value, for the description meta tag and both networks.
  • og:site_name, og:url, og:type: the site title, the page URL, and website.
  • og:title, twitter:title: the page's customTitle field, else its title. This reads the page field directly, so set opengraph.title and twitter.title to change it from metadata().
  • og:image: the first file of the thumbnail value, resized to at most 1200 pixels wide, with its width, height, and alt field. An opengraph.image of your own replaces the thumbnail together with its width, height, and alt text – set those yourself.
  • twitter:image, twitter:image:alt: the Open Graph image and its alt text.
  • twitter:card: summary_large_image, or summary when there is no image – also when you set summary_large_image yourself.

Further tags go into the meta array, one name per key:

site/config/config.php
return [
    'johannschopplich.helpers' => [
        'meta' => [
            'defaults' => [
                'meta' => [
                    'author' => 'Kirby Gallery'
                ]
            ]
        ]
    ]
];

robots()

A robots meta tag when the robots value is set, and a canonical link to the canonical value or the page URL.

jsonld()

One <script type="application/ld+json"> per entry of the jsonld array. The key becomes @type, and @context is https://schema.org; both can be overridden in the entry.

opensearch()

A <link rel="search"> to /open-search.xml, titled with the site title. The plugin does not serve that file – add it, or a route for it, yourself.

Nested and Namespaced Properties

An array value in opengraph or twitter expands into one tag per key: image with a width key becomes og:image:width, or twitter:image:width. 'image' => ['width' => 1200] and 'image:width' => 1200 address the same tag; where both are set, the flat key wins.

A namespace: prefix replaces the og: part, which is how article:* and product:* tags are written. It applies to opengraph only. In a page model's metadata():

'opengraph' => [
    'type' => 'article',
    'namespace:article' => [
        'published_time' => $this->date()->toDate('c'),
        'author' => 'Johann Schopplich'
    ]
]
<meta content="article" property="og:type" />
<meta content="2026-10-09T00:00:00+00:00" property="article:published_time" />
<meta content="Johann Schopplich" property="article:author" />

Read Values

$page->meta()->get('description') returns any value as a field, looked up in the order above; false as the second argument skips the site field. Any other method name reads the value of that name, so $page->meta()->description() is the same call. priority() returns the sitemap priority as a float.