Head tags — title, meta, link, script, style — are emitted by FaceTheory in a stable order so that server-rendered HTML and client-hydrated DOM match exactly. Reaching around the head primitive to inject tags directly into a component body breaks the determinism guarantee.
The head primitive
FaceTheory exposes the head primitive and helper-first authoring APIs from the main entry:
import {
canonical,
jsonLd,
metaTag,
normalizeHeadTags,
openGraph,
renderFaceHead,
renderHeadTag,
titleTag,
twitterCard,
type FaceHeadTag,
} from '@theory-cloud/facetheory';
renderFaceHead(out, options)— render the head section from aFaceRenderResult. Accepts an optionalcspNonceandallowedOrigin.normalizeHeadTags(tags, options)— canonicalize an array of head tags (de-duplicate, apply nonces).renderHeadTag(tag)— serialize a single tag to HTML.titleTag(title, { template })— create a deterministic<title>, optionally applying a%stitle template.metaTag(name, content)— create a named<meta>tag.openGraph(...)/twitterCard(...)— create typed Open Graph and Twitter card meta groups.canonical(href)— create a canonical same-origin or http(s) link tag.jsonLd(data, { nonce? })— create a safeapplication/ld+jsonscript tag for structured data.
FaceHeadTag shape
type FaceHeadTag =
| { type: 'title'; text: string }
| { type: 'meta'; attrs: FaceAttributes }
| { type: 'link'; attrs: FaceAttributes }
| { type: 'script'; attrs: FaceAttributes; body?: string }
| { type: 'style'; cssText: string; attrs?: FaceAttributes }
| { type: 'raw'; html: string };
Faces declare head tags through FaceRenderResult.headTags:
import {
canonical,
jsonLd,
metaTag,
openGraph,
titleTag,
twitterCard,
} from '@theory-cloud/facetheory';
return {
html: '<h1>Hello</h1>',
headTags: [
titleTag('Hello', { template: '%s · FaceTheory' }),
metaTag('description', 'A FaceTheory page'),
...openGraph({
title: 'Hello FaceTheory',
type: 'website',
url: 'https://app.example/',
image: '/assets/card.png',
}),
...twitterCard({
card: 'summary_large_image',
title: 'Hello FaceTheory',
image: '/assets/card.png',
}),
canonical('/'),
jsonLd({
'@context': 'https://schema.org',
'@type': 'WebPage',
name: 'Hello FaceTheory',
}),
],
};
Helpers return normal FaceHeadTag objects. They do not create a parallel head
pipeline; de-duplication, nonce application, escaping, and stable ordering still
come from renderFaceHead() / normalizeHeadTags().
For strict CSP routes that set csp.inlineScripts === false, JSON-LD is the one
nonce-carried inline script body FaceTheory permits. Pass the request nonce to
the renderer (renderFaceHead(out, { cspNonce: ctx.request.cspNonce }), or let
createFaceApp() do that for Face responses). The JSON-LD tag must be
type="application/ld+json" and carry the matching request nonce; inline
hydration JSON and generic inline scripts still fail closed.
De-duplication
normalizeHeadTags() de-duplicates tags that have a deterministic key:
- the latest
<title>wins; - meta tags key by
charset,name,property, orhttp-equiv; - link tags key by
rel+href+ optionalas; - script tags key by
srcorid; - style tags key by
idordata-emotion.
Tags without one of those keys are intentionally exempt from de-duplication and
are emitted in order after charset/title normalization. That includes keyless
JSON-LD tags, because pages often need multiple structured-data blocks. Add an
id only when you want normal last-wins de-duplication for a specific JSON-LD
block.
The raw escape hatch
{ type: 'raw', html } inserts HTML verbatim into <head> without escaping or nonce augmentation. Use it only when the caller fully owns the HTML, and never for content that could carry user input. Strict CSP rules disable this path — see Strict CSP.
Structured <style> vs raw HTML
Prefer structured styleTags (which take cssText + optional attrs) over { type: 'raw' } for <style> injection. The structured path lets FaceTheory’s deterministic emission and CSP enforcement apply consistently. See Core Patterns → Emit custom head styles through structured tags.
CSS-in-JS extraction
For React + Emotion, the React adapter wires @emotion/server automatically when you use createReactStreamFace with Emotion-aware components. The extracted CSS is emitted as deterministic <style> tags. For Vue and Svelte, framework-native style emission (Vue scoped styles, Svelte compile-time CSS) flows through the same head primitive.