Host under a sub-path (base URL)
When a site works at root (/) locally but the target host — a GitHub Pages project site, a reverse-proxied sub-app, or an Azure Front Door path — serves it under a sub-path, one extra argument to build covers the difference. There is no per-link refactor and no separate build mode: the same RunOrBuildAsync call handles both cases, with BaseUrlHtmlRewriter prefixing every root-relative href, src, and action on the way out.
Assumptions
- A working Pennington site that builds locally with
dotnet run -- build(see Build a static site if not). - The sub-path the host will serve from — for example
/docsforhttps://example.com/docs/or/<repo>for a GitHub Pages project site. - Internal links authored as root-relative (
/guides/first-page/) — the rewriter keys off the leading/. - The host is already configured to serve
output/at that sub-path (see Deploy to GitHub Pages or Self-host behind Nginx or IIS).
For a working setup, see examples/SubPathDeployableExample. The nested /guides/first-page/ route is deliberate: it makes sub-path rewriting observable on a deep link.
Steps
- 1
Pass the base URL to
buildOutputOptions.FromArgsaccepts the sub-path as either a positional token or a named flag; the named flag form survives reordering in CI scripts more reliably. Include the leading slash and omit the trailing slash — the rewriter normalizes either way.# positional — base URL first, output directory second dotnet run -- build /docs # named flag (preferred for CI) dotnet run -- build --base-url=/docs --output=distSee CLI and build arguments for the full argument grammar parsed by
OutputOptions.FromArgs. - 2
Know what the rewriter prefixes
BaseUrlHtmlRewriterruns atOrder => 30in theIHtmlResponseRewriterchain — after xref resolution (10) and locale prefixing (20) — so every upstream transform hands it root-relative paths. It prefixes anyhref,src, oractionattribute whose value starts with/(but not//, which is protocol-relative) and stampsdata-base-urlon<body>for client-side code that needs to reproduce the prefix on dynamically built URLs. See Pennington.Infrastructure.IResponseProcessor for the full rewriter surface. - 3
Use root-relative links in your content
Because the rewriter only matches the leading
/, protocol-relative URLs (//cdn.example.com/x.js) and absolute URLs (https://…) pass through untouched, while hash (#section) and page-relative links (./neighbor/) are ignored. The nested/guides/first-page/link in the example's landing page is the smallest case that makes the prefix visible — copy its shape for internal cross-links.--- title: Welcome description: The deployment demo landing page — verify that internal anchors, assets, and scripts rewrite when the site is built with a sub-path base URL. order: 10 --- # Welcome This is the landing page for **SubPathDeployableExample**, the demo app that backs every how-to under `/how-to/deployment/*`. The teaching artefacts for those pages are **sibling fixture files** of this project — you can copy them verbatim into your own repo: - `.github/workflows/deploy.yml` — GitHub Pages Actions workflow - `staticwebapp.config.json` — Azure Static Web Apps - `netlify.toml` — Netlify - `nginx.conf` — self-host behind Nginx - `web.config` — self-host behind IIS ## Verify the base URL Jump to the [first guide page](/guides/first-page/) and inspect the generated HTML. When the site is built with a sub-path — for example `dotnet run -- build /my-sub-path` — every root-relative anchor, stylesheet, and script on this page is prefixed with `/my-sub-path` by `BaseUrlHtmlRewriter`. When the site is built at the root (`dotnet run -- build`), the same links stay unprefixed. One HTTP pipeline, two outputs. - 4
Read
data-base-urlfrom client-side codeWhen an island, Blazor component, or hand-rolled script builds URLs at runtime, read the prefix from
document.body.dataset.baseUrlrather than hard-coding/docs. This keeps a single build portable across hosts — the sameoutput/works under/docsin staging and/in preview with no code change, only a different--base-url.const base = document.body.dataset.baseUrl ?? ""; const href = `${base}/guides/first-page/`;
Verify
- Run
dotnet run -- build --base-url=/docsand openoutput/index.html— every internalhref,src, andactionnow starts with/docs/, and<body>carriesdata-base-url="/docs". - Serve
output/under the same sub-path (for examplenpx http-server output -p 5000behind a reverse proxy at/docs/) — deep links like/docs/guides/first-page/resolve and their in-page links stay under the prefix. - Re-run with no
--base-url— the generated HTML reverts to root-relative paths with nodata-base-urlattribute, confirming the rewriter short-circuits when the prefix is empty or/.
Related
- Reference: CLI and build arguments — the
build [baseUrl] [outputDirectory]surface this page drives. - Reference: Pennington.Infrastructure.BaseUrlHtmlRewriter — the rewriter that prefixes every root-relative
href,src, andactionand stampsdata-base-urlon<body>. - Background: The response-processing pipeline — why base-URL rewriting runs at
Order => 30, after xref and locale rewriters. - Background: Dev mode and build mode share one code path — why the same rewriter runs identically in
dotnet runanddotnet run -- build.