Skip to content
Astro as deployment 4 min read

Deploying to GitHub Pages

GitHub Pages is a free static hosting service baked into every GitHub repository, which makes it a natural home for an Astro site. Because Astro ships zero JavaScript by default and pre-renders to plain HTML, the output drops cleanly onto Pages with no server required. The one wrinkle most people hit is the base path: project sites are served from a subdirectory (https://user.github.io/repo/), so your site and base config must match or every asset and link will 404. This page walks through configuring the project, wiring up the official GitHub Actions workflow, and shipping your first deploy.

Choosing your URL shape

GitHub Pages serves two kinds of sites, and which one you have determines your configuration.

Site typeRepository nameURLbase needed?
User/Org siteuser.github.iohttps://user.github.io/No
Project siteanything elsehttps://user.github.io/repo/Yes — /repo

If you publish a project site under a subpath, you must set base so Astro rewrites internal links and asset URLs correctly. For a custom domain (configured via a CNAME file in public/), you serve from the root and omit base.

Configuring Astro

Set both site (your full deploy URL) and, for project sites, base (the repository name with a leading slash) in astro.config.mjs.

import { defineConfig } from 'astro/config';

export default defineConfig({
  site: 'https://your-username.github.io',
  base: '/your-repo-name',
});

Gotcha: base is not automatically prepended when you hardcode URLs. Always build internal links and asset paths from import.meta.env.BASE_URL so they survive a change of base path or a move to a custom domain.

Use the base URL helper anywhere you reference a static asset or internal route:

---
const base = import.meta.env.BASE_URL; // '/your-repo-name/' or '/'
---
<a href={`${base}about`}>About</a>
<img src={`${base}logo.svg`} alt="Logo" />

For links between pages, the astro:transitions and routing helpers already respect base, so prefer relative anchors or the BASE_URL prefix over absolute / paths.

The official GitHub Actions workflow

GitHub publishes a first-party action, withastro/action, that installs dependencies, runs astro build, and uploads the dist/ output as a Pages artifact. Create .github/workflows/deploy.yml:

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:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Build with Astro
        uses: withastro/action@v3
        # with:
        #   path: .              # root of the Astro project
        #   node-version: 20
        #   package-manager: pnpm@latest

  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

The permissions block is mandatory — without pages: write and id-token: write, the deploy step cannot authenticate. The concurrency group ensures only one deploy runs at a time so an in-flight publish is never clobbered.

Enabling Pages and deploying

  1. Push the workflow file and your config changes to the main branch.
  2. In the repository, open Settings → Pages.
  3. Under Build and deployment → Source, choose GitHub Actions (not “Deploy from a branch”).
  4. The push triggers the workflow; watch it under the Actions tab.

On success, the deploy job prints the live URL:

Output:

Deploy to GitHub Pages
  Reporting build success to GitHub
  Created deployment for <sha>
  Deployment status: success
  page_url: https://your-username.github.io/your-repo-name/

To verify a build locally before pushing, run the same command the action uses and preview it:

npm run build
npm run preview

Output:

 astro v5.0.0 ready in 412 ms
┃ Local    http://localhost:4321/your-repo-name/
┃ Network  use --host to expose

Note the preview server respects your base, so the local path mirrors production — a quick sanity check that nothing is hardcoded to /.

Custom domains

To serve from your own domain, add a public/CNAME file containing the bare hostname (e.g. docs.example.com), set site to https://docs.example.com, and remove base. Configure the DNS records and domain field under Settings → Pages. Because public/ is copied verbatim into dist/, the CNAME file survives every build.

Best Practices

  • Always set site so canonical URLs, sitemaps (@astrojs/sitemap), and RSS feeds emit correct absolute links.
  • For project sites, derive every internal link and asset URL from import.meta.env.BASE_URL rather than hardcoding /.
  • Pin the workflow to withastro/action@v3 and actions/deploy-pages@v4 rather than floating tags, then bump deliberately.
  • Run npm run build && npm run preview locally to catch base-path mistakes before they reach production.
  • Keep the permissions and concurrency blocks in the workflow — they are required for OIDC auth and safe serialized deploys.
  • Add a .nojekyll file (via public/.nojekyll) if you ship asset folders starting with an underscore, to stop Pages’ legacy Jekyll processing.
Last updated June 14, 2026
Was this helpful?