Skip to content

<head> Management

Each renderer accepts a headContent callback in its server entry. The callback returns the request-specific HTML that τjs places inside the document <head>.

Route meta does not emit tags by itself. It is one input to headContent, alongside critical data, dedicated headData and routeContext.

A rendered document combines three sources:

  1. index.html supplies the static structure and tags shared by the application.
  2. headContent produces request-specific tags from the route inputs.
  3. τjs adds any build, development and renderer tags required for the response.

The route inputs available to headContent are:

InputSourceUse
metaattr.metaStatic route metadata and fallback values
dataattr.dataCritical route data, subject to renderer timing below
headDataattr.head.dataDynamic data resolved before the renderer starts
routeContext{ appId, path, attr, params }Route-aware values outside the data snapshot

If two sources produce the same tag, headContent decides which value wins. τjs does not merge or deduplicate application-provided titles, metadata or links.

This React example has the same shape in Vue and Solid. Import createRenderer and escapeHtml from @taujs/vue or @taujs/solid for those renderers.

import { createRenderer, escapeHtml } from "@taujs/react";
import { App } from "./App";
export const { renderSSR, renderStream } = createRenderer({
appComponent: ({ location }) => <App location={location} />,
headContent: ({ headData, meta }) => {
const title = headData?.title ?? meta?.title ?? "My application";
const description =
headData?.description ?? meta?.description ?? "Application description";
return `
<title>${escapeHtml(title)}</title>
<meta name="description" content="${escapeHtml(description)}">
`;
},
});

escapeHtml is exported by all three renderer packages. It is suitable for HTML text and quoted attribute values. It does not validate URL schemes and it is not the right encoding for script contents.

Use the narrowest declaration that satisfies the response:

RequirementPut it in
Fixed for the routeattr.meta
Critical body dataattr.data
Dynamic value required in a streamed document headattr.head.data
Content allowed to arrive behind a streamed boundaryattr.deferred
A change after client-only navigationThe client application or router head handling

Deferred values are never passed to headContent. Work that can cause a redirect or determine response status belongs in critical resolution. A value that must appear in the initial document head belongs in meta, data or headData, not attr.deferred.

On render: "ssr", all three renderers receive fully resolved critical data. A declared attr.head loader runs after critical data resolution and before the renderer starts.

headContent: ({ data, meta }) => `
<title>${escapeHtml(data.title ?? meta?.title ?? "Product")}</title>
`;

The availability of critical data reflects native renderer behaviour:

  • React builds the head at shell readiness from the current snapshot, which may be empty.
  • Vue builds the head once before rendering from the current snapshot, which may be empty.
  • Solid builds the head after critical route data has settled.

The portable rule is therefore: use meta for static values and attr.head for dynamic values that must be present in a streamed document head. This keeps the declaration correct if an application changes renderer and makes the head dependency explicit in the request contract.

Independent shell content is not delayed by deferred data. Head work is different: attr.head resolves before the renderer starts, so it delays the shell by design.

attr.head.data uses the same handler and service-dispatch contract as attr.data. The result is passed to headContent as headData on both rendering strategies.

{
path: "/products/:id",
attr: {
render: "streaming",
meta: {
title: "Products",
description: "Product details",
},
data: (params, ctx) =>
ctx.call("catalogue", "getProduct", { id: params.id }),
head: {
data: (params, ctx) =>
ctx.call("catalogue", "getProductHead", { id: params.id }),
timeoutMs: 3_000,
optional: false,
},
},
}
headContent: ({ headData, meta }) => `
<title>${escapeHtml(headData?.title ?? meta?.title ?? "Product")}</title>
<meta
name="description"
content="${escapeHtml(headData?.description ?? meta?.description ?? "")}">
`;

The host applies these rules:

  • timeoutMs defaults to 3,000 ms and must be a positive finite number.
  • A live-request timeout aborts the head loader where possible, logs an advisory and continues with headData: undefined.
  • An ordinary loader rejection fails the request unless optional: true is declared.
  • optional: true reclassifies an ordinary rejection as degradation and continues with headData: undefined.
  • A client disconnect aborts the work and the renderer does not start.
  • headData is never included in __INITIAL_DATA__; it exists only for server head generation.
OutcomeResponse behaviour
ResolvedheadData contains the result
Deadline while request remains liveAdvisory log, then headData: undefined
Rejection, optional omitted or falseRequest fails through the normal error path
Rejection, optional: trueAdvisory log, then headData: undefined
Client disconnectRequest work is aborted and rendering does not continue

Always handle headData as optional and keep the loader small. It is intentionally on the pre-shell path.

headContent runs for a server-rendered τjs route. It is not a client-side head manager.

A screen known only to the client router can stay undeclared and client-rendered, but navigation to that screen does not rerun headContent. Update document.title and any route-specific metadata through the client application or router. On a direct document request, the server-selected shell still determines the initial head. See Request Contracts & Data for the shell ownership rules.

Use escapeHtml for title text and quoted attribute values:

import { escapeHtml } from "@taujs/react"; // or @taujs/vue, @taujs/solid
headContent: ({ headData, meta }) => {
const title = headData?.title ?? meta?.title ?? "Default title";
const image = headData?.image ?? meta?.image;
return `
<title>${escapeHtml(title)}</title>
${image ? `<meta property="og:image" content="${escapeHtml(image)}">` : ""}
`;
};

escapeHtml prevents an untrusted URL from escaping an attribute. It does not make the URL safe. For user-influenced canonical or image URLs, parse the value and allow only the schemes and origins the application expects.

HTML escaping corrupts JSON inside a script raw-text element. Serialise the value as JSON and escape < so an embedded </script> cannot terminate the element:

const jsonForScript = (value: unknown): string => {
const json = JSON.stringify(value);
if (json === undefined) throw new TypeError("JSON-LD value is not serialisable");
return json.replace(/</g, "\\u003c");
};
headContent: ({ headData }) =>
headData?.jsonLd
? `<script type="application/ld+json">${jsonForScript(headData.jsonLd)}</script>`
: "";

headContent does not receive the request CSP nonce, and τjs does not rewrite application-provided head scripts to add one. τjs nonces the scripts it emits itself. If the application CSP requires an inline head script to be authorised, account for it explicitly with a policy-supported hash or a different delivery strategy.

  • Use meta as static input, not as an automatic tag generator.
  • Use attr.head for dynamic streamed-head dependencies.
  • Treat headData as optional and provide a static fallback.
  • Keep status-bearing, redirect-bearing and head-critical values out of attr.deferred.
  • Escape every dynamic value according to its output context.
  • Keep head loaders fast because they delay the shell.
  • Manage head changes after client-only navigation in the client application.