Skip to content
Margin

Extensions

Replace the single-site request policy, resolution, or telemetry module.

The framework serves one documentation site out of the box. Everything a host decides per request — which site a request serves, which content version, whether to redirect, how to tag the CDN cache, what telemetry to attach — goes through one seam: a module exporting extensions: DocsExtensions. The types live in src/extensions/types.ts; that file is the whole contract.

You do not need an extension to deploy. Replace the default only when you host several sites from one deployment, route by hostname, or attach your own telemetry.

Choosing the module

Set DOCS_EXTENSIONS=./extensions.ts and rebuild to select a root-level module.

The server imports #docs-extensions. vite.config.ts and vitest.config.ts alias it to:

  • src/extensions/default.ts when DOCS_EXTENSIONS is unset, or
  • the file DOCS_EXTENSIONS names, resolved against the framework’s root directory when relative.

The choice is made at build time, so set DOCS_EXTENSIONS in the build environment. TypeScript resolves #docs-extensions to the default through tsconfig.json paths; your module must export the same name and type:

For a local extensions.ts at the repository root, extend the working default and add the hooks you need:

import type { DocsExtensions } from "./src/extensions/types"
import { extensions as defaultExtensions } from "./src/extensions/default"

export const extensions: DocsExtensions = {
    ...defaultExtensions,
    telemetry: {
        eventProperties(request) {
            return { docsHost: new URL(request.url).host }
        },
    },
}

With DOCS_EXTENSIONS set, the test suite resolves #docs-extensions to your module, so only code that imports #docs-extensions exercises it; the default extension’s own tests import src/extensions/default.ts directly.

The contract

resolveSite(request): Promise<SiteResolution>

Called once per request (the result is shared by every reader of that request). It returns:

FieldMeaning
siteThe DocsSite the request serves, or null when the request names no site. Production answers null with 404; previews render an empty, uncacheable page.
versionSelectorWhich content version to serve: "current" (the site’s live pointer), { kind: "commit", sha } (an exact ingested commit) or { kind: "preview", shaPrefix } (a preview build by commit prefix; an ambiguous prefix serves nothing).
previewtrue makes every response no-store and noindex.
canonicalHostProduction only: when set and different from the request’s host, the request is answered with a 308 to this host. null never redirects.

The server, not the extension, applies the serving gates: a version is served only when it is ready and the site’s status is active, read live on every request. An extension may therefore cache site without delaying a suspension.

DocsSite carries the fields the server needs from the docs_site row: id, slug (the public identifier used in analytics, cache tags and URLs), name, description, repo ({ owner, name, branch, subdir } or null), analyticsEnabled (false suppresses the site’s configured analytics integrations) and webmcpEnabled.

requestPolicy(request): RequestPolicy

Called on every request before any database read, so it must be cheap and synchronous.

FieldMeaning
hstsPreloadtrue sends HSTS with preload; includeSubDomains; false sends a plain max-age. Only enable it for domains you control end to end.
cacheTagThe Vercel-Cache-Tag for purgeable responses: at most 256 characters, no commas. null sends no tag.
previewTreat the request as a preview without consulting the database.
shortCircuitA Response to return immediately for the whole request, or null.

telemetry

All hooks are optional.

  • eventProperties(request, resolution) adds properties to every analytics event.
  • groups(resolution, request) returns an analytics group (groupType, groupKey, optional properties) for the event and its one-time group identify, or null. resolution is null when the site lookup failed, so a host can still group by the request.
  • onEvent(event) receives every observability event the framework emits.
  • visitorKey(request, resolution) is the stable input the cookieless visitor and session ids hash alongside the address and user agent. Defaults to the served site’s slug (the request host when no site resolves).

webmcpRollout(site, posthog) (optional)

Turns WebMCP on for a site in addition to site.webmcpEnabled. It runs on every page render and must never block: answer from a cache and refresh in the background.

rateLimitScope(site) (optional)

The identity the public rate limiter and the feedback signature key on. Defaults to site.id.

clientIp(request) (optional)

The caller’s address for the public rate limiters (chat, feedback), stored only hashed. Return undefined to fall back to the default: Vercel’s x-vercel-forwarded-for, then x-forwarded-for. A host behind another proxy (Cloudflare, say) answers with that proxy’s client-address header — only for requests it knows came through the proxy.

The default extension

src/extensions/default.ts serves a single site:

  • Site: the docs_site row whose slug is SITE_SLUG (default docs). Lookups — found or missing — are cached for 30 seconds; a failed lookup is remembered for 5 seconds and re-thrown, never answered from a stale row.
  • Version: { kind: "commit", sha: DOCS_CONTENT_SHA } when the build baked the commit it ingested, so each deployment serves exactly its own content; "current" otherwise (local development).
  • Preview: true when VERCEL_ENV is preview.
  • Canonical host: the host of SITE_ORIGIN in production, unless DOCS_CANONICAL_REDIRECT=0; never on previews or in development.
  • Request policy: no HSTS preload, cache tag site-<slug>, no short-circuit.
  • Telemetry, WebMCP rollout, rate-limit scope: none; the defaults apply.
VariableDefaultEffect
SITE_SLUGdocsWhich docs_site row to serve. Lowercase letters, digits and inner hyphens, at most 55 characters.
DOCS_CONTENT_SHAunsetThe commit the build ingested; set by the build.
DOCS_CANONICAL_REDIRECT10 disables the production redirect to the SITE_ORIGIN host.