Guides
Titles, descriptions, sitemaps and redirects
Control how pages appear in search results and what happens to old URLs.
At the end of this page each entry carries its own title, description and social card in the
rendered page's head, /sitemap.xml and /robots.txt answer, and an old URL forwards to the new
one instead of going dead.
You need the SEO capability on. It is on by default; the switch is at
Extensions › Capabilities, and capabilities and packs
explains what turning it off takes away. You also need BASE_URL in .env set to the site's
public origin: while it is unset, or left at the bare http://localhost default, Thallo leaves
the canonical and Open Graph URLs out of the head rather than tell a crawler the site lives on
localhost.
Fill in an entry's SEO tab
- Open the entry under Content and choose the SEO tab in the editor's side panel, beside Publishing and Versions. The Design view carries the same tab in its own side panel.
- Type a Title and a Description. The hints beside them count towards 60 and 160 characters; they are advice, not limits.
- Leave Robots at
indexunless the page should stay out of search results. The other choices arenoindexandnoindex,nofollow. - Pick a Twitter card if you want one — Summary or Summary large image. Not set writes no card tag at all.
- Open Open Graph for OG title, OG description and OG image URL. The image is
either an absolute
https://URL or a path beginning with a single/. - Press Save SEO.
Every value is stored against the entry and the locale you are editing, so a translated entry gets its own title and description. Switch locale in the header and fill the tab again.
What each field becomes in the head
| Field | In the page's head |
|---|---|
| Title | The <title> element, and og:title when OG title is empty. |
| Description | <meta name="description">, and og:description when OG description is empty. |
| Robots | <meta name="robots">, written only when the value is not index. |
| Twitter card | <meta name="twitter:card">, written only when you pick one. |
| OG title | <meta property="og:title">. |
| OG description | <meta property="og:description">. |
| OG image URL | <meta property="og:image">, made absolute against the origin when it starts with /. |
Thallo adds four tags you do not edit: og:type (website on the homepage, article
everywhere else), og:url and <link rel="canonical">, both the entry's canonical URL, and
og:site_name, the site's name from Settings › General. An entry published in more than one locale also gets a
<link rel="alternate" hreflang="…"> per locale and an x-default.
Values are escaped before they are written, and a URL that is neither http://, https:// nor
rooted at a single / is dropped rather than emitted.
A page rendered in a preview session is the exception: it carries
<meta name="robots" content="noindex, nofollow"> and no other head tag, so a draft is never
indexed or scraped for a social card.
The tags come from {{ seo_head() }} in the theme's layout. The default theme's layout.twig
calls it; a theme of your own has to call it too.
What Thallo writes when a field is empty
- Title — the entry's
titlefield, run through the title template{title} — {site_name}. With neither an override nor atitlefield, the head carries the site name alone. - Description — nothing, unless the content type has a fallback field. The description tag is simply not written.
- OG title and OG description — the resolved title and description.
- OG image — the site-wide default image, if one is set.
- Robots —
index, which writes no tag.
A title you type in the SEO tab is used verbatim. Only a title taken from a field goes through the template.
Set the site-wide defaults
Three values in .env feed the defaults:
| Setting | Default | What it does |
|---|---|---|
SEO_TITLE_TEMPLATE |
{title} — {site_name} |
Applied to a title taken from a field. {site_name} is the site's name from Settings › General, which is also the title of a page that has none of its own. |
SEO_DEFAULT_OG_IMAGE |
empty | The og:image of every page without one. |
To feed the meta slots from fields of your own, add config/seo.php to the project and map them
per content type:
<?php
return [
'fallbacks' => [
'posts' => [
'title_field' => 'title',
'description_field' => 'excerpt',
'image_field' => 'cover',
],
],
]; The key is the content type's slug, and those three are the only keys read. Without a map, the
title still falls back to a field named title; description and image do not fall back to
anything.
Serve the sitemap and robots.txt
The feeds write absolute URLs against the site's public origin: BASE_URL, or a workspace's own
address when workspaces are on. PUBLIC_URL_BASE in .env overrides it, for a site whose feeds
must name a different address. While the origin is still the http://localhost default, all
three feeds answer 409 with the plain-text body
No public origin: set BASE_URL to the site's address (PUBLIC_URL_BASE overrides it). rather
than publish URLs a crawler cannot follow.
With a real origin, three URLs answer:
/sitemap.xml— a<urlset>of up to 50 000 URLs. Above that it becomes a<sitemapindex>listing the page files instead./sitemap/1.xml,/sitemap/2.xml… — one page file each, up to 50 000 URLs. A number past the last page is a 404./robots.txt.
The sitemap holds one <url> per published entry and locale whose content type has Public
delivery on (Settings › Content Types, on the type). Each one carries the entry's canonical
URL, a <lastmod> of when that version was published, and an xhtml:link alternate for every
other locale the entry is published in. Listing, archive and term URLs are not in it. The XML is
cached with no expiry and dropped whole on any content change, so a publish shows up on the next
request.
robots.txt is built from a list of groups. The default is one group:
User-agent: *
Allow: /
Sitemap: https://example.com/sitemap.xml The Sitemap: line is appended for you. To change the groups, add a robots key to
config/seo.php:
'robots' => [
['user_agent' => '*', 'allow' => ['/'], 'disallow' => ['/cart']],
], Let Thallo redirect a changed slug
Change an entry's slug and Thallo writes a 301 from the old slug to the entry. Nothing to do: the old URL keeps working. Claiming a slug also clears any redirect that pointed away from it, so moving an entry back does not leave a loop. Content types, entries and fields has the URLs a slug builds.
These automatic redirects sit in the same list as the ones you write, and can be removed the same
way. If the entry they point at stops being published, the old URL answers 410 Gone instead of
forwarding — see drafts, preview and publishing.
Write your own redirect
- Go to Settings › Redirects and press Add redirect.
- Choose the Content type the old slug belonged to.
- Type the Source slug — the slug alone,
old-path, not a whole path. Thallo looks for it at that content type's URL:/posts/old-pathfor a type served at/posts,/old-pathfor one mounted at root. - Type the Target URL: a path beginning with a single
/, or an absolutehttp://orhttps://URL. A protocol-relative//example.comis refused. - Choose the Status —
301,302or308. - Press Add redirect.
A source slug another entry of that type already holds as its own slug is refused: the entry wins the URL, and a redirect there would never fire. Redirects are written in the site's default language.
The list shows Content type, Source, Status and State. State is live when
the target resolves and broken when it does not — a redirect pointing at an entry that was
unpublished or deleted. Each row ends with a Remove redirect button.
The same redirects, one content type at a time, are behind the Redirects button on that type's entry list.
Check it worked
Open a published page in a browser and view its source: the <title>, the description and the
canonical link should be the ones you set. Then, from any machine:
curl -s https://example.com/robots.txt
curl -s https://example.com/sitemap.xml
curl -sI https://example.com/posts/old-path The first two answer with the robots groups and the XML rather than the 409 text. The third
answers 301 with a Location header holding the entry's canonical URL — absolute once
PUBLIC_URL_BASE is set.