Guide 05 · Developer docs

Content types and custom entities

Which built-in type fits which purpose, how categories, tags, slugs, custom URLs and SEO settings behave across them, how revisions and publishing gate what the public sees, how to model a domain the platform has no type for, and where uploaded files live.

L1–L3
Unit 02 · Taxonomy, URLs and SEO

SEO settings and Open Graph

Every content record carries SEO settings: a title, a description, a canonical URL, a robots value and a share image. The platform prints none of them; the site's templates do. The fields, the robots mapping that is easy to get backwards, what the sitemap does with them, and Open Graph data from custom fields, in both engines.

Who this is for. The theme developer writing the head of the base template, and the content modeller deciding what authors fill in for search engines and social cards. Afterwards you can print every record's SEO settings from one include, with Open Graph data the authors control.

The fields

Admin labelGetterNotes
SEO TitlegetTitle()Empty unless the author set one; fall back to the record's name
Meta DescriptiongetDescription()
Meta KeywordsgetKeywords()Comma-separated
Canonical URLgetCanonicalUrl()Free text: absolute or a path
Alternate URLsgetAlternateUrls()One URL per line
Robots settingsgetRobots()A stored code, mapped below
Share imagegetShareImage()An image, or nothing

Pages, books, units and book pages, blogs, blog posts, FAQ pages and questions, and product and category descriptions have them, through getSeoSettings(). A custom entity record keeps them on its revision, when its definition's SEO Settings switch is on: getActiveRevision().getSeoSettings(). Any of these can return nothing for a record whose SEO panel was never filled in, so test before reading through it.

The framework prints none of this: no title tag, no meta tags and no canonical link. The site's base template does, which is why it belongs in one include that every page type calls.

The robots value needs a mapping

The robots setting is stored as one of four codes, and the codes are not meta-robots values:

Admin labelStoredPrint
Default (recommended)emptynothing
Index & followallindex, follow
Index, but don't followindexindex, nofollow
Don't index, but followfollownoindex, follow
Don't index and don't follownonenoindex, nofollow

Printed as it is, follow gives no instruction about indexing, so a page the author marked "Don't index" is indexed. Map the codes, as the include below does.

Open Graph from custom fields

SEO settings take custom fields. Define them once, in the admin under Customization, Custom Fields, for the entity SEO Settings: a select ogType with the options website and article, and an image ogImage. They appear in the SEO panel of every record that has SEO settings, so every page type gains them without a new layout area. One include then prints the lot:

Template code includes/seo.html, called with seo and title
{$robotsMap = ['all' => 'index, follow', 'index' => 'index, nofollow', 'follow' => 'noindex, follow', 'none' => 'noindex, nofollow']}
{if $seo && $seo->getTitle()}{$title = $seo->getTitle()}{/if}
<title>{$title|escape}</title>
<meta property="og:title" content="{$title|escape}">
{if $seo}
  {if $seo->getDescription()}<meta name="description" content="{$seo->getDescription()|escape}">{/if}
  {if $seo->getRobots()}<meta name="robots" content="{$robotsMap[$seo->getRobots()]}">{/if}
  {if $seo->getCanonicalUrl()}<link rel="canonical" href="{$seo->getCanonicalUrl()|escape}">{/if}
  <meta property="og:type" content="{$seo->getCustomFieldText('ogType', 'website')|escape}">
  {$ogImage = $seo->getCustomFieldImage('ogImage')}
  {if !$ogImage}{$ogImage = $seo->getShareImage()}{/if}
  {if $ogImage}<meta property="og:image" content="{$_request->getSchemeAndHost()}{$ogImage->getPath()}">{/if}
{/if}
{% set robotsMap = {'all': 'index, follow', 'index': 'index, nofollow', 'follow': 'noindex, follow', 'none': 'noindex, nofollow'} %}
{% if seo and seo.getTitle() %}{% set title = seo.getTitle() %}{% endif %}
<title>{{ title|e }}</title>
<meta property="og:title" content="{{ title|e }}">
{% if seo %}
  {% if seo.getDescription() %}<meta name="description" content="{{ seo.getDescription()|e }}">{% endif %}
  {% if seo.getRobots() %}<meta name="robots" content="{{ robotsMap[seo.getRobots()] }}">{% endif %}
  {% if seo.getCanonicalUrl() %}<link rel="canonical" href="{{ seo.getCanonicalUrl()|e }}">{% endif %}
  <meta property="og:type" content="{{ seo.getCustomFieldText('ogType', 'website')|e }}">
  {% set ogImage = seo.getCustomFieldImage('ogImage') ?: seo.getShareImage() %}
  {% if ogImage %}<meta property="og:image" content="{{ _request.getSchemeAndHost() }}{{ ogImage.getPath() }}">{% endif %}
{% endif %}
Template code In a page template, and in a custom entity record's template
{include 'includes/seo.html' seo=$page->getSeoSettings() title=$page->getName()}

{include 'includes/seo.html' seo=$store->getActiveRevision()->getSeoSettings() title=$store->getName()}
{{ include('includes/seo.html', {seo: page.getSeoSettings(), title: page.getName()}) }}

{{ include('includes/seo.html', {seo: store.getActiveRevision().getSeoSettings(), title: store.getName()}) }}

Each page type passes its own record and the include does the rest, so a new page type needs one line rather than its own head. Structured data follows the same shape: a schemaType custom field on SEO settings chooses the @type of a JSON-LD block, and the values come from the record. The custom fields read back with the default you give when a record's author left them empty.

What the sitemap does with them

The platform generates /sitemap.xml from the published records of the types that are switched on for it, and it reads their SEO settings:

  • a record whose robots value is follow or none (either "Don't index" option) is left out;
  • a record with a canonical URL is listed at that URL, and left out when the canonical URL is on another site;
  • a record's translations are listed as its alternates.

The file is generated in the background and kept for an hour, so a change shows in the sitemap within the hour, not at once.

Go deeper

Was this page helpful?

Your answer goes to the docs team.