Adapt the deploy workflow for other hosts
With Deploy to GitHub Pages already in place and the dotnet run -- build [baseUrl] → output/ → artifact pipeline understood, this page covers the deltas for targeting Azure Static Web Apps, Cloudflare Pages, or Netlify instead. Start with the GitHub Pages recipe first — the material here assumes familiarity with it.
Assumptions
- Deploy to GitHub Pages is already working, and the canonical workflow builds cleanly against the project.
- 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. - Editing one host config file per target is comfortable territory — the snippets below are complete, not starting points.
For a working setup, see examples/SubPathDeployableExample. The .github/workflows/deploy.yml, staticwebapp.config.json, and netlify.toml siblings are the teaching surface for this page; the rest of the example is outside scope here.
Steps
- 1
Lock in the four shared values
Every host configuration shares four knobs: the build command (
dotnet run --project <your-project> -- build "$BASE_URL"), the publish directory (output/, the default fromOutputOptions), the .NET SDK pin (11.0.x, matchingsetup-dotnet@v4in the GitHub Pages workflow), and the base URL (/for apex domains,/<path>for sub-path hosting). Settle on these four values before opening any host dashboard — the per-host files below are each those same four values expressed in that host's syntax. See CLI and build arguments for theOutputOptions.FromArgsgrammar. - 2
Pick your host and copy the per-host deltas
The table below is the authoritative diff against the GitHub Pages workflow — blank cells mean nothing changes from the canonical recipe. Find the row for the target host, then continue to the matching step for the host-specific config file.
Concern GitHub Pages (canonical) Azure Static Web Apps Cloudflare Pages Netlify Config file .github/workflows/deploy.ymlstaticwebapp.config.jsonat repo root + SWA's own build actionPages dashboard or wrangler.tomlnetlify.tomlat repo rootBuild command dotnet run --project … -- build "$BASE_URL"same (invoked via Azure/static-web-apps-deploy@v1app_build_command)same (set in dashboard → Build command) same (declared in [build] command)Publish directory output(viaupload-pages-artifact@v3)output_location: "output"on the SWA actionBuild output directory: outputpublish = "output".NET SDK pin actions/setup-dotnet@v4with11.0.xadd actions/setup-dotnet@v4before the SWA actiondashboard env: DOTNET_VERSION = 11.0.x(Cloudflare autodetects from there)[build.environment] DOTNET_VERSION = "11.0.x"Base URL strategy derived from ${{ github.event.repository.name }}pass explicitly — SWA serves at apex by default, so usually ""pass explicitly — Cloudflare serves at apex, usually ""$BASE_URLenv var with/default; override in dashboard per siteSPA / deep-link fallback .nojekyllmarker +404.htmlnavigationFallback.rewrite: "/404.html"(see step 3)Cloudflare auto-serves 404.htmlfrom build output — no extra config[[redirects]]withstatus = 404 → /404.html(see step 4)Cache headers for /_content/*GitHub Pages default (short TTL) routes[]entry,Cache-Control: public, max-age=31536000, immutable_headersfile inoutput/(same directive)[[headers]] for = "/_content/*"(same directive).nojekyllneeded?yes no (SWA does not run Jekyll) no no - 3
Azure Static Web Apps — drop in
staticwebapp.config.jsonCommit the JSON below 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), confirmapp_build_commandisdotnet run --project <your-project> -- buildandoutput_locationisoutput— everything else from the canonical 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/*", "/_spa-data/*", "/*.{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" } } - 4
Netlify — drop in
netlify.tomlCommit the TOML below at the repo root; Netlify autodetects it and no dashboard build-setting changes are needed beyond linking the repo.
BASE_URLdefaults to/— override it in Site configuration → Environment variables for sub-path hosting. The[[redirects]]block withstatus = 404routes deep-link misses to the generatedoutput/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 = "11.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 - 5
Cloudflare Pages — configure in dashboard (no config file needed)
Cloudflare Pages has no first-party config file equivalent to SWA or Netlify, so everything goes in the project dashboard under Settings → Builds & deployments: set Build command to
dotnet run --project <your-project> -- build, Build output directory tooutput, and add environment variablesDOTNET_VERSION=11.0.xandBASE_URL=/(or the relevant sub-path). For custom cache headers on/_content/*, drop a_headersfile intowwwroot/so it ships as part ofoutput/— the directive format matches the Netlify and Azure snippets above. - 6
Pass the right
baseUrlfor the host's URL shapeGitHub Pages defaults to a sub-path; Azure Static Web Apps, Cloudflare Pages, and Netlify default to the apex — so the build argument is usually different. For apex deploys pass nothing (or
/); for a sub-path pass/<path>andBaseUrlHtmlRewriterprefixes every internal href, src, and action on the way out. Full details are in Host under a sub-path (base URL).
Verify
- Trigger a deploy on the target host; the build log shows
setup-dotnet(or equivalent) picking up11.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="...">(empty or/<path>/depending on the host). - 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.