GitHub Pages Base Path Pitfall

When migrating a static site to a hosting platform that serves from a subdirectory (e.g., `username.github.io/repo/`), every hardcoded internal link breaks. The migration isn't done when the deploy workflow is green -- it's done when every `href`, asset path, and client-side route has been audited f...

Tags

GitHub Pages Base Path Pitfall

The Lesson

When migrating a static site to a hosting platform that serves from a subdirectory (e.g., username.github.io/repo/), every hardcoded internal link breaks. The migration isn't done when the deploy workflow is green -- it's done when every href, asset path, and client-side route has been audited for the new base path.

Context

HAx is an Astro 6.x static site with ~55 TED talks, 24 experiments, Preact islands (search, save, share), and Playwright e2e tests. The project migrated from Cloudflare Pages (which serves at the domain root) to GitHub Pages (which serves project sites at /<repo-name>/). The migration was planned as a single phase (Phase 10) covering the deploy workflow, Astro config, and Cloudflare-specific file cleanup.

What Happened

  1. Phase 10 created the GitHub Actions deploy workflow, updated astro.config.mjs to set site to the GitHub Pages URL, removed Cloudflare-specific files (_headers, _redirects), and added a robots.txt update.
  2. The deploy workflow succeeded -- the site built and deployed to GitHub Pages without errors.
  3. On the live site, every internal navigation link was broken. Clicking "Experiments" navigated to /experiments/ instead of /hax/experiments/, returning a 404.
  4. Root cause: Astro's base config option was not set, and all internal href values in components were hardcoded strings like "/experiments/" rather than using import.meta.env.BASE_URL.
  5. A 24-file fix was required: every component (Header, Footer, Breadcrumb, cards), every page route, the BaseLayout print stylesheet reference, the Pagefind search import path, and all Playwright test assertions had to be updated.
  6. The fix used import.meta.env.BASE_URL (Astro's built-in) to prefix all internal hrefs, making the site base-path-aware regardless of where it's hosted.

Key Insights

Examples

Before (hardcoded, breaks on subdirectory hosting):

<a href="/experiments/">Experiments</a>
<link rel="stylesheet" href="/print.css" media="print" />

After (base-path-aware, works anywhere):

<a href={`${import.meta.env.BASE_URL}experiments/`}>Experiments</a>
<link rel="stylesheet" href={`${import.meta.env.BASE_URL}print.css`} media="print" />

Applicability

This lesson applies to any static site generator (Astro, Next.js, Hugo, Jekyll) when deploying to a subdirectory path. It does NOT apply when deploying to a custom domain at the root (e.g., hax.example.com/) -- in that case, the base path is / and hardcoded paths happen to work. However, using BASE_URL from the start is still good practice because it makes future hosting migrations trivial.

Related Lessons

Related Lessons