Hosting on Azure Static Web Apps
Plenty of organisations run their infrastructure on Azure and want their documentation there too: inside the same subscription and billing, behind the same identity provider, under the same compliance controls. Azure Static Web Apps is Microsoft's managed host for static sites, with a global CDN, pull request staging environments, built-in authentication with Entra ID, and optional serverless APIs. It does the essentials well, and it has its own configuration model and a few surprises for teams coming from Netlify or Cloudflare.
This guide deploys an internal-and-public documentation setup to Azure Static Web Apps: a Hugo site built in GitHub Actions, routes, headers and error pages in staticwebapp.config.json, per-pull-request staging environments, a custom domain, Entra ID sign-in for the internal section, and measured performance. It is part of Self-Hosting Static Sites on S3, Nginx and Caddy, which covers running static sites on infrastructure you control.
Prerequisites
- An Azure subscription and a Static Web Apps resource (Standard plan for custom authentication and more staging environments).
- A GitHub repository; the Azure portal can create the deploy workflow and store a deployment token as a secret.
- A Hugo site (any generator works; the build runs in Actions).
Step 1: Build in Actions, Upload the Output
The generated workflow uses the Azure/static-web-apps-deploy action, which can build with Oryx or upload a prebuilt folder. Building yourself gives control over the Hugo version and caching:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: peaceiris/actions-hugo@v3
with: { hugo-version: '0.134.3', extended: true }
- run: hugo --gc --minify
- 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: public
skip_app_build: true
output_location: ''
skip_app_build: true uploads public/ as is. On pull requests, the same action creates a staging environment and posts its URL as a comment; a second job with action: close removes it when the pull request closes.
Step 2: Routes, Headers and Error Pages
staticwebapp.config.json in the output root configures routing. Hugo copies it from static/:
{
"trailingSlash": "always",
"globalHeaders": {
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Content-Security-Policy": "default-src 'self'; img-src 'self' data:; frame-ancestors 'none'"
},
"routes": [
{ "route": "/docs/installation.html", "redirect": "/guide/install/", "statusCode": 301 },
{ "route": "/assets/*", "headers": { "Cache-Control": "public, max-age=31536000, immutable" } },
{ "route": "/internal/*", "allowedRoles": ["employee"] }
],
"responseOverrides": {
"401": { "redirect": "/.auth/login/aad?post_login_redirect_uri=.referrer", "statusCode": 302 },
"404": { "rewrite": "/404.html" }
},
"mimeTypes": { ".webmanifest": "application/manifest+json" }
}
Two surprises for teams used to other hosts. First, routes are limited in number (the configuration file has a size limit), so a site with thousands of migration redirects cannot list them all here; those were handled by generating Hugo alias pages for the rarely used ones and keeping only the most-visited 200 as real redirects. Second, navigationFallback is for single-page apps; a static site must not set it, or missing pages return the home page with a 200 instead of a 404.
Step 3: Entra ID Sign-In for the Internal Section
The /internal/ section — runbooks for support staff — requires sign-in. With the Standard plan, a custom Entra ID app registration limits sign-in to the company tenant:
"auth": {
"rolesSource": "/api/GetRoles",
"identityProviders": {
"azureActiveDirectory": {
"registration": {
"openIdIssuer": "https://login.microsoftonline.com/<tenant-id>/v2.0",
"clientIdSettingName": "AAD_CLIENT_ID",
"clientSecretSettingName": "AAD_CLIENT_SECRET"
}
}
}
}
A small managed function at /api/GetRoles assigns the employee role to users from the tenant; the route rule allowedRoles: ["employee"] does the rest. Public pages remain anonymous and cacheable; internal pages require a session. The same idea on other platforms is covered in Protecting a Static Site Behind Authentication.
Step 4: Custom Domain and Caching
Custom domains are added in the portal or with the Azure CLI; the platform issues and renews certificates automatically for subdomains, and apex domains need an Azure DNS alias record or a TXT validation. Static Web Apps serves content from its global edge network; for more control over caching rules and a WAF, Azure Front Door can sit in front on the Standard plan ("enterprise-grade edge").
Cache headers from routes apply at the browser; the platform's own edge caching is managed for you and purged on each deploy. HTML was left with the platform default (short caching with revalidation), which meant no purge step was needed after deploys.
Step 5: What It Costs and How It Performs
The site — 2,300 pages, about 40,000 page views a day — ran on the Standard plan at a flat monthly fee per app plus bandwidth above the included quota. Measured with RUM and synthetic checks from six regions against the same site on Cloudflare Workers static assets:
| Measure | Azure Static Web Apps | Cloudflare Workers static assets |
|---|---|---|
| TTFB p75, Europe | 95 ms | 84 ms |
| TTFB p75, Asia-Pacific | 210 ms | 120 ms |
| Deploy time (upload 2,300 pages) | 1 min 40 s | 29 s |
| Pull request previews | yes (staging environments) | yes (versions) |
| Built-in Entra ID sign-in | yes | via Cloudflare Access |
| Redirect rules in configuration | ~200 practical | 2,000 static + 100 dynamic |
For this organisation's mostly European internal audience, Azure's numbers were good, and the integrated Entra ID sign-in and Azure billing decided the choice. For a global public site, the edge network difference in Asia-Pacific would weigh more.
Operating It
The deployment token is the one long-lived secret; it is scoped to the single app and rotated every 90 days from the portal, with the GitHub secret updated in the same change. Staging environments count against plan limits, so the close job on pull request closure matters, and a weekly job deletes any left behind. Application Insights, enabled on the app, records the managed function's invocations and errors; static requests are visible in the platform's metrics. The redirect and header checks from Testing Redirects and Headers Before Deploy run against each staging environment, which caught the navigationFallback mistake — a copied sample configuration had enabled it — before production.
Infrastructure as code is worth adopting from the start: the Static Web Apps resource, its custom domain and the Entra ID app registration are defined in Bicep, so a second environment or a disaster recovery rebuild is a deployment rather than an afternoon of portal clicks.
Measured Impact
| Measure | Previous (IIS on a VM) | Azure Static Web Apps |
|---|---|---|
| Deploy process | manual file copy | GitHub Actions on merge |
| Pull request previews | none | every pull request |
| Internal section access control | IP allowlist | Entra ID sign-in, tenant only |
| TTFB p75, Europe | 340 ms | 95 ms |
| Monthly infrastructure cost | VM + maintenance | fixed plan fee, no servers |
Pitfalls & Rollback
navigationFallbackon a static site. Turns every 404 into the home page with status 200.- Too many redirect routes. The configuration file has a size limit; plan for the rest.
- Free plan limits. Custom authentication and more staging environments need Standard.
- Forgetting the close job. Staging environments accumulate against the plan limit.
- Rollback: each deployment is immutable; redeploying a previous commit's build restores it, and the custom domain can be moved back to the old host by DNS if needed.
Conclusion
Azure Static Web Apps is a solid managed host for static sites that need to live inside Azure: builds from GitHub Actions, one JSON file for routes, headers and error pages, per-pull-request staging environments, and Entra ID sign-in for internal sections without writing an auth layer. Watch the redirect limit, never enable the single-page-app fallback on a static site, and compare edge performance for your audience's regions before committing a global public site to it.
FAQ
When is Azure Static Web Apps a good fit for a static site?
When the organisation already runs on Azure and needs the site inside its Azure subscription for compliance, billing or identity reasons, especially with Entra ID sign-in for internal documentation. For public sites without those constraints, other managed hosts are usually simpler.
How are redirects and headers configured?
In staticwebapp.config.json at the root of the deployed output, with routes for redirects, rewrites and per-route headers, globalHeaders for headers on every response, responseOverrides for custom error pages, and navigationFallback for single-page apps.
Does Azure Static Web Apps support pull request previews?
Yes. The GitHub Actions and Azure DevOps integrations create a staging environment for each pull request with its own URL, and close it when the pull request closes. The Free plan allows a small number of staging environments; the Standard plan allows more.
Can I restrict an internal docs site to company users?
Yes. The built-in authentication supports Microsoft Entra ID, and routes can require roles such as authenticated or custom roles. The Standard plan allows a custom Entra ID registration to limit sign-in to your tenant.
Related
- Parent: Self-Hosting Static Sites on S3, Nginx and Caddy — hosting on infrastructure you control.
- Static Hosting on Google Cloud Storage — the Google Cloud equivalent.
- Deploying a Static Site to S3 and CloudFront — the AWS equivalent.
- How to Set Up GitHub Actions for Hugo Deployments — the build job.
- Securing Deploy Credentials with GitHub OIDC — reducing long-lived tokens.
Filed underHow-tos