Configuration
An OTF Web project is configured by a single otfw.config.js at the project root. It is read by the toolchain (@opentf/web-cli) at build and dev time, and by the themed UI (@opentf/web-docs) at runtime.
// otfw.config.js import { defineDocsConfig } from "@opentf/web-docs/config"; export default defineDocsConfig({ site: { url: "https://example.com" }, docs: { title: "My Project", dir: "docs", repoUrl: "https://github.com/org/repo", // "Edit this page" links lastUpdated: true, // "Last updated" line per page }, blog: { dir: "blog", lastUpdated: true }, });
defineDocsConfig is an identity helper — it returns the object unchanged but gives your editor full type hints.
The CLI uses it to wire build plugins (nav, posts, search, feeds, LLM context, last-updated); the Navbar/DocsLayout components read docs at runtime; and the SSG step reads site.url for canonical URLs, sitemap, and feeds.
Top-level keys
| Key | Purpose |
|---|---|
site | Canonical origin — { url }. Drives absolute canonicals, the sitemap, blog feeds, and LLM context links. |
docs | The documentation section (sidebar, navbar, search). |
blog | An optional blog section (post list, banners, feeds). |
i18n | Internationalization — locales for URL-prefix routing (@opentf/web-i18n). |
proxy | Dev-only request forwarding — send path prefixes to a separately-running backend (otfw dev). |
site
site: { url: "https://example.com" }
Required for production docs/blog builds. Omit it and the build fails before writing dist/; pass --base-url at build time to override it for a specific deploy.
docs
The main documentation section. Common fields:
| Field | Type | Notes |
|---|---|---|
title | string | Product name in the navbar. |
version | string | Version badge (e.g. "v0.4.0"). |
logo | string | Navbar logo URL. |
github | string | GitHub URL (navbar icon). |
repoUrl | string | Source repo — enables per-page "Edit this page" links. |
dir | string | Content folder under app/ (default "docs"). |
nav | array | Top navbar links: { label, href, icon?, external? }. |
footer | object | { text, links }. |
search | object | { provider: "pagefind" } to enable static search. |
lastUpdated | boolean | Show a "Last updated" line per page (git/frontmatter). Requires the last-updated build plugin (set on docs or blog). |
repoUrl + lastUpdated | — | Together they enable "Edit this page" links (<repoUrl>/edit/main/<source>). repoUrl alone is not enough. |
See Packages → web-docs → Configuration for the exhaustive field reference and per-folder _meta.js ordering.
Multiple sections
A section like an API reference is just another folder with a DocsLayout — no config. Any top-level folder under app/ that has a layout rendering DocsLayout becomes its own section with its own generated sidebar, breadcrumbs, prev/next, and (with lastUpdated) the "Last updated" / "Edit this page" line — the same traits as the main docs.
import { DocsLayout } from "@opentf/web-docs"; import config from "../../otfw.config.js"; export default function (props) { return ( <DocsLayout config={config.docs} frame={false}> {props.children} </DocsLayout> ); }
That's the whole thing — the same layout as app/docs/layout.jsx. DocsLayout reads the generated nav and scopes it to the current route, so app/api/** shows the /api sidebar (ordered by each folder's _meta.js). Add a third section by adding a folder and a copy of this file.
blog
Turns on the blog generator (post list, banners, reading time, feeds). See Packages → web-docs → Blog.
blog: { dir: "blog", title: "My Blog", lastUpdated: true }
i18n
Enables URL path-prefix locale routing for @opentf/web-i18n. The toolchain threads it into the client mountApp, pre-renders one static page per locale (otfw build --ssg), and serves per-locale SSR with locale detection (otfw serve).
i18n: { locales: ["en", "fr", "ja"], defaultLocale: "en", }
| Field | Type | Notes |
|---|---|---|
locales | string[] | All supported locales. |
defaultLocale | string | Served at the bare path; others are prefixed (/fr/…). |
See Packages → web-i18n → Locale Routing for the full routing model and the message/formatting API.
proxy
Dev-only. Forward matching request path prefixes to another origin instead of handling them inside otfw dev. This keeps the framework provider-agnostic: run your backend on its real runtime and proxy to it, while the dev server keeps serving the SPA with hot reload. The primary use is reaching platform bindings the dev server can't host itself — e.g. a local wrangler dev that provides Cloudflare D1, KV, and secrets.
// otfw.config.js — forward /api/* to a locally running worker export default { proxy: { "/api": "http://localhost:8787", }, };
| Form | Meaning |
|---|---|
"/api": "http://localhost:8787" | Prefix → target origin. The full path + query are preserved. |
"/api": { target: "http://localhost:8787" } | Object form (room for future options). |
Keys are matched longest-prefix-first, so "/api/admin" can point elsewhere than "/api". A matched prefix is forwarded before the in-process API handler runs, so proxied endpoints never also execute locally. proxy has no effect on otfw build or a production deploy — in production the backend runs for real (see Fetch handler).
Platform bindings often exist only inside the host's dev runtime. Running the API there and proxying /api/* to it means the same handler code runs in dev and production — no mocks, no drift.
otfw.config.js is the project config. The docs/api/blog blocks are interpreted by @opentf/web-docs; a non-docs app can use just site and the CLI still builds it.