Back to blog

Blog

Deploying Nuxt 4 to Azure Static Web Apps: the configuration that actually worked

· 8 min read · Evan Ritter

nuxtazuredeployment
Deploying Nuxt 4 to Azure Static Web Apps: the configuration that actually worked

If you've landed here from a search engine at an unsociable hour because your Nuxt 4 deployment to Azure Static Web Apps is failing in a way that none of the existing tutorials describe — skip to the next section. The configuration is there. You can come back for the explanation later.

For everyone else: Nuxt 4 and Azure Static Web Apps are both perfectly reasonable choices, and they go together perfectly well, but the path between them is paved with stale documentation. Most of what you'll find online assumes Nuxt 3, uses the wrong Nitro preset, or relies on the SWA GitHub Action's default build behaviour in ways that quietly break when you upgrade. The working setup is small, but the path to it isn't obvious. This post is the post I wish I'd found.

The TL;DR config

Three things matter:

  1. Build the site with npx nuxi generate rather than nuxt build.
  2. Point the SWA action at .output/public as the app_location.
  3. Set skip_app_build: true so Oryx doesn't try to rebuild what you've already built.

Here's the relevant slice of the GitHub Actions workflow:

- name: Build Nuxt app
  run: npx nuxi generate

- name: Deploy to Azure Static Web Apps
  uses: Azure/static-web-apps-deploy@v1
  with:
    azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
    repo_token: ${{ secrets.GITHUB_TOKEN }}
    action: "upload"
    app_location: ".output/public"
    skip_app_build: true

No custom Nitro preset. No output_location. No api_location unless you actually have a separate API. If you only need a working deploy, that's the answer — bookmark this and get some sleep.

Why this combination is awkward in 2026

The friction isn't anyone's fault. It's the sum of three reasonable design decisions that don't quite line up.

Nuxt 4 changed the build output structure. The .output directory is now the canonical place where everything lands, with .output/public for static assets and .output/server for the Nitro server bundle when there is one. For a fully static deploy — which is what SWA wants — .output/public is the directory you care about. Older guides point at .nuxt/dist, dist/, or public/. None of those are right for Nuxt 4.

Nitro has a long list of deployment presets, including azure, azure-functions, and azure-static. For a static-generated site going to Static Web Apps, you don't need any of them. Setting one will either build a server bundle you don't want, or build for an Azure target that doesn't match the SWA action's expectations. The cleanest answer is to leave the preset alone and run nuxi generate, which produces static output by default.

The SWA GitHub Action wants to be helpful. By default, when you point it at a source directory it runs Oryx, Microsoft's auto-build system, to detect your framework and build the site for you. Oryx's Nuxt support lags behind current Nuxt releases by enough that, at the time of writing, it will quietly produce broken output for a Nuxt 4 project. The fix isn't to teach Oryx about Nuxt 4; it's to build the site yourself in an earlier step and then tell the action not to bother by setting skip_app_build: true.

Each of those decisions is sensible in isolation. Together, they mean the obvious thing — point the SWA action at your repo and let it figure things out — doesn't work, and the fix involves disabling helpful defaults across two different systems.

The Nitro preset question

Nitro is the server engine underneath Nuxt 3 and 4. It produces deployable output tailored to a target platform — a Vercel build for Vercel, a Cloudflare Workers build for Cloudflare, a Node server for traditional hosting, and so on. You pick the target with a preset, either via the NITRO_PRESET environment variable or in nuxt.config.ts:

export default defineNuxtConfig({
  nitro: {
    preset: 'node-server'
  }
})

For Azure specifically, Nitro ships three presets: azure, azure-functions, and azure-static. The names look like they map cleanly to Azure services. They don't, quite.

  • azure and azure-functions build a server bundle deployed as Azure Functions. Useful for SSR scenarios on Static Web Apps' managed Functions backend or on Azure Functions directly. Not what you want for a static site.
  • azure-static builds for Static Web Apps' static hosting. Closer to what you want, but it produces output in a structure tailored to the SWA action's older expectations, and in practice nuxi generate with no preset is simpler and produces output that the action handles cleanly.

If you don't need server-side rendering and you're happy with a fully static site — which is the common case for marketing sites, documentation, blogs, and most of what SWA is good at — then nuxi generate is the right command and you can ignore Nitro presets entirely. Nuxt will produce static HTML for every route it can pre-render, drop it in .output/public, and that's the directory the SWA action uploads.

If you do need SSR, Static Web Apps isn't the wrong choice, but the configuration is meaningfully different and worth a separate post. Most of the time, ask yourself whether you actually need SSR before reaching for it. For a content site rendered from Nuxt Content, you don't.

The app_location trap

The single most confusing parameter in the SWA action is app_location. The documentation describes it as "the location of your application code" — which is technically correct and practically misleading.

What app_location actually does depends on whether Oryx is going to build your app. If skip_app_build is false (the default), Oryx looks at app_location for source code to build. If skip_app_build is true, the action treats app_location as the directory of already-built output to upload.

