Deploying your site

Ship your build folder to Cloudflare Pages, Netlify, Vercel or GitHub Pages, by hand or on every push.

nitro build turns your project into a folder of plain static files. There's no server to run and nothing to configure at request time, so any static host can serve it. This guide covers the three hosts nitro deploy supports out of the box, GitHub Pages, and running all of it from GitHub Actions.

What nitro build gives you

nitro build

Everything lands in build/:

  • One HTML file per page. src/pages/about.py becomes build/about.html, and dynamic routes write one file per path.
  • Minified HTML and CSS, with content hashes in CSS and JS filenames, such as main.3d10a61c76f3.css. Turn hashing off with --no-fingerprint.
  • sitemap.xml and robots.txt, built from your pages and the base_url in nitro.config.py.
  • Everything in src/public/, copied to the root of build/ as is. That's where host config files like _headers, _redirects and vercel.json go.

The folder is wiped at the start of every build, so a deleted or renamed page never lingers in a deploy.

Set base_url to your real domain before you deploy. The sitemap uses it, and so does anything else that needs absolute links, like an RSS feed.

Clean URLs

Nitro builds about.html, but you'll want to link to /about. Whether that works depends on the host:

Hostnitro deployClean URLsHeaders and redirects
Cloudflare PagesYesYes, and /about.html redirects to /about_headers, _redirects
NetlifyYesYes_headers, _redirects
VercelYesOnly with cleanUrls turned onvercel.json
GitHub PagesNoYesNot supported

Deploy with nitro deploy

nitro deploy builds your site, then hands build/ to the host's own CLI:

nitro deploy --platform cloudflare --prod
OptionWhat it does
--platformnetlify, vercel or cloudflare
--prodPublish to production. Without it you get a preview deploy
--no-buildDeploy the existing build/ folder without rebuilding
--verboseShow the host CLI's output as it runs

Always pass --platform. Without it, Nitro looks for netlify.toml, vercel.json or wrangler.toml in your project root, then for an installed netlify, vercel or wrangler CLI, in that order. If you have the Netlify CLI installed for another project, Netlify wins.

Use --verbose the first time you deploy to a new host. Nitro hides the host CLI's output by default, so if the CLI stops to ask you a question, all you'd see is a spinner that never finishes.

Cloudflare Pages

Install Wrangler and log in:

npm install -g wrangler
wrangler login

Nitro names the Cloudflare project after your project folder, lowercased, with spaces and underscores turned into hyphens. Create a project with that name once:

wrangler pages project create my-site --production-branch main

Then deploy:

nitro deploy --platform cloudflare --prod

--prod deploys to the main branch, which is your production site if main is the production branch. Without --prod, Wrangler uses the branch you have checked out, so a deploy from a feature branch becomes a preview with its own URL.

Netlify

Install the Netlify CLI, log in and link your project to a site. Run these from your project root:

npm install -g netlify-cli
netlify login
netlify link

netlify link connects the folder to an existing site, and writes the site ID to .netlify/state.json. To create a new site, run netlify sites:create first.

nitro deploy --platform netlify          # draft URL to check first
nitro deploy --platform netlify --prod   # live site

Vercel

Install the Vercel CLI and log in:

npm install -g vercel
vercel login

Vercel doesn't serve about.html at /about unless you turn on clean URLs. Create src/public/vercel.json, so it ends up at the root of the folder Vercel receives:

{
  "cleanUrls": true
}

Then deploy:

nitro deploy --platform vercel --prod

Keep vercel.json in src/public/, not the project root. Vercel reads it from the root of the deployed folder, which is build/. It does mean Nitro can't use the file to auto-detect Vercel, which is one more reason to pass --platform.

Deploy from GitHub Actions

Building in CI means every push to main goes live, and nobody needs host credentials on their laptop. Each workflow below goes in .github/workflows/deploy.yml and assumes your dependencies, including nitro-cli, are listed in requirements.txt.

Cloudflare Pages

This is the workflow that deploys nitro.sh. Add CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID as repository secrets, and create the Pages project first, as above.

name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      deployments: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - run: nitro build
      - uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          command: pages deploy build --project-name=my-site --commit-dirty=true

Netlify

CI has no .netlify/state.json, so the CLI needs the site ID as well as a token. Add NETLIFY_AUTH_TOKEN, a personal access token, and NETLIFY_SITE_ID, the project ID from your site's settings, as repository secrets.

name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - run: nitro build
      - run: npm install -g netlify-cli
      - run: netlify deploy --dir build --prod
        env:
          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
          NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}

GitHub Pages

GitHub Pages has no CLI for nitro deploy, but it has its own Actions. In your repository settings, under Pages, set the source to GitHub Actions, then add:

name: Deploy

on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: pages
  cancel-in-progress: true

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - run: nitro build
      - uses: actions/upload-pages-artifact@v4
        with:
          path: build

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - id: deployment
        uses: actions/deploy-pages@v4

Watch the URL. A repository called my-site is served at username.github.io/my-site/, so a link to /about points at username.github.io/about and breaks. Either give the site a custom domain under Pages in your settings, or name the repository username.github.io, which is served from the root.

Caching and headers

Because nitro build puts a content hash in every CSS and JS filename, a changed stylesheet always gets a new URL. That makes it safe to tell browsers to keep those files for a year. On Cloudflare Pages and Netlify, add src/public/_headers:

/assets/styles/*
  Cache-Control: public, max-age=31536000, immutable

/assets/js/*
  Cache-Control: public, max-age=31536000, immutable

/*
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin

If you ever build with --no-fingerprint, remove the first two blocks. Otherwise visitors keep last year's CSS long after you've changed it.

Things to watch for

Keep the output folder name

nitro deploy uploads the folder named by build_dir in nitro.config.py, which defaults to build. If you change it, change the build path in your CI workflow too.

Add a 404 page

Every host above serves 404.html from the root of your site when a page doesn't exist. Create src/pages/404.py and Nitro builds it like any other page.

Previews before production

Leave off --prod while you're checking a change. Netlify gives you a draft URL, Vercel a preview deployment and Cloudflare a branch preview, and your live site doesn't change until you deploy again with --prod.