This documentation is also published as Markdown for efficient machine reading: the whole site is indexed at /llms.txt, and every page has a clean Markdown copy under /_llms/. These are generated from the same source and cost far fewer tokens to read than this rendered HTML.

Skip to main content Skip to navigation
Guides

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 to build. 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.

json
{
  "$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.

toml
# 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 -- build would ignore BASE_URL entirely and always deploy at the apex.
  • Build output directory: output
  • Environment variables: DOTNET_VERSION=10.0.x, plus BASE_URL=/<path> when serving under a sub-path (leave BASE_URL unset 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 up 10.0.x, dotnet run -- build exiting zero, and the host uploading output/ 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 generated output/404.html rather than the host's default 404 shell.