Everything a developer changes on a Core dna site lives in its theme repository. This guide covers the repository and its build, printing records in templates, stylesheets and client-side code, and the parts of a site around the main content.
L1–L2
Unit 05 · Around the main content
Canonical URLs and structured data
Around the main contentPage 35 of 35Updated
Where a record's canonical URL comes from, the resolution order that respects the editor's Canonical URL field, and how to build JSON-LD structured data from the record and its SEO settings.
Who this is for. Developers writing the head of a site's templates. After this page you can print a canonical link for every kind of record and add structured data that changes when the content does.
The SEO settings a template can read
Every versioned record (page, blog post, book page, FAQ page and question, product and category description) has SEO settings, reached with getSeoSettings():
The admin describes the Canonical URL field as the "URL of a page that has the same content and should be regarded as the original page by search engines". Resolve the canonical in this order:
the Canonical URL an editor set on the record, when it is not empty;
otherwise the record's own path, getFullPath(), which already reflects a custom URL when the record has one, with the site's scheme and hostname in front.
Build the fallback from the record rather than from the request, so the canonical is the same however a visitor reached the page. Put the resolution in one include that takes the record:
{% set siteUrl = 'https://' ~ _currentSite.getPrimaryEnvironmentPrimaryHostname() %}
{% set seoSettings = record.getSeoSettings() %}
{% set cmsCanonical = seoSettings.getCanonicalUrl() %}
{% set canonicalUrl = cmsCanonical != "" ? cmsCanonical : (siteUrl ~ record.getFullPath()) %}
<link rel="canonical" href="{{ canonicalUrl }}">
Test the editor's value with neq "" (Twig != ""), not with isset(): an empty string counts as set, and isset() would print href="". The hostname comes from getPrimaryEnvironmentPrimaryHostname(), so a staging copy points its canonical at the live site.
On every kind of record
A canonical tag in the page template covers pages only. Blog posts, products, FAQ and book pages render through their own templates, each with its own head, so call the include from each of them with the record it shows: {include 'includes/frontend/canonical.html' record=$post} in Smarty, {% include 'includes/frontend/canonical.html' with { record: post } %} in Twig. Then check the page source of one record of each kind.
Structured data from the record
Structured data is a JSON-LD block that describes the page to search engines. Build it from the record and its SEO settings rather than typing values into a template, so it changes when an editor changes the content. Build the whole structure as an array and encode it once:
Encode every value. Every value passes through the JSON encoder, never into a hand-quoted string, so a quotation mark or a line break in CMS text cannot break the block. For one or two values, encoding each one works too: "headline": {$post->getName()|json_encode}.
Escape tags. The flag 8 is JSON_HEX_TAG: it encodes < and >, so CMS text cannot close the <script> element early.
Add the nonce. The house practice is to give every inline <script> the CSP nonce, JSON-LD included: The CSP nonce.
More than one node
When a page describes several things, such as the organisation and the article, list them under '@graph' in the same array and encode it once, the same way. Organisation details that appear on every page, such as the name, address and phone number, belong in site data, where an administrator can correct them without a deploy (Site data and feature flags).