newX 1.3 — islands to disk, server-mode islands, image proxy

Build & Deploy

Build & deploy

x produces optimized production builds: prerendered HTML, island bundles, and a server entry point, all in one command.

Build command

Run x build to produce a production build. The build output goes to a .x/ directory (override with --outDir).

terminal
x build
[x] resolving routes...
[x] found 12 routes
[x] building static pages...
[x] building island bundles...
[x] building server bundle...
[x] build complete in 1.2s -> .x
x build --outDir dist # write to ./dist instead of .x/

Output structure

The .x/ directory contains everything needed to deploy:

output tree
.x/
client/ // Static site: prerendered HTML + public/ assets
index.html
about/index.html
blog/
hello-world/index.html
_islands/
index-abc123/ // One hydration bundle per page's islands
index-abc123.js
styles.css // Compiled Tailwind
favicon.ico
server/
index.ts // Server entry for SSR/API/actions (run with x start)

Static pages (and their public/ assets) live under .x/client/ and deploy to any static host. Server code ships as .x/server/index.ts, a single entry that re-imports your x.config.ts at runtime.

Static page export

Pages with mode = "static" are rendered at build time, including their loader output. Dynamic segments generate one HTML file per unique path at build time.

Incremental static regeneration

A static page can opt into time-based revalidation with revalidate. The page is prerendered on first request, cached in memory for N seconds, then re-rendered on demand. Each response carries an X-Revalidated header ( hit / miss / none).

src/pages/pricing.tsx
export const mode = "static";export const revalidate = 3600; // re-render at most once per hour

Bust the cache programmatically with a POST to /__x/revalidate — send { "path": "/pricing" } to revalidate one page or an empty body to clear the whole cache.

Production server

Use x start to run .x/server/index.ts. It serves static files and handles dynamic routes, and the entry handles SIGTERM/SIGINT gracefully: it stops accepting connections, flushes the error reporter, and drains in-flight requests for up to 3 seconds before exiting.

terminal
x start
[x] production server running at http://localhost:3000
PORT=8080 x start # PORT env var overrides the default 3000

If a build has no server entry (every page is static and there are no API routes), x start falls back to serving .x/client/ as a plain static file server with an SPA index.html fallback.

Programmatic build

build() from @thexjs/core runs the same pipeline the CLI uses, useful for custom deploy scripts:

build.mjs
import { build } from "@thexjs/core";await build({  pagesDir: "src/pages",  apiDir: "src/api",  actionsDir: "src/actions",  outDir: "dist",  configPath: "x.config.ts",});

Docker deployment

Deploy with a minimal Docker image using the official Bun runtime:

Dockerfile
FROM oven/bun:1 AS buildWORKDIR /appCOPY package.json bun.lock .RUN bun installCOPY . .RUN bun run build:packagesRUN x build --outDir distFROM oven/bun:1-slimWORKDIR /appCOPY --from=build /app/dist distCOPY --from=build /app/node_modules node_modulesEXPOSE 3000CMD ["x", "start", "--outDir", "dist"]

Deploy to Vercel

Vercel doesn't run a long-lived Bun process, so it uses a different adapter: @thexjs/adapter-vercel builds a .vercel/output/ tree (Build Output API v3) directly, so no vercel.json is required.

terminal
bun add -d @thexjs/adapter-vercel
x build --adapter vercel
vercel deploy --prebuilt

If every page in your app uses mode = "static" (no API routes, no server actions), no function is emitted at all, just static files and filesystem routing. Server-mode pages, API routes, and server actions run inside a single bundled nodejs20.x function.