Meta Tags
The meta() page method renders the tags a page needs in its <head>. It renders no <title> – that stays in your template:
<?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:
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
<?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:
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: thedescriptionvalue, for thedescriptionmeta tag and both networks.og:site_name,og:url,og:type: the site title, the page URL, andwebsite.og:title,twitter:title: the page'scustomTitlefield, else its title. This reads the page field directly, so setopengraph.titleandtwitter.titleto change it frommetadata().og:image: the first file of thethumbnailvalue, resized to at most 1200 pixels wide, with its width, height, andaltfield. Anopengraph.imageof 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, orsummarywhen there is no image – also when you setsummary_large_imageyourself.
Further tags go into the meta array, one name per key:
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.