page.contact.seo.title. Locale lives in Macrocontent’s i18n layer — not inside the key — same as body content. Page identity
Macrocontent needs a stable page slug to scope SEO keys. Resolution order:
data-macro-page on <html>
Preferred. Use a decoded slug; nested paths are fine (
blog/2026/post).<html lang="en" data-macro-page="contact">resolvePageSlug in config
Optional function when the URL shape does not match your content model.
export const macroConfig = { // ... resolvePageSlug: (pathname) => { if (pathname.startsWith('/blog/')) return pathname.slice(1).replace(/\.html$/, '') return pathname.replace(/^\/|\.html$/g, '') || 'home' }, }URL heuristic
Fallback:
/contact.html→contact,/→home.
Slugs that contain / are encoded in keys: blog/2026/macro-seo → page.blog__2026__macro-seo.seo.title.
Head markup
Mark each SEO field with data-macro-seo. Fallback text/attributes ship in HTML and are overwritten when Macrocontent has a value. Unmarked developer tags are never touched.
data-macro-managed) when values exist; it will not overwrite unmarked developer HTML. <html lang="en" data-macro-page="contact">
<head>
<title data-macro-seo="title">Contact — Example</title>
<meta
name="description"
data-macro-seo="description"
content="Default description"
/>
<meta property="og:title" data-macro-seo="ogTitle" content="Contact — Example" />
<meta
property="og:description"
data-macro-seo="ogDescription"
content="Default OG description"
/>
<meta
property="og:image"
data-macro-seo="ogImage"
content="https://example.com/og.jpg"
/>
<link
rel="canonical"
data-macro-seo="canonical"
href="https://example.com/contact.html"
/>
<meta name="robots" data-macro-seo="robots" content="index,follow" />
<meta name="twitter:card" data-macro-seo="twitterCard" content="summary" />
<meta name="twitter:title" data-macro-seo="twitterTitle" content="Contact — Example" />
<meta
name="twitter:description"
data-macro-seo="twitterDescription"
content="Default Twitter description"
/>
<script type="application/ld+json" data-macro-seo="jsonLd">
{"@context":"https://schema.org","@type":"WebPage","name":"Contact"}
</script>
</head>
<body>
<!-- page body -->
</body>
</html>Injection contract
| Name | Type | Description |
|---|---|---|
title | <title> | Written to textContent |
description | <meta> | Written to content |
ogTitle | <meta> | property="og:title" → content |
ogDescription | <meta> | property="og:description" → content |
ogImage | <meta> | property="og:image" → content (absolute https URL) |
canonical | <link> | rel="canonical" → href |
robots | <meta> | name="robots" → content |
twitterTitle | <meta> | name="twitter:title" → content |
twitterDescription | <meta> | name="twitter:description" → content |
twitterCard | <meta> | name="twitter:card" → summary | summary_large_image |
jsonLd | <script> | type="application/ld+json" → textContent (JSON object/array) |
ogImagemust resolve to an absolutehttps://URL in production HTML.- Empty canonical in Macrocontent → the HTML fallback is left alone.
robotsvalues:index,follow·noindex,follow·index,nofollow·noindex,nofollow.twitterCard:summaryorsummary_large_image.jsonLd: valid JSON object/array →<script type="application/ld+json">.
Runtime vs build
At runtime the SDK hydrates SEO (including managed inject) from GET /content/seo with the public key. For crawler-ready HTML at first byte, use Prerender (or Bake for offline exit).
Related
- Editors · SEO fields — overlay SEO tab
- Marking basics — body primitives
- API reference — Content and editor SEO endpoints