SEO, Sitemap, and Robots
Vext provides SEO as a framework capability for full-stack applications. It
merges application defaults, route metadata, and per-render metadata into the
server-rendered document, and can generate sitemap.xml and robots.txt at
build time or serve them at runtime.
The feature is opt-in. Omitting frontend.seo keeps the existing rendering and
build output unchanged.
Basic Configuration
publicOrigin is the deployment origin, not one fixed page URL. Vext combines
it with the current request pathname. For example, /posts/hello and
/posts/release-notes produce different canonical URLs even though they share
one publicOrigin.
Set PUBLIC_ORIGIN per environment when preview, staging, and production use
different domains. It must be an absolute HTTP(S) URL without user info, query,
or hash.
Page-level Metadata
Static, JSON-safe metadata belongs on the existing route declaration. Its
finite static grammar treats inline objects as the simplest form and also
accepts same-file const bindings and TypeScript static wrappers. A route
options helper call is rejected because the index does not execute helper
bodies and cannot know their final metadata. Inline the final object or pass a
same-file const. Imported values, computed expressions, and interpolated
templates are not executed:
Data-dependent metadata belongs in the third argument to res.render():
Merge order is frontend.seo.defaults → RouteOptions.frontend.seo →
res.render(..., { seo }). Canonical, alternate, and relative Open Graph URLs
require a declared origin. Canonical and sitemap values are absolute pathnames;
query strings and hashes are rejected so accidental duplicate URLs fail early.
Supported metadata includes title, description, robots directives, canonical,
Open Graph, Twitter cards, language alternates, and JSON-LD. Existing
res.render(..., { head }) remains available; explicit legacy head fields are
merged after structured SEO for compatibility.
Build-time Sitemap
sitemap: {} defaults to build mode. Successful static artifacts are included
automatically, and an entries provider can add dynamic paths known during the
build:
Build mode requires publicOrigin. Output is written into the frontend build
closure and included in the deploy manifest with the correct XML/TXT content
types. Static routes with frontend.seo.index: false or a noindex robots
directive are excluded. More than maxUrlsPerFile entries produce a sitemap
index and numbered chunks; the limit defaults to 50,000.
The complete sitemap set also has independent budgets: maxUrls defaults to 100,000, maxBytes to 50 MiB of rendered UTF-8 output, and runtime timeoutMs to 5,000 ms. Generation stops and fails closed as soon as a budget is exceeded; the runtime deadline aborts the provider signal.
The provider receives only { mode, origin, originKey, signal }; Vext does not
inject app, services, or app.db into configuration callbacks. Read dynamic
entries from a build-safe module or external content source, and honor the
abort signal.
Runtime Sitemap and Dynamic Domains
Use runtime mode when entries or the public domain must be selected per request:
At runtime, the request Host must exactly match publicOrigin or one entry in
origins. Unknown hosts return 404 instead of generating attacker-controlled
canonical or sitemap URLs. A route or render can select a finite named origin
with seo.originKey; undeclared keys fail closed.
Configured origins are canonicalized for host comparison, including trailing dots and default ports, while any pathname base in publicOrigin is preserved in canonical, sitemap-index, chunk, and robots URLs. Runtime SEO endpoints support both GET and HEAD; HEAD returns the same status and headers without an entity body.
Runtime sitemap and robots responses use Cache-Control: no-store. Add an
explicit cache at your reverse proxy only after defining its host and refresh
policy.
Robots
The path is /robots.txt. When sitemap is enabled, the generated robots file
also includes its URL. Runtime SEO endpoints fail startup if they conflict with
an existing user GET or HEAD route.
SEO without Browser Hydration
SEO is applied before the document policy, so a route with
frontend.hydration: "none" keeps SSR HTML, CSS, canonical metadata, Open
Graph, JSON-LD, and user-authored document scripts while omitting the Vext and
React browser runtime. See Hydration for the exact
boundary.
This is page-level server-only rendering. It is not Selective or Partial Hydration, an Islands architecture, React Server Components, or Partial Prerendering (PPR).