SEO Structured Data¶
Badger puts schema.org structured data (JSON-LD) in the <head> of storefront pages so search engines can identify the site, its owner and its search box, and show breadcrumb trails in results.
| Markup | Where | Built by |
|---|---|---|
WebSite + Organization (+ SearchAction) |
Every storefront page: home, CMS pages, collections, products, search, basket and checkout | SiteStructuredDataService |
BreadcrumbList |
Product and collection pages (Nova family) | BreadcrumbJsonLdRenderer |
This page covers the site-wide graph. Breadcrumbs are covered by the breadcrumb fragment (templates/nova/fragment/breadcrumb.html).
Site-wide graph¶
Each page carries one <script type="application/ld+json"> holding an @graph with two nodes that point at each other by @id:
WebSite(<siteUrl>#website): the site name, its canonical URL, andpublisherpointing at the Organization. When search is available it also has apotentialActionSearchAction, which is what Google uses for the sitelinks search box.Organization(<siteUrl>#organization): the name, the URL, and then each of the following that is set:logosameAs: the social profileslegalName,address,identifier(company number) andvatID
Example, for a site with search available, a logo, two social accounts and legal entity details:
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "WebSite",
"@id": "https://acme.example.com#website",
"name": "Acme Outdoors",
"url": "https://acme.example.com",
"publisher": { "@id": "https://acme.example.com#organization" },
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": "https://acme.example.com/search?q={search_term_string}"
},
"query-input": "required name=search_term_string"
}
},
{
"@type": "Organization",
"@id": "https://acme.example.com#organization",
"name": "Acme Outdoors",
"url": "https://acme.example.com",
"logo": "https://cdn.example.com/media/logo.png",
"sameAs": [
"https://www.facebook.com/acmeoutdoors",
"https://www.instagram.com/acmeoutdoors"
],
"legalName": "Acme Outdoors Limited",
"address": "1 High Street, Buxton, SK17 6AA",
"identifier": { "@type": "PropertyValue", "propertyID": "Company number", "value": "01234567" },
"vatID": "GB123456789"
}
]
}
Where each value comes from¶
| Property | Source |
|---|---|
url, the @ids |
https:// + the site's primary domain (domainURL). Other domains are not used, because they redirect to the primary one. |
name |
The site name (Site Identity). |
logo |
The site logo (Site Identity). CDN URLs that start with // get https:, and site-relative paths get the site URL. |
sameAs |
The social accounts (Site Identity). A bare handle becomes a profile URL using the same prefixes as the Social Links extension (Facebook, Instagram, Twitter/X, LinkedIn company, YouTube channel, Pinterest, TikTok). A value that is already a URL is used as it is. A handle for an unknown network is dropped. |
legalName |
legal-entity-name, otherwise giftaid-org-name, so a charity that has set up Gift Aid doesn't have to enter its name twice. |
address |
legal-entity-registered-address. It is stored as one line, so it is output as text and not split into a PostalAddress. |
identifier |
legal-entity-company-number. |
vatID |
legal-entity-vat-number. |
The legal entity keys are the ones an admin confirms for Subject Access Requests (see Subject Access Requests).
When the SearchAction appears¶
The /search?q= page is always mapped, but its results come from Typesense. The SearchAction is therefore added only when Typesense is configured for the site, using the same check (TypesenseClientFactory.isConfigured) the indexer uses. A site that has explicitly set useSearch to false is treated as having opted out. The default value of useSearch is ignored, because the Nova and Brock themes don't read that key.
Configuration¶
| Key | Type | Default | Category | Effect |
|---|---|---|---|---|
structured-data-enabled |
Boolean | true |
seo-config |
Turn it off to remove the WebSite/Organization graph from every page, for example on a site that adds its own Organization markup. Breadcrumbs are not affected. |
Changes appear within five minutes. The graph is cached per site and catalogue in the per-pod in-memory cache.
How it is wired¶
SiteStructuredDataService (commerce-core, seo package)
builds the graph, serialises it with the app's ObjectMapper, escapes it, caches it per site
|
SiteStructuredDataInterceptor (web-mvc, registered in WebMvcConfiguration)
postHandle: puts it on every rendered view as `siteStructuredDataJsonLd`
|
templates/bootstrap/fragment/structuredData.html :: siteJsonLd
prints it; included from fragment/head.html in nova, brock, pop, depot, spec, atelier and bootstrap
- Built in Java. Templates only print the attribute. They never build JSON.
- Escaped for
<script>. After serialising, the service writes<,>and&as<,>and&. These characters only occur inside JSON strings, where the escapes mean the same thing, so a site name such as</script>can't close the element. This is why the fragment can useth:utext. - One fragment for every theme. The fragment is in the bootstrap theme, the last leg of template resolution, so every theme picks it up. Each theme's head has a single
th:replaceline for it. - An interceptor, not an extension. It covers every page, including those rendered outside the extension pipeline, without each site having to place an extension. Like
StorefrontChromeInterceptor, it skips redirects and leaves the attribute off if anything fails. It never fails a page. - Not on error pages. Exception-handler views don't run interceptor
postHandle, so error pages have no site graph.
Testing¶
SiteStructuredDataServiceImplTest: the graph with search on and off (including an explicituseSearch=false), social accounts tosameAs, missing logo, legal entity details and the Gift Aid fallback, the disabled flag, a missing domain, and</script>escaping.SiteStructuredDataInterceptorTest: adds the attribute, skips redirects and views without a model, and never fails a page.SiteStructuredDataHeadRenderTest: renders each theme's realfragment/headand checks the script is inside<head>, exactly once.
Check a live page with Google's Rich Results Test or the Schema Markup Validator.