# Static Hosting

> Serve the build directory from any static host — nginx, S3 + CloudFront, Caddy, and friends.

Htmlpress' build is a plain directory of `.html`, `.css`, `.js`, and asset files. Anything that can serve a directory of files will work. This guide covers the common shapes.

## Build the site

```sh
htmlpress build
```

Output lands in `.htmlpress/build/`. Treat that directory as your document root. `htmlpress preview` serves the same build locally, the way a host will.

## Prerendering vs SPA fallback

Every page is a real file, an `index.html` in a folder named after its source: `guides/build.html` is written to `guides/build/index.html` and served at `/guides/build/`, the way any static host answers a folder URL. There is no client-side routing, so no host needs an SPA fallback.

What is worth configuring is the not-found page. Every build writes a `404.html` at the root (the project's own, or a built-in one); point the host's error page at it so unknown paths get the site's layout instead of the host's default error.

## nginx

```
server {
  listen 80;
  server_name docs.example.com;
  root /var/www/docs;
  index index.html;

  # Try the exact file, then a folder's index.html, else the site's 404 page.
  location / {
    try_files $uri $uri/ =404;
  }
  error_page 404 /404.html;
}
```

Copy `.htmlpress/build/*` to `/var/www/docs/` on each release.

## Caddy

```
docs.example.com {
  root * /var/www/docs
  file_server
  encode gzip zstd
  handle_errors {
    rewrite * /404.html
    file_server
  }
}
```

Caddy auto-provisions Let's Encrypt certificates when the domain resolves to it.

## AWS S3 + CloudFront

The standard pattern is an S3 bucket as origin and CloudFront in front:

1. Upload `.htmlpress/build/` to an S3 bucket (`aws s3 sync .htmlpress/build/ s3://my-docs --delete`).
2. Set **Static website hosting → Index document** to `index.html` and **Error document** to `404.html`.
3. Create a CloudFront distribution pointing at the bucket's website endpoint.
4. In CloudFront, add a **Custom error response** mapping `404` to `/404.html`, keeping the response code `404`.
5. Set the distribution's default root object to `index.html`.

The files under `_htmlpress/` keep their names from build to build, so cache them for a short time, or invalidate them on each release.

## Apache

```
DirectoryIndex index.html
ErrorDocument 404 /404.html
```

Put these in an `.htaccess` at the docroot and place the build there. Real files and folders are served directly; anything else gets the site's 404 page.

> **Warning**
>
> Htmlpress skips dot-files when it copies a project, so keep the `.htaccess` on the server or copy it in after the build.

## Other hosts

The same recipe works on Render Static, Surge, Fly Static, Bunny, or any object store with a CDN: upload the directory, point a domain at it, and serve `404.html` for unknown paths.

## Deploy with GitHub Actions

A minimal nginx/SSH example:

```yaml
name: deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v4
        with:
          node-version: 24
      - run: npx gethtmlpress build
      - uses: easingthemes/ssh-deploy@v5
        with:
          SSH_PRIVATE_KEY: ${{ secrets.SSH_KEY }}
          REMOTE_HOST: ${{ secrets.SSH_HOST }}
          REMOTE_USER: ${{ secrets.SSH_USER }}
          SOURCE: .htmlpress/build/
          TARGET: /var/www/docs/
          ARGS: -avzr --delete
```

For S3, swap the deploy step for `aws-actions/configure-aws-credentials` + `aws s3 sync`.

## See also

- [Site Metadata](/customization/site-metadata/): `site` powers absolute URLs and the sitemap.
- [Deployment](/deployment/): back to the host index.
