Advanced

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

KeyTypeRenders
titlestringSets document.title and <title>.
titleTemplatestringWraps title, e.g. "%s — Acme". %s is replaced by the title.
metaobjectNamed meta tags: { description, "theme-color", keywords }<meta name content>. A charset key emits <meta charset>.
ogobjectOpen Graph: { title, image, type, url }<meta property="og:KEY">.
twitterobjectTwitter cards: { card, site, creator }<meta name="twitter:KEY">.
linkobject[]Array of <link> descriptors, e.g. [{ rel: "canonical", href }, { rel: "alternate", hreflang: "fr", href }].
jsonLdobject | object[]JSON-LD structured data → <script type="application/ld+json">. @context defaults to https://schema.org.
basestring | object<base href> for the document.
htmlAttrsobjectAttributes 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 resultMeaning
htmlThe rendered application markup.
stateState snapshot to ship for hydration.
headThe 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

Next