Document head & SEO.
$head({ … }) is a reactive document-head manager.
It sets document.title, meta tags, the canonical link,
Open Graph / Twitter cards, JSON-LD, and <html>
attributes — and the same resolved head feeds
renderToString, so server-rendered pages are
crawlable and show rich social previews. Anything that has to rank in
search or render a link preview — a marketing page, a blog post,
a docs site, an e-commerce PDP — needs this.
Quick start
Call $head({ … }) from anywhere that runs during a
render — usually the top of a page component. Values can be
reactive: read $state and the head re-applies when it
changes.
function ProductPage() {
$head({
title: `${$product.name} — Acme`,
meta: { description: $product.summary, "theme-color": "#111" },
og: { title: $product.name, image: $product.image, type: "product" },
twitter: { card: "summary_large_image" },
link: [{ rel: "canonical", href: $canonicalUrl }],
jsonLd: { "@type": "Product", name: $product.name, offers: { price: $product.price } }
})
return Column([ /* … the page … */ ])
}
Call $head where it renders. Put it
inside a component body (or the page tree) so it runs on every render
pass. A bare top-level $head(…) that nothing
evaluates won’t fire — like any expression, it has to be
reached during rendering.
Configuration
| Key | Type | Renders |
|---|---|---|
title | string | Sets document.title and <title>. |
titleTemplate | string | Wraps title, e.g. "%s — Acme". %s is replaced by the title. |
meta | object | Named meta tags: { description, "theme-color", keywords } → <meta name content>. A charset key emits <meta charset>. |
og | object | Open Graph: { title, image, type, url } → <meta property="og:KEY">. |
twitter | object | Twitter cards: { card, site, creator } → <meta name="twitter:KEY">. |
link | object[] | Array of <link> descriptors, e.g. [{ rel: "canonical", href }, { rel: "alternate", hreflang: "fr", href }]. |
jsonLd | object | object[] | JSON-LD structured data → <script type="application/ld+json">. @context defaults to https://schema.org. |
base | string | object | <base href> for the document. |
htmlAttrs | object | Attributes for <html>, e.g. { lang: "en", dir: "ltr" }. In SSR these are returned for the page shell. |
Reactive heads
Because $head runs during render and reads your state, the
head is just another reactive output. Change the state and the title /
meta update live — no manual DOM writes.
$unread = 3
function Inbox() {
$head({ title: $unread > 0 ? `(${$unread}) Inbox` : "Inbox" })
return Column([ /* … */ ])
}
Per-route composition
Multiple $head(…) calls in one render pass
merge in call order — later calls win on
conflicts. This is the layout-plus-page pattern: a shared layout sets
defaults, and the routed page overrides just the title and description.
function Layout(page) {
$head({ // site-wide defaults
titleTemplate: "%s — Acme",
meta: { "theme-color": "#111", description: "Acme builds tools for teams." },
og: { type: "website", image: "/og-default.png" }
})
return Column([ Navbar(), page, Footer() ])
}
function AboutPage() {
$head({ title: "About", meta: { description: "Who we are and why." } })
return Column([ /* … */ ])
}
$app(Layout(AboutPage()))
// → About — Acme , description "Who we are and why.",
// and the layout's theme-color + default OG image survive the merge.
When the route changes, the new page’s render contributes a fresh head and the previous route’s tags are replaced — titles don’t pile up. See Routing for the router surface.
Server-side rendering
renderToString evaluates your program and returns the
resolved head alongside the HTML, so you can inject a crawlable
<head> into your page shell:
import { renderToString } from "aktion-runtime";
const { html, state, head, headAttrs } = renderToString(source);
const page = `<!doctype html>
<html ${Object.entries(headAttrs).map(([k, v]) => `${k}="${v}"`).join(" ")}>
<head>
<meta charset="utf-8">
${head} <!-- resolved <title>, meta, OG, JSON-LD -->
</head>
<body>
${html}
<script>window.__AKTION_STATE__ = ${JSON.stringify(state)}</script>
</body>
</html>`;
renderToString result | Meaning |
|---|---|
html | The rendered application markup. |
state | State snapshot to ship for hydration. |
head | The resolved <head> markup (title, meta, OG, Twitter, links, JSON-LD) emitted by every $head(…) that ran. Empty string when none ran. |
headAttrs | <html> attributes (lang, dir, …) contributed via $head({ htmlAttrs }). |
The client re-applies the same head on hydration, so the title / meta you served and the title / meta the app manages stay in sync. See Production & deployment for the full SSR + hydration flow.
Recipes
Canonical + alternates for i18n
$head({
link: [
{ rel: "canonical", href: $url },
{ rel: "alternate", hreflang: "en", href: $urlEn },
{ rel: "alternate", hreflang: "fr", href: $urlFr }
]
})
Article JSON-LD for rich results
$head({
title: $post.title,
meta: { description: $post.excerpt },
og: { title: $post.title, type: "article", image: $post.cover },
jsonLd: {
"@type": "Article",
headline: $post.title,
author: { "@type": "Person", name: $post.author },
datePublished: $post.date
}
})
Blocking indexing on a staging build
$head({ meta: { robots: $isProd ? "index,follow" : "noindex,nofollow" } })
Notes & gotchas
- Managed tags are owned by Aktion. Every tag
$headinjects carriesdata-rui-head. The manager replaces its previous set on each commit and clears them when the program is torn down — so a re-plan never leaves a stale title behind. Don’t hand-write tags Aktion also manages. - Commits are batched. Contributions in a render pass
accumulate and commit on a microtask, so several
$headcalls resolve to one merged head with no flicker. In SSR the merge happens synchronously when the head is serialized. - Escape HTML in values? No need. Titles, meta content, and link attributes are escaped for you. JSON-LD is serialized safely.
linkde-dupes byrel+href+hreflang, so a layout and a page declaring the same canonical won’t emit it twice.
Next
Routing
Per-route pages and navigation — where per-route $head composition lives.
Production & deployment
SSR with renderToString, hydration, and shipping a crawlable shell.
Third-party widgets
Mount, WebComponent, $script, and $dom for imperative libraries.