# Static routes and 404s

A generated content site should serve each route's HTML and return HTTP 404 for a missing page. New lizardpack builds for Astro static, Docusaurus, VitePress, Hugo, and SvelteKit adapter-static use that policy. React and Vue SPAs keep the `index.html` fallback needed by browser routers.

The custom configuration below is optional. Use it for a routing policy that the detected framework does not provide. Existing images keep their old rules until you rebuild them.

## Choose the routing policy

| App type | Route behavior |
|---|---|
| React or Vue SPA | A valid client route loads `index.html`, then the client router renders it. |
| Generated documentation or content | A route resolves to its generated HTML; an unknown path returns HTTP 404. |
| Server-rendered app | The application server resolves routes and returns the status. |

An SPA catch-all view can show a missing-page message, but browser code cannot change the HTTP status of the HTML response already sent. If you need route-aware HTTP status for an SPA, use a server or prerendered route setup that knows which paths exist.

## Configure nginx for generated pages

Create `nginx.conf` at the application root:

```nginx
server {
  listen 80;
  server_name _;
  root /usr/share/nginx/html;
  index index.html;

  location / {
    try_files $uri $uri.html $uri/ =404;
  }

  error_page 404 /404.html;
  location = /404.html {
    internal;
  }
}
```

This supports both `guide.html` and `guide/index.html` output layouts. Include a generated `404.html` if you want a custom error page. Otherwise omit the `error_page` and exact-location blocks to use nginx's default error body. Keep one canonical URL style in the site's links and sitemap; this lookup rule does not add canonical redirects.

## Build the site with the config

Use this complete Dockerfile for an npm project with a committed lockfile. Change `BUILD_SCRIPT` and `OUTPUT_DIR` defaults to match the table below:

```dockerfile
FROM node:22-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
ARG BUILD_SCRIPT=build
RUN npm run "$BUILD_SCRIPT"

FROM nginx:alpine
ARG OUTPUT_DIR=dist
COPY --from=build /app/${OUTPUT_DIR}/ /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
```

| Site | `BUILD_SCRIPT` | `OUTPUT_DIR` |
|---|---|---|
| Astro static | `build` | `dist` |
| Docusaurus | `build` | `build` |
| VitePress with a `docs/` root | `docs:build` | `docs/.vitepress/dist` |
| VitePress at the project root | `docs:build` or your actual script | `.vitepress/dist` |
| SvelteKit with adapter-static | `build` | `build` |
| Next.js export | `build` | `out` |
| Nuxt generate | `build` with `nuxt generate` | `.output/public` |

Use the generated-pages policy only when the app has those route files. A SvelteKit SPA fallback, for example, needs its own routing policy. [Next.js static export](https://lizard.build/docs/framework-guides/nextjs/static-export) has a complete recipe with a trailing-slash layout.

Exclude dependencies, build output, `.git`, and `.env*` in `.dockerignore`. If the build needs public variables, declare their `ARG` values before the build command. Do not copy private secrets into the image or browser output.

After [CLI setup](https://lizard.build/docs/framework-guides#prepare-the-project), create a service with `lizard add --service web`, then deploy this source with `lizard up --service web --port 80`. Skip the add step when the service already exists. On an existing service, check [build decision order](https://lizard.build/docs/concepts/build-pipeline#build-decision-order): command overrides can take precedence over the Dockerfile. A config file by itself does not replace the generated nginx configuration; the Dockerfile must copy it into the image.

## Verify HTTP responses

Set `SITE_URL` to the actual local test URL or deployed origin, then replace `/guide/` with a page that exists:

```bash
SITE_URL=https://YOUR_PUBLIC_HOST
curl -sS -o /dev/null -w '%{http_code}\n' "$SITE_URL/"
curl -sS -o /dev/null -w '%{http_code}\n' "$SITE_URL/guide/"
curl -sS -o /dev/null -w '%{http_code}\n' "$SITE_URL/this-page-does-not-exist"
```

Expect 200 for real pages and 404 for the missing path. If a valid route redirects to its canonical form, inspect the redirect and then verify the destination. Also test an actual asset and a made-up `.js` path: a missing script must not return the home page as HTML with status 200.

Finally, open the inner route directly in a browser and reload it. Correct client navigation alone can hide a server routing error. Check the framework's canonical URL and sitemap settings alongside HTTP status before publishing an indexed site.

See [tested versions and cloud results](https://lizard.build/docs/framework-guides/validation) for the September 9, 2026 deployment checks and their limits.

