GioJSdocs
On this page

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

bash
npm create giojs@latest
# ? Which language?      › TypeScript / JavaScript
# ? What are you building? › Server app / Static site

You can also pass it non-interactively:

npm create giojs@latest my-site -- --static

Build

Develop with npm run dev as usual. When you're ready to ship, export to the out/ folder:

npm run build      # runs: gio export  →  ./out

Every 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 getServerSideProps run; 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's 404.html shows 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.html next to app/page.tsx stays at /public/index.html only, and the exporter lists it as skipped.
  • app/sitemap.ts, app/robots.ts and app/manifest.ts are written as sitemap.xml, robots.txt and manifest.webmanifest (see Metadata & SEO); set GIO_SITE_URL so their relative URLs become absolute. Without those modules the exporter generates robots.txt (and sitemap.xml listing every exported page when GIO_SITE_URL is 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 / generateMetadata run at export time; relative Open Graph and canonical URLs resolve against metadataBase or GIO_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:

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

app/docs/[[...slug]]/page.tsx
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/.

Dynamic routes without 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.ts handlers 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: GioImage renders its plain src in an export (no srcset), so ship pre-sized images
  • gio.toml settings the Rust server applies, such as [[fonts]]: declare fonts with @font-face in 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:

bash
# 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