Production Build
otfw build compiles every route and component (JSX/MDX → native DOM), bundles the result, and writes a deployable site to dist/.
bun run build # → otfw build
What it produces
dist/ index.html the app shell, with the hashed bundle injected assets/ bundle-<hash>.js the entry bundle (code-split per route) <chunk>-<hash>.js lazily-loaded route chunks <name>-<hash>.css compiled stylesheets <worker>-<hash>.js bundled web workers (new Worker(new URL(…))) <asset>-<hash>.wasm binary assets (new URL(…, import.meta.url))
Routes are code-split: visiting a page loads only its chunk. Filenames are content- hashed, so they can be cached forever. Web workers and files referenced with new URL(…, import.meta.url) are emitted here too — see Assets & Workers.
How route code splitting works
otfw build generates a small entry bundle plus one lazy chunk per route (and shared chunks such as layout.jsx). The entry calls mountApp with a map of dynamic imports — each route is () => import("…/page.jsx") — and Rolldown emits separate hashed files under dist/assets/.
On first navigation to a route, the browser fetches that route's chunk; other routes stay unloaded. In dev, the same model applies on demand: the first visit to a path compiles /app/.../page.jsx into a /__route/<id>.js module instead of bundling the whole app up front.
Chunks load lazily on navigation today. Automatic prefetch (for example on link hover or when a <Link> enters the viewport) is planned but not implemented in the router yet.
Single-page output
A plain build emits one index.html; the client bundle renders every route. Configure your host to serve index.html for unknown paths so deep links resolve:
location / { try_files $uri $uri/ /index.html; }
Add --ssg to also emit a static .html for every route — see Static Generation. Crawlers then get full markup without running your JS. Per-route <head> tags come from Metadata & SEO exports on your pages and layouts.
Base URL
Set site.url in otfw.config.js so the build can emit absolute canonical URLs, a sitemap, and (when a blog is configured) RSS/Atom feeds. Production docs/blog builds fail loudly when no site URL is configured:
export default defineDocsConfig({ site: { url: "https://example.com" }, });
You can override it at build time with --base-url=https://example.com.
LLM context
Docs/blog builds also write /llms.txt and /llms-full.txt from the filesystem router. The compact file links to key docs, API, and blog routes; the full file includes cleaned Markdown content for MD/MDX pages. Drop public/llms.txt or public/llms-full.txt to override either generated file.
Previewing locally
dist/ is plain static files — serve it with any static server to smoke-test the production output before deploying:
bunx serve dist