Adapt the deploy workflow for other hosts
Port the GitHub Pages recipe to Azure Static Web Apps, Cloudflare Pages, or Netlify by swapping four shared values and dropping in one host-specific config file.
With Deploy to GitHub Pages already in place and the dotnet run -- build [baseUrl] → output/ → artifact pipeline understood, this page covers the deltas for Azure Static Web Apps, Cloudflare Pages, and Netlify. The material assumes the GitHub Pages recipe is the starting point.
Before you begin
- The canonical GitHub Pages workflow is committed and building cleanly.
- A deploy target account exists (SWA resource, Cloudflare Pages project, or Netlify site) and the repo is connected.
- The site serves either at the host's domain root (
baseUrl = "/") or under a known sub-path to pass as the first positional argument tobuild. See Host under a sub-path (base URL) for the rewriter behavior.
For a working setup, see examples/SubPathDeployableExample — the .github/workflows/deploy.yml, staticwebapp.config.json, and netlify.toml siblings each express the same handful of settings in their host's syntax.
Host deltas
Every host restates the same four settings — build command (dotnet run --project <your-project> -- build "<base-url>"), publish directory (output/, the default from OutputOptions), .NET SDK pin (10.0.x, matching setup-dotnet@v4 in the GitHub Pages workflow), and base URL (/ for apex domains, /<path> for sub-path hosting). The table below is the diff against the GitHub Pages workflow — it shows where each setting diverges per host. See CLI and build arguments for the OutputOptions.FromArgs grammar.
| Concern | GitHub Pages (canonical) | Azure Static Web Apps | Cloudflare Pages | Netlify |
|---|---|---|---|---|
| Config file | .github/workflows/deploy.yml |
staticwebapp.config.json + SWA's own build action |
Pages dashboard (no first-party config file) | netlify.toml |
| Build command | dotnet run --project … -- build "$BASE_URL" |
same (invoked via Azure/static-web-apps-deploy@v1 app_build_command) |
same (set in dashboard → Build command) | same (declared in [build] command) |
| Publish directory | output (via upload-pages-artifact@v3) |
output_location: "output" on the SWA action |
Build output directory: output |
publish = "output" |
| .NET SDK pin | actions/setup-dotnet@v4 with 10.0.x |
add actions/setup-dotnet@v4 before the SWA action |
dashboard env: DOTNET_VERSION = 10.0.x |
[build.environment] DOTNET_VERSION = "10.0.x" |
| Base URL strategy | derived from ${{ github.event.repository.name }} |
BASE_URL env var, passed into app_build_command as "${BASE_URL:-/}"; apex by default |
BASE_URL env var, passed into the build command as "${BASE_URL:-/}"; apex by default |
BASE_URL env var with / default; override in dashboard per site |
| SPA / deep-link fallback | .nojekyll marker + 404.html |
navigationFallback.rewrite: "/404.html" (see Azure below) |
Cloudflare auto-serves 404.html from build output |
[[redirects]] with status = 404 → /404.html (see Netlify below) |
Cache headers for /_content/* |
GitHub Pages default (short TTL) | routes[] entry, Cache-Control: public, max-age=31536000, immutable |
_headers file in output/ (same directive) |
[[headers]] for = "/_content/*" (same directive) |
.nojekyll needed? |
yes | no | no | no |
Azure Static Web Apps
Commit staticwebapp.config.json at the repo root; SWA reads it during deploy and applies routes, MIME overrides, nav fallback, and 404 handling. In the SWA workflow (.github/workflows/azure-static-web-apps-<id>.yml, generated by the Azure portal), set app_build_command to dotnet run --project <your-project> -- build "${BASE_URL:-/}" and output_location to output. The ${BASE_URL:-/} expansion is what carries the sub-path through — a bare -- build always deploys at the apex regardless of the env var. Define BASE_URL as a workflow env: entry (or leave it unset for apex hosting). Everything else from the GitHub Pages workflow applies unchanged.
{
"$schema": "https://json.schemastore.org/staticwebapp.config.json",
"trailingSlash": "auto",
"mimeTypes": {
".json": "application/json",
".xml": "application/xml",
".webmanifest": "application/manifest+json"
},
"routes": [
{
"route": "/sitemap.xml",
"headers": {
"Cache-Control": "public, max-age=3600"
}
},
{
"route": "/llms.txt",
"headers": {
"Cache-Control": "public, max-age=3600"
}
},
{
"route": "/_content/*",
"headers": {
"Cache-Control": "public, max-age=31536000, immutable"
}
}
],
"navigationFallback": {
"rewrite": "/404.html",
"exclude": [
"/_content/*",
"/*.{css,js,json,png,jpg,jpeg,gif,svg,webp,ico,woff,woff2,ttf,xml,txt,webmanifest}"
]
},
"responseOverrides": {
"404": {
"rewrite": "/404.html"
}
},
"globalHeaders": {
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "strict-origin-when-cross-origin"
}
}
Netlify
Commit netlify.toml at the repo root; Netlify autodetects it and no dashboard build-setting changes are needed beyond linking the repo. BASE_URL defaults to / — override it in Site configuration → Environment variables for sub-path hosting. The [[redirects]] block with status = 404 routes deep-link misses to the generated output/404.html.
# Netlify configuration for a Pennington static site.
#
# Netlify serves `publish = "output"` verbatim. Set `BASE_URL` in the
# Netlify dashboard (Site configuration → Environment variables) if you
# need a sub-path; for a root-served site leave it as the default `/`.
#
# The 404 fallback uses Netlify's conditional `status = 404` rewrite so
# deep-link misses return the generated `output/404.html` page body
# instead of Netlify's default 404 shell.
[build]
command = "dotnet run --project examples/SubPathDeployableExample -- build ${BASE_URL:-/}"
publish = "output"
[build.environment]
DOTNET_VERSION = "10.0.x"
BASE_URL = "/"
[[headers]]
for = "/_content/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
[[headers]]
for = "/*"
[headers.values]
X-Content-Type-Options = "nosniff"
Referrer-Policy = "strict-origin-when-cross-origin"
# Pretty-URL fallback: Pennington's DocSite emits `<slug>/index.html`,
# which Netlify already serves at `/<slug>/`. The explicit 404 rule
# below only fires when nothing else matches.
[[redirects]]
from = "/*"
to = "/404.html"
status = 404
Cloudflare Pages
Cloudflare Pages has no first-party config file equivalent to SWA or Netlify, so the four shared values live in the project dashboard under Settings → Builds & deployments:
- Build command:
dotnet run --project <your-project> -- build "${BASE_URL:-/}"— the${BASE_URL:-/}expansion passes the env var through as the base-url argument, defaulting to/when it is unset. A bare-- buildwould ignoreBASE_URLentirely and always deploy at the apex. - Build output directory:
output - Environment variables:
DOTNET_VERSION=10.0.x, plusBASE_URL=/<path>when serving under a sub-path (leaveBASE_URLunset for apex hosting).
For custom cache headers on /_content/*, drop a _headers file into wwwroot/ so it ships as part of output/ — the directive format matches the Netlify and Azure snippets above.
Verify
- Trigger a deploy on the target host. The build log shows
setup-dotnet(or equivalent) picking up10.0.x,dotnet run -- buildexiting zero, and the host uploadingoutput/as the publish directory. - Open the deployed URL — the landing page loads, nested links resolve, and view-source shows the expected
<body data-base-url="...">(either absent for root deployments, or/<path>with no trailing slash for sub-path hosts). - Visit a non-existent path like
/does-not-exist/— the response body is the generatedoutput/404.htmlrather than the host's default 404 shell.
Related
- Recipe: Deploy to GitHub Pages — the canonical workflow this page diffs against.
- Recipe: Self-host behind Nginx or IIS — for hosts where you own the web server config instead of a managed platform.
- Recipe: Host under a sub-path (base URL) — how
BaseUrlHtmlRewriterprefixes internal URLs when the host serves under/<path>/. - Reference: CLI and build arguments — the
build [baseUrl] [outputDirectory]surface every host command above invokes.