Templates & Design
One call writes the whole head tag
Title, description, canonical, robots, share cards and structured data for every page and every object, resolved when the page renders. You add one line to your layout; the values come from the record, from its collection, and from one site-wide record.
Total CMS writes the head of the document for you. One call in your layout — cms.seo.head(page) — emits the title, meta description, canonical link, robots directives, Open Graph and Twitter card tags, your search-console verification tokens and a single JSON-LD @graph of Organization, WebSite, WebPage, BreadcrumbList and Article nodes. There is no build step: values resolve when the page renders. Delete the title and description tags your layout carries, or the page ships two of each.
Values come from three places and the first non-empty one wins. The SEO card on the record holds the per-record overrides: title, social title, description, social image, canonical URL, no index, no follow, structured data type. The collection's field mapping names the properties that already hold the headline, the summary and the picture, so you do not copy them onto a card for every object. The Site SEO record supplies the rest — site name, base URL, title template, default description, default share image, organization details.
A blog needs no configuration: any collection on the blog, blog-legacy or feed schema is mapped to Article already, with title, summary and image pointed at. A record with No Index on drops out of the sitemap it would otherwise appear in, and loses its canonical tag too, because asking a crawler not to index a page while naming the address to index is two instructions at once. The card's Title accepts ${property} placeholders, so one value composes a title out of the record — ${name} — ${city}, with ${site} for the site name. A title whose placeholders all come back empty falls through rather than emitting the punctuation with nothing around it.
Site SEO is a reserved single-object collection rather than a settings panel, so the default share image and the organization logo are real uploads, and the record reads in Twig like any object. You cannot save over the reserved schema, but on Pro a custom schema with inheritFrom set to seo-site puts site-wide fields of your own — a tagline, a phone number — on the same record.
This page is the worked case. totalcms.co runs on Total CMS: builder/layouts/base.twig carries the single call, and the features template overrides that block to pass the feature record and its collection, handing its own FAQPage node to the same call. This page ships one JSON-LD script with the core nodes and the FAQ in one graph.
What you get
One line in the layout
cms.seo.head() in the head of your layout covers every page on the site. Call it with no arguments on a 404 or a search page and you still get the site title, default description and the WebSite and Organization nodes.
Three layers, first one wins
The SEO card on the record, then the collection's field mapping, then the Site SEO defaults. Leave the card empty and nothing is lost — every field falls through.
Blogs are mapped already
Collections built on the blog, blog-legacy or feed schema get Article markup with title, summary and image mapped out of the box. Other collections point the mapping at whatever their headline and excerpt are called.
No Index means no index
Switching it on removes the record from its sitemap and drops the canonical tag as well, so a crawler is never handed a directive and its contradiction on the same URL.
Your nodes join the core graph
Pass a jsonld option and your FAQPage, Product or Event node lands in the same @graph as the core nodes, referencing the Organization and WebSite core already wrote instead of describing them again.
In practice
One snippet
{# layouts/base.twig — covers every page on the site #}
<head>
<meta charset="utf-8">
{% block seo %}{{ cms.seo.head(page|default(null)) }}{% endblock %}
</head>
{# pages/blog.twig — describe the post, not the page record that routes to it #}
{% set post = cms.collection.object('blog', params.id) %}
{% block seo %}{{ cms.seo.head(post, {collection: 'blog'}) }}{% endblock %}
{# Hand it your own schema.org nodes and they join the same @graph #}
{{ cms.seo.head(post, {collection: 'blog', jsonld: [faq]}) }}
The layout call handles every page. A template rendering a collection object overrides the block and names the collection — without that name Total CMS cannot read the collection's mapping, resolve the object's URL or build its image. No |raw is needed anywhere: head() escapes every value it prints.
FAQ
Common questions
Does it replace the title tag my layout already has?
It emits one, so delete yours. head() writes both the title and the meta description, and a layout that keeps its own ships two of each. The four bundled starter templates already carry {% block seo %}{{ cms.seo.head(page|default(null)) }}{% endblock %} in place of the old title and description blocks, so a page template can override the whole block when it renders something other than the page record.
Where does the share image come from?
The Social Image on the record's SEO card first, then whatever image property the collection maps, then the Default Social Image on the Site SEO record. Whichever wins is served through ImageWorks at 1200×630 as an absolute URL, and if it carries alt text that becomes og:image:alt and twitter:image:alt. The Twitter card type follows: summary_large_image when an image resolved, summary when none did.
How do I keep one page out of search results?
Switch on No Index on that record's SEO card. The robots meta tag is emitted only when noindex or nofollow is on — no tag means index, follow, and it is quieter. The record also leaves the sitemap and loses its canonical tag. If you added the SEO card to a schema of your own, put "seo" in that schema's index array as well: the sitemap builders read the collection index rather than the object files, so without it a noindexed object still shows up in the sitemap.
Can I add my own structured data?
Yes. Pass a jsonld option to head() and your nodes join the same @graph instead of arriving in a second disconnected script. Each entry must be a hash, your nodes land after the core ones, and the first node for any @id wins — so reference {base}/#organization rather than redeclaring it. Core generates five node types and stops there; Product, Event, Recipe and the rest are yours to write. If you want the resolved values in the body of the page instead, cms.seo.data() returns them as a plain array.
Related features
- RSS & sitemap builders RSS, Atom and podcast feeds built in Twig from any query, plus opt-in XML sitemaps per collection and a discoverable index at /sitemap.xml.
- Site Builder Pages live in the builder-pages collection with a route and a template. The router matches URLs at request time, so a page is live the moment you save it.
- Singleton collections For content there is only ever one of — a homepage, an about page, global settings. One object, opened directly, with no list to navigate.
Want the details? Read the SEO built in documentation →
Or keep browsing: every Total CMS feature →
Start Your Free 45-Day Trial