---
title: "Static Site Generation"
description: "Deploy your WPNuxt site as a fully static site"
canonical_url: "https://wpnuxt.com/deploy/ssg"
---
# Static Site Generation

> Deploy your WPNuxt site as a fully static site

WPNuxt supports full static site generation, pre-rendering all pages at build time. This produces static HTML files that can be deployed to any CDN or static hosting provider.

## When to Choose SSG

<table>
<thead>
  <tr>
    <th>
      Consideration
    </th>
    
    <th>
      SSG
    </th>
    
    <th>
      SSR
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Content updates
    </td>
    
    <td>
      Requires rebuild
    </td>
    
    <td>
      Instant (with SWR cache)
    </td>
  </tr>
  
  <tr>
    <td>
      Hosting
    </td>
    
    <td>
      Any static host, no server needed
    </td>
    
    <td>
      Requires Node.js
    </td>
  </tr>
  
  <tr>
    <td>
      Performance
    </td>
    
    <td>
      Fastest TTFB (~10-50ms from CDN)
    </td>
    
    <td>
      Fast with caching (~50-200ms)
    </td>
  </tr>
  
  <tr>
    <td>
      Build time
    </td>
    
    <td>
      Grows with content volume
    </td>
    
    <td>
      N/A
    </td>
  </tr>
  
  <tr>
    <td>
      Preview support
    </td>
    
    <td>
      Limited
    </td>
    
    <td>
      Full support
    </td>
  </tr>
  
  <tr>
    <td>
      Cost
    </td>
    
    <td>
      Cheapest (static hosting)
    </td>
    
    <td>
      Depends on compute
    </td>
  </tr>
</tbody>
</table>

**Choose SSG** for blogs, documentation, and marketing sites where content changes infrequently and you can trigger rebuilds on publish.

**Choose SSR** for sites with many pages, frequent updates, preview requirements, or authenticated content.

## Configuration

Set the Nitro preset to `static` and configure pre-rendering:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  modules: ['@wpnuxt/core'],

  nitro: {
    preset: 'static',
    prerender: {
      concurrency: 10,   // Parallel route fetches
      interval: 1000,     // Delay between batches (avoid overwhelming WordPress)
      failOnError: false, // Don't fail build on individual route errors
      routes: ['/'],      // Seed route for crawling
    },
  },

  wpNuxt: {
    wordpressUrl: 'https://your-wordpress.com',
  },
})
```

## Pre-rendering WordPress Routes

Nuxt's crawler discovers routes by following links from the seed routes. For WordPress sites, it's more reliable to fetch all routes explicitly using the `prerender:routes` hook:

```ts [nuxt.config.ts]
const WORDPRESS_URL = 'https://your-wordpress.com'
const GRAPHQL_ENDPOINT = '/graphql'

export default defineNuxtConfig({
  modules: ['@wpnuxt/core'],

  nitro: {
    preset: 'static',
    prerender: {
      concurrency: 10,
      interval: 1000,
      failOnError: false,
      routes: ['/'],
    },
  },

  hooks: {
    async 'prerender:routes'(ctx) {
      await fetchWordPressRoutes(ctx.routes)
    },
  },

  wpNuxt: {
    wordpressUrl: WORDPRESS_URL,
  },
})

