Static Generation

Davaux supports static site generation (SSG) out of the box. The davaux static command compiles your application and pre-renders every page to a plain HTML file — no Node.js required at runtime.

Running the static build

davaux static

This command:

  1. Compiles your TypeScript source and bundles client islands
  2. Starts an in-process server
  3. Crawls every known route and renders it to HTML
  4. Writes the output to the out/ directory

The entire process is self-contained — you do not need to run davaux build first.

Once the build completes, run davaux preview to serve the output locally and verify everything looks right before deploying:

davaux static && davaux preview

Output structure

The URL tree maps directly to the file tree:

RouteOutput file
/out/index.html
/aboutout/about/index.html
/blog/hello-worldout/blog/hello-world/index.html
/api/...(API routes are skipped in SSG)

Any static assets from your public/ directory are copied as-is. The compiled client island bundles land in out/_davaux/.

SSG config options

Several static generation options can be set permanently in davaux.config.ts under the ssg key:

// davaux.config.ts
import { defineConfig } from 'davaux/config'

export default defineConfig({
  ssg: {
    outDir: 'dist/static',
    trailingSlash: 'never',
    basePath: '/my-project',
    notFound: true,
    sitemap: { baseUrl: 'https://example.com' },
  },
})

outDir

Override the default out/ output directory. The --out CLI flag still takes priority when both are set.

davaux static          # writes to outDir from config, or out/ if not set
davaux static --out /tmp/preview  # CLI flag wins

trailingSlash

Controls how route paths map to output files:

Setting/about renders to
'always' (default)out/about/index.html — served as /about/
'never'out/about.html — served as /about

Choose 'never' when your host doesn't rewrite /about/about/index.html automatically.

basePath

Path prefix for deployments to a subdirectory — for example, a project deployed to GitHub Pages at https://user.github.io/my-project/:

ssg: { basePath: '/my-project' }

Davaux prepends the basePath to every injected /_davaux/* script and stylesheet URL in the generated HTML. Links within your own pages are your responsibility — use the same prefix in your <a href> values.

notFound — 404.html for static hosts

Static hosts (Netlify, GitHub Pages, Cloudflare Pages) serve a 404.html from the site root for unmatched paths. When your app has a _error.tsx, Davaux generates this file automatically by running the error page through its normal rendering pipeline:

ssg: { notFound: true }   // explicit (default when _error.tsx exists)
ssg: { notFound: false }  // suppress even when _error.tsx exists

The generated 404.html gets the same layout and styles as your other error pages.

sitemap

Auto-generate a sitemap.xml from all successfully rendered routes:

ssg: { sitemap: { baseUrl: 'https://example.com' } }

The sitemap respects trailingSlash and basePath. The root route / always appears as https://example.com/. Submit the sitemap URL to Google Search Console after deploying.

Changing the output directory via CLI

The --out flag is a one-off override — use ssg.outDir in config for a permanent setting:

davaux static --out dist
davaux static --out /var/www/html

Dynamic routes and getStaticPaths

Static routes (no [brackets]) are rendered automatically. Dynamic routes need you to declare which paths to render by exporting a getStaticPaths function:

// src/routes/blog/[slug].page.tsx
import { definePage, defineStaticPaths } from 'davaux'

export const getStaticPaths = defineStaticPaths(async () => {
  const posts = await db.posts.findAll()
  return posts.map((post) => ({ params: { slug: post.slug } }))
})

export default definePage(async (ctx) => {
  const post = await db.posts.findBySlug(ctx.params.slug)
  ctx.head.title = post.title

  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.htmlContent}</div>
    </article>
  )
})

defineStaticPaths returns an array of { params } objects. Each entry causes Davaux to render one HTML file during the static build.

If a dynamic route is missing getStaticPaths, Davaux prints a warning with a code hint and skips that route — the build continues instead of failing. If an individual page throws during rendering it is also warned and skipped, so one broken page does not abort the whole output.

Per-route rendering mode

By default every page route is included in the static build. Set prerender = false to opt a route out:

import { definePage } from 'davaux'

// This route is skipped during `davaux static`
export const prerender = false

export default definePage(async (ctx) => {
  // auth-gated, personalised, or otherwise dynamic content
})

A common pattern is to mix both modes in one app:

// src/routes/index.page.tsx — rendered to out/index.html at build time
export default definePage(() => <LandingPage />)

// src/routes/blog/[slug].page.tsx — rendered to out/blog/*/index.html at build time
export const getStaticPaths = defineStaticPaths(async () => { /* ... */ })
export default definePage(async (ctx) => { /* ... */ })

// src/routes/dashboard.page.tsx — skipped by davaux static, served dynamically
export const prerender = false
export default definePage(async (ctx) => {
  const user = ctx.state.session.get('userId')
  if (!user) redirect('/login')
  return <Dashboard userId={user} />
})

Deploy out/ to a CDN and run davaux start for the server-rendered routes. An nginx reverse proxy can serve both from one domain:

server {
  # Try the static file first; fall back to the Node.js process
  location / {
    root /var/www/out;
    try_files $uri $uri/index.html @node;
  }

  location @node {
    proxy_pass http://localhost:3000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
  }
}

Static routes are served by nginx at CDN speed with no Node.js involved. Server routes hit the Node process only when needed.

Middleware during static generation

The full middleware chain runs during SSG exactly as it does for live requests. App-level middleware (src/middleware.ts) and scoped _middleware.ts files all execute. This means authentication, headers, and other middleware side-effects apply to generated output — useful for setting cache headers or canonical URLs.

Limitations

Because SSG pre-renders at build time with a synthetic GET request, some request-specific data is unavailable:

  • ctx.cookies — no browser cookies during build, so cookie-gated content will see an unauthenticated request
  • ctx.req.headers — headers from a real browser (Accept-Language, User-Agent, etc.) are not present
  • ctx.req.method — always GET during generation

Design SSG pages to work with no session state. For personalized content, either fetch it client-side from a createResource in an island, or mark the route with export const prerender = false to skip SSG entirely and serve it dynamically.

Deploying the output

The out/ directory is a self-contained static site. Deploy it to any static host:

Netlify:

netlify deploy --dir out --prod

Vercel:

{
  "outputDirectory": "out"
}

Cloudflare Pages:

wrangler pages deploy out

Plain nginx:

server {
  root /var/www/out;
  index index.html;
  try_files $uri $uri/ $uri/index.html =404;
}

Islands in static output

Islands are fully supported in static output. Each island's initial HTML is pre-rendered into the page (no blank flash), and the hydration bundle is included as a <script> tag pointing to _davaux/client.js. Interactive islands work identically to SSR mode — the client picks up the pre-rendered HTML and hydrates it in place.