SEO
Coil SEO is runtime-owned metadata built from route, site, and locale context.
Start With The Output
On a normal public page, Coil can inject markup like this into the document head:
<meta name="description" content="..." />
<link rel="canonical" href="https://gitly.example.com/fr/explore" />
<meta name="robots" content="index,follow" />
<link rel="alternate" hreflang="en-GB" href="https://gitly.example.com/explore" />
<link rel="alternate" hreflang="fr-FR" href="https://gitly.example.com/fr/explore" />
<meta property="og:title" content="..." />
<script type="application/ld+json">...</script>
That is the right mental model:
- templates own visible structure
- the runtime owns the search-facing metadata envelope
What Is Configured?
Current checked-in SEO config looks like this:
[seo]
canonical_host = "gitly.example.com"
emit_json_ld = true
And it works together with i18n config such as:
[i18n]
default_locale = "en-GB"
supported_locales = ["en-GB", "fr-FR", "de-DE"]
fallback_locale = "en-GB"
localized_routes = true
Because canonical and alternate URLs are route- and locale-aware, SEO cannot be treated as a completely separate subsystem.
Field Reference
canonical_host
- Required: yes in current checked-in platform configs
- Type: host string
- Meaning: default canonical host for absolute URL generation
Interaction:
- site-specific canonical hosts override the app-level default when a site is resolved
emit_json_ld
- Required: no
- Type: boolean
- Meaning: whether the runtime should emit built-in JSON-LD page metadata
What Is Automatic Today?
Coil currently generates these pieces automatically at the document boundary:
- meta description
- canonical URL
- robots meta
- alternate
hreflanglinks - Open Graph title, description, and type
- baseline JSON-LD page nodes when enabled
Important practical behaviour:
- if the page already has
</head>, the runtime injects before it - if the page has no
<head>, the runtime creates one
This is why templates do not need to re-implement head assembly page by page.
What Is Customizable?
The runtime can merge route- or handler-provided metadata into the automatic baseline.
Current custom inputs include:
- explicit title
- explicit description
- explicit canonical URL
- extra alternate URLs
- extra robots directives
- extra JSON-LD nodes
The extension point is typed metadata from runtime code, not hand-built strings in templates.
Canonical And Alternate URL Logic
Coil builds canonical and alternate URLs from:
- resolved site
- site canonical host
- route name
- route params
- route locale policy
- supported locales for that site
So for a localised route:
- the site resolves from the request host
- the locale resolves from the route and site policy
- the runtime emits the canonical URL for that exact route/site/locale
- the runtime emits alternates only for equivalent localised routes
That is why hardcoding canonical links in templates is almost always the wrong move.
JSON-LD
Current behaviour:
- if
emit_json_ld = true, the runtime emits a page-level JSON-LD node automatically - extra JSON-LD nodes can be appended through typed metadata
This is the correct place for:
- page schema
- product schema
- event schema
- structured metadata that belongs to the route, not to incidental template layout
Common Mistakes
Building canonical URLs by string concatenation
Use runtime site-aware route generation instead.
Forgetting site and locale when reasoning about metadata
That is the fastest way to produce wrong-host or wrong-language head output.
Treating private or draft content like public indexed content
SEO output should follow actual publication state.
Rebuilding <head> behaviour inside every template
That defeats the whole runtime-owned metadata model.
Supporting Implementation And Repo Examples
Concrete supporting files:
apps/shoppr/platform.tomlapps/shoppr/platform.dev.tomlapps/gitly/platform.tomlapps/gitly/platform.dev.tomlcrates/coil-runtime/src/render/seo.rscrates/coil-runtime/src/http/routing/model.rscrates/coil-runtime/src/render/model.rs