Static Website Hosting
Purpose
Design and stand up a production-ready static website on Azure Static Web Apps — resource naming, Bicep template, deploy scripts, GitHub Actions workflow with the deploy token, and a baseline staticwebapp.config.json — with decisions recorded so the deployment can be reproduced or handed over. Custom domains, DNS and TLS are covered by azure-swa-custom-domains; routes, headers, CSP and live-site edits by static-website-config-and-csp.
When to use
Use this skill when setting up a new static website, migrating one from click-ops to infrastructure-as-code, or adding a second site (subfolder or subdomain) to an existing repo. Apply it when the site needs:
- Repeatable infrastructure deployments (not click-ops)
- Automated deployment on push to main, including sites whose Azure resource does not exist yet
- Clear operator documentation for future maintenance
Also useful when auditing an existing deployment for gaps in IaC coverage or CI hygiene.
Inputs expected
Provide as many of the following as available. Partial inputs are acceptable — the AI should identify gaps and ask structured follow-up questions only where needed.
- Domain name (e.g.
example.com) and preferred primary host (defaultwww) - Hosting platform preference (default: Azure Static Web Apps Free tier) and audience location (drives region)
- Azure subscription and preferred resource group naming convention
- GitHub repository name and branch to deploy from (default:
main) - Static site output folder (default:
/for pre-built HTML, or build output path); whether a second site lives in a subfolder - Security header requirements (default:
nosniff,SAMEORIGIN,same-origin) - Any existing infrastructure to preserve or migrate from
Guiding principles
- Use Infrastructure-as-Code (Bicep) from the start. Click-ops deployments create undocumented state and are hard to reproduce. Even for a Free-tier SWA, a Bicep template takes 30 minutes to write and saves hours on every future change or rebuild.
- Name resources using CAF conventions:
rg-<workload>-<env>for resource groups,stapp-<workload>-<env>for Static Web Apps. - Azure SWA is offered in only five regions (none in Australia); static content is served from a global edge network regardless, so the region choice mainly affects attached Functions. East Asia is the standard pick for Australian audiences.
- For a token-deployed SWA, omit repository properties from the Bicep entirely —
provider: 'GitHub'+repositoryUrlwithout arepositoryTokenadds friction, while a bareMicrosoft.Web/staticSitesdeploys cleanly and is fed by the workflow's deploy token. Gate anycustomDomainschild behind a bool param (if (deployCustomDomain)) because it fails validation until DNS exists. See Token-deployed Bicep inreference.md. - Store the deployment token in GitHub Secrets — never in code. The token grants full deployment access and must be rotated if it is ever exposed. Set it with
gh secret set --bodyon a trimmed variable; piping theazCLI output straight intogh secret setfrom PowerShell corrupted the token and produced "deployment_token provided was invalid" at deploy time. - Gate a not-yet-provisioned deploy job with a repository VARIABLE in the job
if:(vars.X == 'true') — GitHub secrets are NOT available inif:conditions but repository variables are — so the workflow stays skipped (green CI) until you provision the resource and set the variable. - Run a second independent SWA from one repo by deploying only a subfolder: a dedicated GitHub Actions workflow with
app_location: <subfolder>,skip_app_build: true, its OWN deploy-token secret (never the main site's), and apaths:filter so a subfolder change never redeploys the whole site. See Subfolder SWA workflow inreference.md. - Host an authenticated SPA (e.g. a Supabase-auth portal) on its OWN subdomain, not as a subdirectory of the marketing site: a subdomain is a distinct browser origin, so the SPA localStorage/PKCE tokens, CSP, and XSS blast radius are isolated, and it deploys as two independent Static Web Apps (each with its own managed TLS + CI) instead of a reverse-proxy / Front Door subpath. Caveat: a subdomain is NOT a cookie trust boundary (the registrable domain is), so use host-only /
__Host-cookies and never wildcard the CSP to*.yourdomain. - Azure SWA cancels an in-progress deployment when a newer push arrives, producing a GitHub Actions failure notification even though the site deploys correctly from the later commit. Verify no deployment is running (
gh run list --limit 3) before pushing to avoid spurious failure alerts. - Ship a baseline
staticwebapp.config.jsonwith the first deploy — security headers inglobalHeaders(not a/*route), a short globalCache-Control,.xml/.jsonMIME types and an explicit/sitemap.xmlroute — then hand the file tostatic-website-config-and-cspfor caching tiers, hidden files, redirects and CSP. - Custom domains are a separate lifecycle from provisioning: publish DNS, bind and prove TLS with
azure-swa-custom-domainsafter this skill's deploy is green. - In a multi-environment system NEVER deploy a change straight to prod — always deploy to DEV first, validate there, then promote the SAME validated build to prod as a deliberate, separately-approved step. Prod sitting behind dev (dev has validated fixes prod doesn't yet) is the correct state, not drift to "fix" by pushing untested code to prod.
- For a no-build static site whose dev/prod backend switch is a single config file, keep the repo copy permanently prod and serve local dev from a staged scratch COPY (robocopy to temp excluding
.git, overwrite the config with dev values,npx servethe copy): dev credentials become impossible to commit or deploy, and "local = dev, deployed = prod" is structural rather than a discipline. - Deploy a Vite SPA to a SEPARATE prod SWA by building with prod env overrides via a temporary gitignored
.env.production.local(Vite loads.localover.env.production) that you DELETE right after, so an ordinarynpm run buildstays on dev config; swap astaticwebapp.prod.config.json(prod CSP/routes) overdist/staticwebapp.config.jsonbefore uploading. Deploy withnpx @azure/static-web-apps-cli deploy ./dist --deployment-token <t> --env production, fetching the token viaaz staticwebapp secrets list --name <swa> --resource-group <rg> --query properties.apiKey -o tsv.
Process
Confirm inputs — domain, audience region, Azure subscription, GitHub repo, single vs multi-site layout. Ask for anything missing.
Design the resource structure
- Resource group:
rg-<workload>-<env>; SWA:stapp-<workload>-<env>(one per site — marketing,go.<domain>, portal) - Region: East Asia for Australian audiences (edge-served regardless)
- Resource group:
Write the Bicep template (
infra/main.bicep)- Bare
Microsoft.Web/staticSiteswithsku: { name: 'Free', tier: 'Free' },properties: {}— no repository properties - Optional
customDomainschild behindif (deployCustomDomain), defaultfalse - Use
existingif the resource already exists; outputsdefaultHostname,siteName,resourceGroupName
- Bare
Write the parameters file (
infra/parameters/prod.parameters.json) —siteName,location,skuName(+deployCustomDomain/customDomainif used); no secretsWrite deploy scripts (
scripts/deploy-infra.ps1and.sh) — default to the CAF resource group, support--what-if, avoid2>&1on native executables in PowerShell 5.1Write the baseline
staticwebapp.config.json—globalHeaderssecurity headers +Cache-Control: public, must-revalidate, max-age=30,.xml/.jsonMIME types,/sitemap.xmlrouteConfigure GitHub Actions
- Deployment token:
az staticwebapp secrets list --name <name> --resource-group <rg>→$t = (…).properties.apiKey.Trim()→gh secret set AZURE_SWA_TOKEN --body $t - Workflow: single deploy job with
app_location: "/",api_location: "",output_location: "/" - Not yet provisioned?
if: vars.<FLAG> == 'true'on the job; set the variable after Bicep succeeds - Second SWA from a subfolder: separate workflow,
app_location: <subfolder>,skip_app_build: true, its own token secret,paths:filter
- Deployment token:
Deploy and confirm — Bicep
--what-ifthen deploy; push;gh run watch; fetchhttps://<defaultHostname>/Hand off — custom domains, DNS and TLS →
azure-swa-custom-domains; routing, caching, CSP, hidden files →static-website-config-and-cspDocument the deployment — SWA name(s), default hostname(s), secret and variable names, workflow files, subfolder layout
Output format
The AI should produce:
- Resource naming summary — resource group, SWA name(s), region, default hostname(s)
- Bicep template — complete
infra/main.bicepcontent - Parameters file — complete
infra/parameters/prod.parameters.jsoncontent - Deploy scripts —
deploy-infra.ps1anddeploy-infra.sh - Baseline
staticwebapp.config.json— headers, cache, MIME, sitemap route - GitHub Actions workflow(s) — deploy job YAML,
vars.<FLAG>gating, any subfolder-SWA workflow, secret/variable names - Hand-off notes — what
azure-swa-custom-domainsandstatic-website-config-and-csppick up next - Post-deployment checklist — confirm each layer is live on the default hostname
Quality checklist
- Resources named using CAF conventions; region chosen deliberately (East Asia for AU audiences)
- Bicep for a token-deployed SWA has no repository properties; any
customDomainschild gated behind a bool - Deployment token stored via
gh secret set --bodyon a trimmed value — never piped, never in a file - Not-yet-provisioned deploy jobs gated with
vars.<FLAG> == 'true', not a secret - Second subfolder SWA has its own token secret and
paths:filter - Authenticated SPA lives on its own subdomain/SWA, with
__Host-cookies and no*.domainCSP wildcard - Baseline config: security headers via
globalHeaders, MIME types,/sitemap.xmlroute - No deployment running (
gh run list) before pushing - Changes deployed to dev first and validated; prod promotion is a separate approved step; any
.env.production.localdeleted right after a prod build - Site reachable on the default hostname; hand-off items listed
Avoid
- Do not hardcode deployment tokens, subscription IDs, or DNS passwords in any file committed to version control
- Do not pipe
azoutput straight intogh secret setfrom PowerShell — trim into a variable and use--body - Do not put
repositoryUrl/provider: 'GitHub'in Bicep for a token-deployed SWA — without arepositoryTokenit only adds friction - Do not manage the apex domain in Bicep — its validation is a separate lifecycle (see
azure-swa-custom-domains) - Do not gate a job on a secret in
if:— secrets are unavailable there; use a repository variable. Do not reuse the main site's deploy token for a second SWA - Do not host an authenticated portal as a subpath of the marketing site — give it its own subdomain and SWA
- Do not add security headers to the
/*route — they only reach HTML responses; useglobalHeaders - Do not use
2>&1on native Azure CLI commands in PowerShell 5.1 — it wraps stderr into error records and breaksConvertFrom-Json - Do not rely on the Azure portal for reproducible deployments — all configuration should be expressible as Bicep or CLI
- Do not push while an Azure SWA deployment is still running — the cancelled run reports a spurious failure; check
gh run list --limit 3first - Do not deploy straight to prod in a multi-env setup, or "fix" prod-behind-dev by pushing untested code — promote the validated build instead
- Do not keep dev values in the repo copy of a config-switched static site — dev lives in a scratch copy; the repo stays prod
Example usage
I'm setting up a static HTML website at
powrdata.com.auwith the repo on GitHub. Provision it on Azure Static Web Apps with Bicep, wire up GitHub Actions with the deploy token, and lay it out so I can add ago.short-link site from a subfolder later. Give me the full setup — I'll do the DNS/custom domain step next.
Source: This skill is sourced from the Matrix Skills library. Learn more at the AI Agent Skills Library.