async function fetchWordPressRoutes(routes: Set<string>) {
  console.log('[wpnuxt] Fetching WordPress routes for prerendering...')

  try {
    const response = await fetch(`${WORDPRESS_URL}${GRAPHQL_ENDPOINT}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        query: `
          query AllContentForPrerender {
            posts(first: 100) { nodes { uri } }
            pages(first: 100) { nodes { uri } }
          }
        `,
      }),
    })

    const data = await response.json() as {
      data?: {
        posts?: { nodes: Array<{ uri: string }> }
        pages?: { nodes: Array<{ uri: string }> }
      }
    }

    const posts = data.data?.posts?.nodes || []
    const pages = data.data?.pages?.nodes || []

    for (const post of posts) {
      if (post.uri) routes.add(post.uri)
    }
    for (const page of pages) {
      if (page.uri) routes.add(page.uri)
    }

    console.log(`[wpnuxt] Added ${posts.length} posts and ${pages.length} pages`)
  } catch (error) {
    console.warn('[wpnuxt] Failed to fetch WordPress routes:', error)
  }
}
```

<callout type="info">

If you have more than 100 posts or pages, use GraphQL pagination (`after` cursor) to fetch all content, or increase the `first` parameter.

</callout>

## URI Normalization

WordPress URIs typically include trailing slashes (e.g., `/hello-world/`). This is important for SSG because the generated file path depends on the URI:

- `/hello-world/` generates `hello-world/index.html`
- `/hello-world` generates `hello-world.html`

Ensure consistency between your WordPress permalink structure and your Nuxt routing. The `prerender:routes` hook above uses URIs directly from WordPress, maintaining the correct format.

If you're using `useNodeByUri()` with `route.path`, note that Nuxt's `route.path` does not include a trailing slash by default. You may need to normalize:

```ts
const uri = route.path.endsWith('/') ? route.path : `${route.path}/`
const { data } = await useNodeByUri({ uri })
```

## CI/CD Considerations

### Schema Download

The GraphQL schema must be available at build time. In CI environments where WordPress may not be accessible:

```ts [nuxt.config.ts]
const IS_CI = process.env.CI === 'true'

export default defineNuxtConfig({
  wpNuxt: {
    wordpressUrl: 'https://your-wordpress.com',
    downloadSchema: !IS_CI, // Skip in CI, commit schema.graphql instead
  },
})
```

If you skip schema download in CI, commit `schema.graphql` to your repository.

### Route Fetching

The `prerender:routes` hook fetches routes from WordPress at build time. Ensure WordPress is accessible from your CI environment:

```ts [nuxt.config.ts]
const IS_CI = process.env.CI === 'true'
const IS_VERCEL = !!process.env.VERCEL

export default defineNuxtConfig({
  hooks: {
    async 'prerender:routes'(ctx) {
      // Fetch on Vercel (has internet) or locally, skip in restricted CI
      if (IS_VERCEL || !IS_CI) {
        await fetchWordPressRoutes(ctx.routes)
      }
    },
  },
})
```

### Environment Variables

Set `WPNUXT_WORDPRESS_URL` in your CI/CD environment:

```bash
# GitHub Actions example
env:
  WPNUXT_WORDPRESS_URL: https://your-wordpress.com
```

## Deploying

### Vercel

Vercel automatically detects static Nuxt sites. See the [Vercel deployment guide](https://wpnuxt.com/deploy/vercel) for details.

```json [vercel.json]
{
  "buildCommand": "pnpm run build",
  "outputDirectory": ".output/public"
}
```

### Netlify

```toml [netlify.toml]
[build]
  command = "pnpm run build"
  publish = ".output/public"
```

### Cloudflare Pages

```toml [wrangler.toml]
[site]
  bucket = ".output/public"
```

Or configure in the Cloudflare dashboard:

- **Build command:** `pnpm run build`
- **Build output directory:** `.output/public`

### Any Static Host

Run `pnpm run build` and upload the `.output/public/` directory to any static hosting provider (AWS S3, GitHub Pages, Firebase Hosting, etc.).

## Triggering Rebuilds

Since static sites don't update automatically, trigger a rebuild when content changes in WordPress:

1. **WordPress webhook** — Use a plugin like [WP Webhooks](https://wordpress.org/plugins/wp-webhooks/) to send a POST request to your hosting provider's deploy hook when content is published
2. **Scheduled builds** — Configure your CI to rebuild on a schedule (e.g., every hour)
3. **Manual** — Trigger a rebuild from your hosting provider's dashboard

Most hosting providers (Vercel, Netlify, Cloudflare) support deploy hooks — a URL that triggers a new build when called.


## Sitemap

See the full [sitemap](https://wpnuxt.com/sitemap.md) for all pages.
