---
url: https://void.cloud/guide/ssg.md
description: >-
  Prerender every page at build time by setting the static output mode in
  void.config.ts.
---

# Static Site Generation (SSG)

Set `output: "static"` in `void.config.ts` to prerender all pages at build time:

```json
{
  "output": "static"
}
```

When `output` is `"static"`:

* Pages default to `prerender = true` and are written as HTML files to `dist/client/`.
* Use `export const prerender = false` in a page's `.server.ts` to opt out. That page will be server-rendered on request.
* Dynamic pages without `getPrerenderPaths()` are implicitly not prerendered (the paths aren't known at build time).
* The build output is self-contained and works for direct Cloudflare deployment, self-hosting, or `void deploy`.

## Trailing slashes and directory indexes

For hosts such as GitHub Pages, enable trailing-slash Pages URLs and directory-style HTML output:

```ts
import { defineConfig } from 'void/config';

export default defineConfig({
  output: 'static',
  routing: { trailingSlash: true },
});
```

`/posts/` is written to `posts/index.html`, and `/posts/hello/` to `posts/hello/index.html`. Pages URLs without the slash redirect to their trailing-slash URL in development and when served by Void. Use trailing slashes in your page links; generated route types follow this setting. API routes keep their existing URLs.

Omitting the option or setting it to `false` preserves the default `posts.html` output. Static builds keep `404.html` at the output root. On Cloudflare, use the default HTML handling or `force-trailing-slash`; `drop-trailing-slash` conflicts with this option. Third-party frameworks retain their own URL configuration.

## Per-page overrides

Individual pages can opt out of prerendering:

```ts
// pages/dashboard.server.ts
export const prerender = false;
```

Dynamic pages with `getPrerenderPaths()` are prerendered for the returned param combinations. Annotate the return type with [`PageParams`](../reference/api/types.md#route-types) so TypeScript checks the param names. Void does not infer them from the filename:

```ts
// pages/blog/[slug].server.ts
import type { PageParams } from 'void/routes';

export async function getPrerenderPaths(): Promise<PageParams<'/blog/[slug]'>[]> {
  return [{ slug: 'hello-world' }, { slug: 'getting-started' }];
}
```

Each object must include every param in the page path. Values can be strings or numbers, and Void URL-encodes them. A single-segment param such as `[slug]` cannot contain `/`. For nested paths, use a catch-all page such as `pages/docs/[...slug]` and return `{ slug: 'guides/intro' }`. If an entry is invalid, the build fails. The error names the server file and the entry index.

## 404 pages

Static builds that prerender at least one page include a default `404.html` in `dist/client/`. To provide your own, add `public/404.html` or prerender a page at `/404`.

Fully static Void deployments serve this page for missing URLs with an HTTP 404 status by default. You can override this with [`routing.notFound`](../reference/config.md#routing-notfound). Hybrid deployments handle missing URLs in the Worker.

When hosting `dist/client/` yourself, configure your host to serve `404.html` with a 404 status for missing pages.

## Comparison with edge prerendering

| `output` value       | Default prerender | Per-page override                | Prerender timing           |
| -------------------- | ----------------- | -------------------------------- | -------------------------- |
| `"server"` (default) | `false`           | `export const prerender = true`  | Deploy-time (platform ISR) |
| `"static"`           | `true`            | `export const prerender = false` | Build or deploy post-build |

With the default `output: "server"`, use `export const prerender = true` for deploy-time [edge prerendering](./edge/prerendering.md).

## Deployment behavior

`void deploy` chooses the deployment automatically:

* **Fully static:** if every page is prerendered and there are no API routes, middleware, cron jobs, or queues, Void deploys the built files as a static site. With `--platform cloudflare`, pages and files are served without running a Worker. If you use [edge rules](./edge/headers.md) (`routing` headers, redirects, rewrites, fallbacks, or `alwaysUseHttps`) or a `_headers` or `_redirects` file in `public/`, a small Worker runs on every request to apply them.
* **Hybrid:** if any pages are not prerenderable, such as dynamic pages without `getPrerenderPaths()` or pages with `export const prerender = false`, a Worker is deployed to handle those routes at runtime. With `--platform cloudflare`, prerendered pages without a `.server.ts` file are served as static files without running the Worker. API routes, pages with a `.server.ts` file, and dynamic pages run in the Worker. Every page runs through the Worker when the app uses middleware, auth, live updates, edge rules (including a `_headers` or `_redirects` file in `public/`), a custom Vite `base`, a `routing.notFound` setting other than `"none"`, or a dynamic page at the top level of `pages/`, such as `pages/[slug].vue`.

## Relationship to `inference.appType: "static"`

The `inference.appType` field describes app type (SPA, static, void), while `output` controls rendering strategy:

|              | `inference.appType: "static"`                   | `output: "static"`                            |
| ------------ | ----------------------------------------------- | --------------------------------------------- |
| **What**     | Deploy a pre-built static site (no Void plugin) | Prerender a Void app at build time            |
| **Worker**   | None (static assets only)                       | Only for server code and edge rules           |
| **Use case** | VitePress, plain HTML, external SSG tools       | Void apps with mostly or fully static content |
