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 type | Repository name | URL | base needed? |
|---|---|---|---|
| User/Org site | user.github.io | https://user.github.io/ | No |
| Project site | anything else | https://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:
baseis not automatically prepended when you hardcode URLs. Always build internal links and asset paths fromimport.meta.env.BASE_URLso 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
- Push the workflow file and your config changes to the
mainbranch. - In the repository, open Settings → Pages.
- Under Build and deployment → Source, choose GitHub Actions (not “Deploy from a branch”).
- 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
siteso 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_URLrather than hardcoding/. - Pin the workflow to
withastro/action@v3andactions/deploy-pages@v4rather than floating tags, then bump deliberately. - Run
npm run build && npm run previewlocally to catch base-path mistakes before they reach production. - Keep the
permissionsandconcurrencyblocks in the workflow — they are required for OIDC auth and safe serialized deploys. - Add a
.nojekyllfile (viapublic/.nojekyll) if you ship asset folders starting with an underscore, to stop Pages’ legacy Jekyll processing.