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.

Sections of the Azure Static Web Apps configuration file Five parts. trailingSlash sets the URL form. globalHeaders adds security headers to every response. routes handles redirects, per-path cache headers and role requirements. responseOverrides maps 401 to a sign-in redirect and 404 to the custom 404 page. mimeTypes fills gaps in content type detection. What staticwebapp.config.json controls trailingSlash always matches Hugo URLs globalHeaders HSTS, CSP, nosniff every response routes redirects, cache, roles 200 redirects max here responseOverrides 401 sign-in, 404 page status stays 404 mimeTypes .webmanifest fills detection gaps
One JSON file, copied into the build output, replaces _headers and _redirects from other hosts.

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.

Sign-in sequence for an internal documentation page on Azure Static Web Apps A reader requests a page under /internal. Static Web Apps finds no session and returns 401, which the response override turns into a redirect to Entra ID sign-in. After sign-in, the platform calls the GetRoles function, which assigns the employee role for users from the company tenant. The original page is then served. Public pages never involve sign-in. Reaching an internal page Reader browser Static Web Apps edge Entra ID company tenant GetRoles managed function GET /internal/runbooks/ 401 → redirect to sign-in sign in roles for user? → employee serve internal page
Role assignment runs once per sign-in; page requests afterwards are ordinary static responses with a session cookie.

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:

MeasureAzure Static Web AppsCloudflare Workers static assets
TTFB p75, Europe95 ms84 ms
TTFB p75, Asia-Pacific210 ms120 ms
Deploy time (upload 2,300 pages)1 min 40 s29 s
Pull request previewsyes (staging environments)yes (versions)
Built-in Entra ID sign-inyesvia Cloudflare Access
Redirect rules in configuration~200 practical2,000 static + 100 dynamic
Time to first byte on Azure Static Web Apps and Cloudflare Workers Paired bars of TTFB at the 75th percentile. In Europe, Azure Static Web Apps 95 milliseconds and Cloudflare 84 milliseconds. In North America, 130 and 96 milliseconds. In Asia-Pacific, 210 and 120 milliseconds. TTFB p75 by region Azure Static Web Apps Cloudflare Workers assets Europe 95 ms 84 ms North America 130 ms 96 ms Asia-Pacific 210 ms 120 ms
Same build on both hosts, synthetic checks from six regions over two weeks plus RUM for Europe.

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

MeasurePrevious (IIS on a VM)Azure Static Web Apps
Deploy processmanual file copyGitHub Actions on merge
Pull request previewsnoneevery pull request
Internal section access controlIP allowlistEntra ID sign-in, tenant only
TTFB p75, Europe340 ms95 ms
Monthly infrastructure costVM + maintenancefixed plan fee, no servers

Pitfalls & Rollback

  • navigationFallback on 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.

Filed underHow-tos