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

Deploy to GitHub Pages

Ship a Pennington site to GitHub Pages with a ready-to-copy Actions workflow, base-URL injection, and the `.nojekyll` marker.

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 directory structure of output/ is easier to automate once it's familiar.

Before you begin

  • 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.

For a working setup, see examples/SubPathDeployableExample — the .github/workflows/deploy.yml and BuildHost helper are the relevant siblings.


Steps

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

Add the deploy workflow

Commit the YAML below to .github/workflows/deploy.yml at the repo root. It pins actions/setup-dotnet@v4 to .NET 10, 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.

yaml
# 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 an org root or a custom domain (served from `/`),
# set `BASE_URL` to an empty string. `build "$BASE_URL"` then passes an
# empty argument and the base-URL rewriter leaves internal links untouched.
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: 10.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

Note

The touch output/.nojekyll step is load-bearing: without it GitHub Pages runs the artifact through Jekyll, which strips any path starting with an underscore — including Pennington's _content/ static-web-asset folder. The marker disables Jekyll so _content/* ships verbatim.

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.

4

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 (https://<org>.github.io/) or a custom apex domain, the site serves from /: set BASE_URL to an empty string so build "$BASE_URL" passes an empty argument and the rewriter leaves links untouched. The workflow's header comment marks the same two lines to change. Sub-path wiring is covered in Host under a sub-path (base URL).

Customize the exit semantics

RunOrBuildAsync already sets a non-zero exit code on errors, so the workflow above fails fast on broken pages. When you need stricter or more selective behavior — failing the main-branch build on broken xrefs while letting warnings pass on feature branches — skip the RunOrBuildAsync extension, run the generator yourself, and inspect the BuildReport before setting the exit code. The BuildHost helper in the example does exactly that:

csharp
public static void PrintBuildReport(BuildReport report)
{
    report.WriteTo(Console.Out);
    if (report.HasErrors)
    {
        Environment.ExitCode = 1;
    }
}

report.HasErrors covers broken xrefs and failed pages; branch on report.Diagnostics for finer-grained rules. Call BuildHost.RunOrBuildAsync from Program.cs in place of the default extension to route the build through it.


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>"> (the rewriter trims the trailing slash).
  • 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.