Social Sharing and SEO Metadata
Shareable by Default
Every page rendered by the default theme (and the starter theme) ships a complete
set of social-sharing tags, so a link pasted into Slack, LinkedIn, iMessage,
WhatsApp, Discord, X, Mastodon, or Facebook unfurls as a rich card – with a
title, a page-specific description, and a branded image – without any per-page
authoring. The tags are emitted by the bundled partials/head_meta.html.jinja
partial, included from each theme’s base.html.jinja <head>.
These tags are derived from data your pages already have (page.title,
page.lead, page.author, page.date, page.tags, page.media, site.url,
site.name). You only add metadata when you want to override a default.
The absolute_url() Helper
absolute_url() turns a root-relative path into an absolute URL using
site.url, available as both a function and a filter:
{{ absolute_url(page.url) }} {# https://example.com/blog/post/ #}
{{ page.url | absolute_url }} {# filter form, same result #}
- Root-relative paths gain the site origin (trailing slash on
site.urltrimmed). - Already-absolute URLs (
http://,https://) pass through unchanged. - When
site.urlis empty the input is returned unchanged.
It does not depend on the request, so canonical links and og:url are
identical in accent serve and static accent build output.
Resolved Card Primitives
The head-meta partial reads three computed values, resolved once per page from the same logic that feeds JSON-LD (so a page’s card and its structured data never disagree):
| Value | Resolution chain | Drives |
|---|---|---|
page.description | frontmatter description -> auto lead -> site.description | <meta name="description">, og:description, twitter:description |
page.social_image | frontmatter image -> first page media image -> structured_data.default_image (absolute) | og:image, twitter:image |
page.meta_tags | merge of site.meta + per-page meta: (page wins) | the open tag map (see below) |
og:type is derived automatically: article for pages with a date
(adding article:published_time, article:modified_time, article:author,
and one article:tag per tag), otherwise website. twitter:card is
summary_large_image when an image resolves, otherwise summary.
Draft, review, and archived pages – and any page with a noindex: true
frontmatter flag – emit <meta name="robots" content="noindex,nofollow">.
Multilingual pages emit <link rel="alternate" hreflang> for each translation
plus x-default.
The Open meta Map
All platform-specific and arbitrary tags flow through one open map, so a network that appears next year is a config line, not a code change. The guiding rule is type the container, never the platforms.
Site-wide defaults (site.meta)
# config.yaml site: name: My Site url: https://example.com description: A site powered by Accent CMS. meta: twitter:site: "@mysite" fediverse:creator: "@author@social.example" # any future tag is just a key structured_data: default_image: /assets/social-card.png # reused as the card fallback image
Per-page overrides (meta: frontmatter)
--- title: Getting Started with Accent CMS description: Install Accent in five minutes. image: /media/getting-started/cover.png meta: og:type: article twitter:creator: "@a_guest_author" ---
Page entries override site entries on key collision. Each tag is emitted as
<meta property="..."> or <meta name="..."> inferred from its key prefix:
og:, article:, fb:, product:, music:, and video: render as
property=; everything else (including twitter:*) renders as name=.
How the partial loops the map
{% for tag in page.meta_tags %}
{% if tag.property %}<meta property="{{ tag.key }}" content="{{ tag.value }}">
{% else %}<meta name="{{ tag.key }}" content="{{ tag.value }}">{% endif %}
{% endfor %}
No template edit is ever needed to add a new tag – it is just another key in
site.meta or a page’s meta: block.
Validating meta with a document model (optional)
The meta map works with no model bound – the card always renders. Binding a
document model layers validation on the same
carrier: a description that exceeds its max_length, or a typo’d og:type /
twitter:card value, is reported at content load instead of surfacing only when
a human inspects the unfurl (platforms silently truncate over-long Open Graph
values).
The default theme ships a base model that types the meta object; content
models inherit it with a one-line extends: base, so the schema is shared
without copy-paste:
# themes/default/models/base.yaml -- the shared schema fields: description: type: string constraints: { max_length: 200 } meta: type: object fields: og:type: { type: enum, values: [website, article, profile], default: website } twitter:card: { type: enum, values: [summary, summary_large_image], default: summary }
# themes/default/models/docs.yaml -- inherit the whole meta schema extends: base
The object stays open: only declared sub-fields are validated, so undeclared
keys (fediverse:creator, fb:app_id, any future tag) still pass through. The
model validates input; it does not resolve fallbacks or emit tags – the
resolvers and this partial are unchanged. See the document-models guide’s
Sharing a Social / SEO meta Schema
section for the full pattern.
Validating Your Cards
After deploying with a real site.url, confirm the unfurl with the platform
validators: the Facebook Sharing Debugger, the X Card Validator, and the
LinkedIn Post Inspector. Because the canonical and og:url are build-safe,
the same tags appear whether you serve dynamically or ship a static build.