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.

JavaScript
// 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.

One file, three consumers

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

KeyPurpose
siteCanonical origin — { url }. Drives absolute canonicals, the sitemap, blog feeds, and LLM context links.
docsThe documentation section (sidebar, navbar, search).
blogAn optional blog section (post list, banners, feeds).
i18nInternationalization — locales for URL-prefix routing (@opentf/web-i18n).
proxyDev-only request forwarding — send path prefixes to a separately-running backend (otfw dev).

site

JavaScript
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:

FieldTypeNotes
titlestringProduct name in the navbar.
versionstringVersion badge (e.g. "v0.4.0").
logostringNavbar logo URL.
githubstringGitHub URL (navbar icon).
repoUrlstringSource repo — enables per-page "Edit this page" links.
dirstringContent folder under app/ (default "docs").
navarrayTop navbar links: { label, href, icon?, external? }.
footerobject{ text, links }.
searchobject{ provider: "pagefind" } to enable static search.
lastUpdatedbooleanShow a "Last updated" line per page (git/frontmatter). Requires the last-updated build plugin (set on docs or blog).
repoUrl + lastUpdatedTogether 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.

JSXapp/api/layout.jsx
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.

JavaScript
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).

JavaScript
i18n: {
  locales: ["en", "fr", "ja"],
  defaultLocale: "en",
}
FieldTypeNotes
localesstring[]All supported locales.
defaultLocalestringServed 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.

JavaScript
// otfw.config.js — forward /api/* to a locally running worker
export default {
  proxy: {
    "/api": "http://localhost:8787",
  },
};
FormMeaning
"/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).

Why proxy instead of running the API locally?

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.

Framework vs. theme config

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.