app()
Declares the application. app() runs only on the server. The recommended entry point is .start() — env-aware: when process.env.PLACE_BUILD is set it static-exports to that directory, otherwise it installs server-side capabilities and starts Bun.serve. One file handles both bun dev and bun run build. Use the explicit .run() or .build({ outDir }) entries when you need to force one path. In the islands hydration model each interactive island ships and mounts its own client bundle, so there is no client-side app entry.
Signature
app(config: AppConfig).start() // env-aware: serves on bun, static-exports on PLACE_BUILD=dir
app(config: AppConfig).run() // always start the server (explicit)
app(config: AppConfig).build({ outDir }) // always static-export (explicit)
Full example
import { app } from '@place-ts/component/server'
import { pathRouter, RouterCap } from '@place-ts/routing'
import { rootLayout } from './layouts/root.layout'
import home from './pages/home.page'
import about from './pages/about.page'
export default app({
name: '@my-org/site',
pages: [home, about],
layout: rootLayout,
theme: tokens,
tailwind: true,
security: 'standard',
viewTransitions: true,
caps: [[RouterCap, pathRouter]],
}).start()
Options
pages (required)
The explicit list of page values. Order is irrelevant for routing; the framework matches by path. Duplicate paths throw at startup.
layout
Default layout chain wrapping every page that doesn't override it. Single layout or array; chains compose outside-in.
caps
Per-app capability provisions. Two shapes:
caps: [
[RouterCap, pathRouter], // function form (client only)
[NoteStoreCap, { // object form (per-runtime)
server: () => inMemoryStore(seed),
client: () => localStorageStore(),
}],
]
The function form runs only on the runtime where the cap is used (typically client for clientOnly caps). The object form lets you ship distinct server and client impls without conditionals.
theme
A themeTokens() result. The active theme class auto-prefixes the <html> element; the framework reads the theme cookie per-request to avoid FOUC.
tailwind
true opts into Tailwind v4 inline compilation; CSS is compiled once at startup and inlined into every page (hash-stable for CSP). Or pass a config object for content globs and a custom base.
security
Per-route security headers. 'standard' ships a strict CSP (nonce-bound scripts, hashed inline styles, frame-ancestors none), HSTS, X-Content-Type-Options, and same-origin defaults. Or pass an object for fine control.
viewTransitions
true appends the @view-transition { navigation: auto } rule gated behind prefers-reduced-motion: no-preference. Browsers without cross-document VT navigate normally.
port
Explicit port, or omit to read process.env.PORT, or fall back to 5174. The server auto-walks to the next 9 ports on EADDRINUSE and logs the chosen fallback.
.start()
The recommended entry point. Reads process.env.PLACE_BUILD:
- Set (e.g.
PLACE_BUILD=dist bun src/app.ts) — static-exports the site to that directory and exits. Same shape as.build({ outDir }). - Unset (e.g.
bun src/app.ts) — installs server-side capabilities and startsBun.serve. Same shape as.run().
Use the explicit lower-level .run() / .serve() / .build({ outDir }) when you don't want env-driven dispatch. .serve() is the same as .run() at one level lower. All four entries run server-side only and throw if invoked in a browser.
app.ts runs only on the server. There is no client-side app runtime: each interactive island ships and mounts its own bundle, so a page with no island ships zero framework JavaScript. Client-side capability factories (from router / caps) are forwarded to the island bundler, which wires them into the island bundles automatically..build({ outDir }) — static export
Instead of .run(), call .build({ outDir }) to pre-render the whole app to a static site. It runs the full server setup — Tailwind compile, island discovery and bundling, theme resolution — then writes index.html per route, the island chunks, and a _headers file (Cloudflare strict CSP) to outDir. The exported site is fully interactive; it's the right shape for CDN static hosts. Server-side only.
// app().build({ outDir }) — pre-render to a static site. Runs the
// full server setup (Tailwind compile, island discovery + bundling,
// theme resolution) then, instead of starting a server, writes the
// complete static site to outDir:
//
// <outDir>/index.html, <outDir>/about/index.html, …
// <outDir>/islands/<name>-<hash>.js (+ shared chunks)
// <outDir>/_headers (Cloudflare strict CSP)
//
// The exported site is fully interactive — island bundles ship and
// SPA-nav works. Server-side only; for CDN static hosts.
import { app } from '@place-ts/component/server'
import { pages } from './pages'
await app({ pages, theme: tokens }).build({ outDir: 'dist' })
discoverPages(dir)
An async helper that imports every *.page.tsx (and subdirectory index.ts barrel) under a directory and returns a flat Page[] — feed it straight into pages with top-level await. It does not derive routes from file paths: each page's page('/path', def) declaration stays the single source of truth for its route.
// discoverPages(dir) — async helper that imports every *.page.tsx
// (plus subdir index.ts barrels) under a directory and returns a
// flat Page[]. It does NOT derive routes from file paths — each
// page's page('/path', def) declaration stays the source of truth.
import { app, discoverPages } from '@place-ts/component/server'
export default await app({
pages: await discoverPages('./src/pages'),
}).start()
routes(prefix, pages, opts?)
A pure value transform — prefixes every page's path and optionally applies a shared layout (pages with their own layout keep theirs). No registration, no side effects; composes recursively. Use it to group feature folders, then spread the groups into app().
// routes(prefix, pages, opts?) — a pure value transform: prefixes
// every page's path and (optionally) applies a shared layout. Used
// to group feature folders. No registration, no side effects.
import { routes } from '@place-ts/component/server'
// admin/index.ts
export default routes('/admin', [dashboard, users, settings], {
layout: adminLayout,
})
// app.ts — compose the groups into one app:
app({ pages: [home, ...adminRoutes, ...postRoutes] }).start()