What reaches the document
On the server the chain is resolved before the render, because an async generateMetadata has to be awaited.
On the server the chain is resolved before the render, because an async generateMetadata has to be awaited. Its
tags are written into <head> before the first byte of the body in every render mode (string, async and
stream); a streamed page sends them with its shell. A page with ssr = false writes the same tags from its static exports, so a crawler sees its title and description without
its content. Its generateMetadata does not run there.
The order inside the head is:
- The defaults (
charsetand aviewportmeta), unless the metadata sets its own. - The
headexports of the routes. - The metadata, which wins over (2) and (1) for the same tag: the same identity rules as the head registry apply
(a single
title, onedescription, aviewport).
Values that repeat keep every entry: each og:image, twitter:image, article:tag, og:locale:alternate, and each
alternate link by hreflang, media or type. An Open Graph image's sub-properties (og:image:width, og:image:alt)
are written for the first image only, because they share one identity. Each Twitter image writes its own, after its URL:
twitter:image:alt, twitter:image:secure_url, twitter:image:type, twitter:image:width and twitter:image:height.
On the client the leaf of the matched chain applies the merged result through the metadata feature of the router.
A navigation replaces the tags of the previous page, so a description the new page does not set is removed. An async
generateMetadata lands when its promise resolves. The client writes the same tags the server writes for the same
route.
The request's query string reaches the generators as search: a record with one value per key, and an array when a
key repeats. It is the raw query, not the validated search of validateSearch.
Escaping and URLs#
Every text value is escaped by the head registry. A URL that is not an http(s) URL is dropped, so javascript: and
data: never reach an attribute. Custom schemes are allowed only for app links (appLinks and the Twitter app
card), and javascript:, data:, vbscript:, file: and blob: are refused even there. A protocol-relative value
(//host/x) is refused. A relative URL resolves against metadataBase; without one it is written as given.
The rules a value must pass to be written (a value that fails is dropped, not repaired):
| Rule | Why |
|---|---|
| No control character, and no space at either end. | A URL parser strips them, so javascript:alert(1) would pass a scheme check on the text and still be a script URL once parsed. |
An http: or https: value is written with // (https://host). |
https:host is a path on an https page and a host on an http page. |
A value with no scheme must not start with //, \ or /\. |
A browser reads a backslash as a slash, so /\evil.example is another host. |
With a metadataBase, a relative value must resolve on the base's own origin. |
Whatever trick a request-built path uses, it cannot move the tag to another site. |
Icons, the manifest link and the links of the meta export (written as given, with no base) refuse any scheme but http(s); an icon may also be a data:image/... URI of 32 KiB at most. |
javascript:, vbscript:, file:, blob: and a data: that is not an image never reach a <link href>. A relative path and a protocol-relative CDN address stay as written. |
JSON-LD#
jsonLd takes one object or an array. Each value becomes a <script type="application/ld+json"> in the document
head, outside the head registry (which refuses scripts). The text escapes <, >, & and the two line separators,
so a value cannot close the script element. The server writes the request's CSP nonce on each script when a CSP is
configured. On the client the scripts follow the page: a page with jsonLd writes its scripts, and leaving it removes
them.
Response headers#
A headers export sets response headers for the page. Inner layers override outer ones, and names are lower-cased.
A name or value that could split a response (a space in the name, a line break in the value) is refused and
reported. content-length, content-type, link, location and set-cookie cannot be set from a route; a
route that tries is reported with phase head.