Static Export
Pre-render your whole app to plain HTML and deploy it free to any static host - Cloudflare Pages, GitHub Pages, Netlify, or an S3 bucket. Static when you can, server when you must.
Choose at create time
When you scaffold a project, pick Static site at the prompt. That wires npm run build to the exporter (after a tsc --noEmit typecheck in TypeScript projects), drops the production server scripts, and declares the starter's fonts with @font-face in app/globals.css instead of [[fonts]] (below).
npm create giojs@latest
# ? Which language? › TypeScript / JavaScript
# ? What are you building? › Server app / Static siteYou can also pass it non-interactively:
npm create giojs@latest my-site -- --staticpnpm create giojs my-site --staticyarn create giojs my-site --staticbun create giojs my-site --staticBuild
Develop with npm run dev as usual. When you're ready to ship, export to the out/ folder:
npm run build # runs: gio export → ./outpnpm build # runs: gio export → ./outyarn build # runs: gio export → ./outbun run build # runs: gio export → ./outEvery static route is rendered through the real SSR pipeline, so what you see in dev is what you get in out/. getServerSidePropsruns at build time and its data is baked into the HTML. The export loads the project's .env files first, with the same precedence as the server (production mode unless NODE_ENV=development).
Every export also writes out/404.html - your app/not-found.tsx if you have one, otherwise the built-in 404 page - so static hosts return a real 404 for unknown URLs instead of falling back to the home page.
The export runs in production mode, like the server: React's production build, and no error messages or stacks in the HTML (dev mode only with NODE_ENV=development). A page that fails to render is skipped and listed with an error reference; the matching log line on stderr has the message and stack.
Interactive pages
Exported pages hydrate exactly like served ones. The exporter builds the client bundles in production mode into out/_next/static/chunks/ and every page carries the same hydration envelope and bootstrap script the server renders, so state, effects, event handlers, and GioLink soft navigation all work on a static host.
GIO_PUBLIC_*values are frozen into the bundles (and the HTML) at export time - re-export after changing them.- Props come from the build-time
getServerSidePropsrun; they ship in the page as JSON, so never return secrets from it. - A route whose bundle fails to build, or is rejected for importing server-only code, still exports as plain HTML (no client JS), and the exporter lists it with the reason.
- Soft navigation fetches the target page's HTML (
/about→out/about/index.html); a URL that was never exported falls back to a full page load, so the host's404.htmlshows with a real 404. - Serve
out/at the domain root: pages reference their chunks as/_next/static/chunks/....
public/ and robots.txt
public/ is copied into out/ twice, matching the server: at the site root, so /favicon.ico, /robots.txt, /manifest.json, and /.well-known/... resolve on any static host, and under out/public/ for links written as /public/....
- The root copy skips what the server never serves at the root: dotfiles (except under
.well-known/), symlinks, and a top-level_gio/. - A rendered page keeps its output file:
public/index.htmlnext toapp/page.tsxstays at/public/index.htmlonly, and the exporter lists it as skipped. app/sitemap.ts,app/robots.tsandapp/manifest.tsare written assitemap.xml,robots.txtandmanifest.webmanifest(see Metadata & SEO); setGIO_SITE_URLso their relative URLs become absolute. Without those modules the exporter generatesrobots.txt(andsitemap.xmllisting every exported page whenGIO_SITE_URLis set).- Either way, a
public/file of the same name wins - as on the server - and a module it shadows is listed as skipped. - Page
metadata/generateMetadatarun at export time; relative Open Graph and canonical URLs resolve againstmetadataBaseorGIO_SITE_URL.
Dynamic routes
A dynamic route like app/posts/[id]/page.tsx needs to know which paths to render. Export getStaticPaths to list them:
export async function getStaticPaths() {
const posts = await db.posts.all();
return { paths: posts.map((p) => ({ params: { id: String(p.id) } })) };
}Catch-all params take the same /-joined string the page receives (an array of segments works too), and an optional catch-all exports its bare parent when the param is omitted or empty:
export function getStaticPaths() {
return {
paths: [
{ params: {} }, // out/docs/index.html
{ params: { slug: 'guides/setup' } }, // out/docs/guides/setup/index.html
{ params: { slug: ['api', 'ref'] } }, // out/docs/api/ref/index.html
],
};
}Entries missing a required param, or whose values contain empty, . or .. segments or a backslash, are skipped with a reason instead of being written - nothing is ever written outside out/.
getStaticPaths are skipped with a warning - they can only be served by the GioJS server.What can't be static
The exporter skips anything that needs a live server, and tells you what it skipped:
route.tshandlers and Server-Sent Events- WebSocket (
wsHandler) routes - ISR revalidation (
export const revalidate- there's no server to revalidate on) - Runtime image optimization via
/_gio/image:GioImagerenders its plainsrcin an export (nosrcset), so ship pre-sized images gio.tomlsettings the Rust server applies, such as[[fonts]]: declare fonts with@font-facein an imported stylesheet instead -url()s to files next to it (or../public/fonts/x.woff2) are bundled with hashed names
If you need any of those, use Server mode instead.
Deploy
out/ is a self-contained static site - no runtime required. Drop it on any static host:
# Cloudflare Pages / Netlify: build command "npm run build", output dir "out"
# GitHub Pages: push ./out to a gh-pages branch
# Or serve locally to check:
npx serve out