So the same parameter means two different things depending on a sibling parameter. The combination you want for Nuxt 4 is:

app_location: ".output/public"
skip_app_build: true

Which reads as: "Don't build anything. The static output is already sitting in .output/public. Upload that."

You'll see older guides set app_location: "/" and output_location: "dist" and let Oryx do the build. That route can work for simple cases, but it leaves you at the mercy of Oryx's framework detection, which is the thing that breaks on every Nuxt major version bump. Building the site yourself in an earlier run step and pointing the action at the finished output is more verbose by one line and dramatically more predictable.

The full working GitHub Actions workflow

Here's a complete, annotated workflow file. This is what's actually running in production for the kind of Nuxt 4 sites this post is about:

name: Deploy to Azure Static Web Apps

on:
  push:
    branches:
      - main
  pull_request:
    types: [opened, synchronize, reopened, closed]
    branches:
      - main

jobs:
  build_and_deploy:
    if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
    runs-on: ubuntu-latest
    name: Build and deploy
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          submodules: true

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Build Nuxt app
        run: npx nuxi generate
        env:
          NUXT_PUBLIC_SITE_URL: ${{ vars.SITE_URL }}

      - name: Deploy to Azure Static Web Apps
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
          repo_token: ${{ secrets.GITHUB_TOKEN }}
          action: "upload"
          app_location: ".output/public"
          skip_app_build: true

  close_pull_request:
    if: github.event_name == 'pull_request' && github.event.action == 'closed'
    runs-on: ubuntu-latest
    name: Close pull request
    steps:
      - name: Close Pull Request
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
          action: "close"

A few details worth noting:

  • Node 22. Nuxt 4 supports Node 20 and above; pin to a specific major version rather than letting the runner default drift.
  • npm ci rather than npm install. Faster, deterministic, and fails loudly if package-lock.json is out of date — which is what you want in CI.
  • Environment variables at build time. nuxi generate runs in CI, so anything from NUXT_PUBLIC_* that needs to end up in the static bundle has to be injected here, either from GitHub Actions variables (non-sensitive) or secrets (sensitive). This is a common stumbling block: setting environment variables in the SWA portal does nothing for a statically generated site, because the build happened before deployment.
  • The close_pull_request job. SWA gives you preview environments per PR for free. The close job tears them down when the PR closes. Worth keeping.

Common failure modes and what they mean

A short field guide to the errors you're most likely to hit.

"Oryx build failed" or "App Directory Location not found" You either haven't set skip_app_build: true, or app_location doesn't point at a directory that exists when the action runs. Check that your build step ran successfully and produced .output/public.

Deployment succeeds but the site is blank Almost always means app_location is pointing at the wrong directory — typically .output instead of .output/public, which uploads the server bundle alongside the static files and confuses SWA's routing.

404s when refreshing on a sub-route SWA doesn't know about your client-side routes by default. Add a staticwebapp.config.json to the root of your public/ directory with a fallback rule:

{
  "navigationFallback": {
    "rewrite": "/index.html",
    "exclude": ["/assets/*", "/*.{css,js,png,jpg,svg,ico}"]
  }
}

This file is copied through to .output/public during the build and tells SWA to serve index.html for any route that doesn't match a static file.

Environment variables are undefined in the deployed site You set them in the SWA portal, expecting them to be available at runtime. For a fully static site there is no runtime. Inject the variables at build time in the workflow instead.

Build works locally, fails in CI Usually a Node version mismatch or a missing environment variable. Pin Node in the workflow and double-check that every NUXT_PUBLIC_* your build reads is set in the env: block of the build step.

When to use SWA, Container Apps, or App Service

A quick frame for picking the right Azure service, since this is the question that usually sits behind "how do I deploy Nuxt to Azure":

  • Static Web Apps for static-generated sites. Marketing sites, documentation, blogs, anything where every route can be pre-rendered. Cheap, fast, free preview environments, generous free tier. The sweet spot for Nuxt Content sites.
  • Container Apps for SSR or anything that needs a long-running server process. You get full control of the Node version, the container image, and the scaling behaviour. Pricier than SWA but the right answer when you genuinely need rendering at request time.
  • App Service if you're already there for other reasons — an existing .NET application, a team that knows it well, or platform requirements that lean that way. Workable for Node but rarely the best fit for a greenfield Nuxt project.

For the Nuxt 4 site this post is about, Static Web Apps is the right answer. If you find yourself fighting SWA to do something it doesn't want to do — server-side data fetching at request time, long-running background jobs, anything stateful — the answer isn't a cleverer SWA configuration. It's Container Apps.

A closing wish

The configuration above works and will keep working. But it shouldn't take a blog post to find it. A first-class nuxt-azure-swa preset in Nitro, or a Nuxt 4-aware build path in Oryx, would mean none of this was necessary. Until one of those exists, point the SWA action at .output/public, skip the app build, and ship.

If you've hit a Nuxt + Azure issue this post doesn't cover, get in touch — I'd rather update this with another working answer than leave the next person searching at midnight.