This site provides a machine-readable index at /llms.txt.

Skip to main content Skip to navigation

Deploy to GitHub Pages

This guide covers deploying a working Pennington site committed to a GitHub repo, so Pages builds and deploys it automatically on every push to main. When the site still only runs under dotnet run, complete Build a static site first — the shape of output/ is easier to automate once it's familiar.

Assumptions

  • A Pennington site that builds locally with dotnet run --project <your-project> -- build (see Build a static site if not).
  • The repo is pushed to GitHub and Pages is enabled under Settings → Pages → Build and deployment → Source: GitHub Actions.
  • The site will serve under a repository sub-path like https://<user>.github.io/<repo>/ — root-domain deployments are called out in Step 5.
  • Working with GitHub Actions YAML at the "copy, commit, inspect the run log" level feels approachable.

For a working setup, see examples/SubPathDeployableExample. The .github/workflows/deploy.yml, host-config siblings (staticwebapp.config.json, netlify.toml, nginx.conf, web.config), and the BuildHost helper are the teaching surface; the rest of the example is outside scope here.


Steps

  1. 1

    Enable GitHub Pages with the Actions source

    In the repo settings, switch Pages → Build and deployment → Source to GitHub Actions so the deploy workflow is authorized to publish. Also confirm the three workflow permissions the deploy action needs — contents: read, pages: write, id-token: write — are not blocked at the organization level. The workflow declares them explicitly, but an org-wide deny overrides that.

  2. 2

    Drop in the canonical workflow

    Commit the YAML below to .github/workflows/deploy.yml at the repo root. It pins actions/setup-dotnet@v4 to .NET 11, derives the base URL from ${{ github.event.repository.name }} so the same file works on forks and renames, runs dotnet run -- build "$BASE_URL", writes .nojekyll, and hands output/ to actions/upload-pages-artifact@v3 and actions/deploy-pages@v4.

    # Canonical GitHub Pages workflow for a Pennington static site.
    #
    # Assumes the site is served under a repository sub-path — the typical
    # project-Pages URL is `https://<user>.github.io/<repo>/`, which requires
    # a matching `baseUrl` argument at build time so internal anchors, CSS,
    # JS, and data URLs all resolve under `/<repo>/`.
    #
    # The workflow:
    #   1. Derives the base URL from `${{ github.event.repository.name }}` so
    #      the same file works on any fork or renamed repo.
    #   2. Runs `dotnet run --project … -- build /<repo>` to emit `output/`.
    #   3. Drops a `.nojekyll` marker so GitHub Pages serves `_content/*`
    #      folders verbatim (Jekyll would silently strip underscore paths).
    #   4. Uploads `output/` as a Pages artifact and deploys it.
    #
    # If your site sits at a custom domain (root `/`), replace the `BASE_URL`
    # calculation with an empty string and strip the `/` from the build arg.
    name: Deploy to GitHub Pages
      
    on:
      push:
        branches: [main]
      workflow_dispatch:
      
    permissions:
      contents: read
      pages: write
      id-token: write
      
    concurrency:
      group: pages
      cancel-in-progress: false
      
    jobs:
      build:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
      
          - name: Setup .NET
            uses: actions/setup-dotnet@v4
            with:
              dotnet-version: 11.0.x
      
          - name: Build static site
            env:
              BASE_URL: /${{ github.event.repository.name }}
            run: |
              dotnet run \
                --project examples/SubPathDeployableExample \
                --configuration Release \
                -- build "$BASE_URL"
    
          - name: Disable Jekyll processing
            run: touch output/.nojekyll
      
          - name: Upload Pages artifact
            uses: actions/upload-pages-artifact@v3
            with:
              path: output
      
      deploy:
        needs: build
        runs-on: ubuntu-latest
        environment:
          name: github-pages
          url: ${{ steps.deployment.outputs.page_url }}
        steps:
          - name: Deploy to GitHub Pages
            id: deployment
            uses: actions/deploy-pages@v4
    
  3. 3

    Point the --project path at your site

    The template targets examples/SubPathDeployableExample; edit the --project argument and any working-directory references so the dotnet run step points at the correct csproj. For repos that host multiple buildable projects, add actions/cache@v4 over ~/.nuget/packages if NuGet restore takes more than a minute — --configuration Release is already set.

  4. 4

    Keep .nojekyll in the artifact

    GitHub Pages runs content through Jekyll by default, which silently strips any path starting with an underscore — that removes Pennington's _content/ copy folder and SPA _spa-data/ payloads. The touch output/.nojekyll step in the workflow disables Jekyll processing; leave it in place.

  5. 5

    Match the build baseUrl to the Pages URL

    Project Pages sites serve at https://<user>.github.io/<repo>/, so the workflow passes /<repo> as the first positional build argument and BaseUrlHtmlRewriter prefixes every internal href, src, and action on the way out. For sites at an org-level root or a custom apex domain, replace the BASE_URL env with an empty string and drop the argument entirely. Sub-path wiring is covered in Host under a sub-path (base URL).

  6. 6

    (Optional) Fail CI on a bad BuildReport

    RunOrBuildAsync already sets a non-zero exit code on errors, so the workflow fails fast on broken pages. For stricter semantics — failing the main-branch build on broken xrefs while letting warnings pass on feature branches — wrap the call and write the report to stdout.

    if (args.Length > 0 && args[0].Equals("build", StringComparison.OrdinalIgnoreCase))
    {
        await app.StartAsync();
        var generator = app.Services.GetRequiredService<OutputGenerationService>();
        var report = await generator.GenerateAsync();
        await app.StopAsync();
      
        PrintBuildReport(report);
    }
    else
    {
        await app.RunAsync();
    }
    report.WriteTo(Console.Out);
    if (report.HasErrors)
    {
        Environment.ExitCode = 1;
    }

Verify

  • Push to main; the Deploy to GitHub Pages workflow runs the build and deploy jobs in sequence and turns green.
  • Visit https://<user>.github.io/<repo>/ — the landing page loads, navigation links resolve under /<repo>/, and view-source shows <body data-base-url="/<repo>/">.
  • Open the build job log — expect the BuildReport summary line with zero failed pages and zero broken links; any non-zero count fails the job.