Blog
Deploying Nuxt 4 to Azure Static Web Apps: the configuration that actually worked
· 8 min read · Evan Ritter

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:
- Build the site with
npx nuxi generaterather thannuxt build. - Point the SWA action at
.output/publicas theapp_location. - Set
skip_app_build: trueso 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.
azureandazure-functionsbuild 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-staticbuilds 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 practicenuxi generatewith 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 cirather thannpm install. Faster, deterministic, and fails loudly ifpackage-lock.jsonis out of date — which is what you want in CI.- Environment variables at build time.
nuxi generateruns in CI, so anything fromNUXT_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_requestjob. 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.