01 · THE PLATFORM

Static files, served fast, with no server behind them.

GitHub Pages serves files from a repository over a CDN. There is no runtime, which is a limitation and the entire reason it stays up without maintenance.

WorksDoes not work
HTML, CSS, JavaScript, images, fonts, downloadsServer-side code of any kind
Static site generators, built before deploymentDatabases, sessions, server-side authentication
Client-side applications calling external APIsForm handling without a third-party endpoint
Custom domains with automatic HTTPSCustom response headers or redirect rules

That last row is the one that catches people. You cannot set cache-control, security headers or server-side redirects on Pages. If you need a content security policy, it goes in a meta tag; if you need redirects, they are HTML pages with a meta refresh and a canonical link. Both work; neither is as clean as a host that lets you write configuration.

Practical limits worth knowing: repositories are expected to stay under roughly a gigabyte, published sites under about the same, with a soft bandwidth allowance and a build limit per hour. A normal documentation or portfolio site is nowhere near any of them.

02 · DEPLOYMENT

Branch, folder, or workflow. Pick by what builds your site.

1. Publish from a branch

The simplest model: the repository contains the finished HTML, and Pages serves the branch root. Nothing builds, so nothing can fail to build. This is the right choice for hand-written sites and the reason plenty of small sites have run untouched for years.

2. Publish from a /docs folder

The same idea, with the site in a subdirectory so it can live beside source code in one repository. Useful for a project that ships documentation alongside its code.

3. Publish from a GitHub Actions workflow

Required for anything that needs a build step — a static site generator, a bundler, a template engine. The workflow builds and uploads an artefact, and Pages deploys it.

.GITHUB/WORKFLOWS/DEPLOY.YML
name: Deploy to Pages
on:
  push:
    branches: [main]
permissions:
  contents: read
  pages: write
  id-token: write
concurrency:
  group: pages
  cancel-in-progress: true
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version-file: .nvmrc
          cache: npm
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-pages-artifact@v3
        with:
          path: dist
  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
    steps:
      - uses: actions/deploy-pages@v4

Do not add a build step you do not need. A workflow is another dependency that can break on a Tuesday because an action deprecated a version. If your site is HTML and CSS, publish the branch directly and keep the moving parts at zero.

03 · DOMAIN AND HTTPS

DNS records, the CNAME file, and the certificate.

Two things have to agree: your DNS points at GitHub, and GitHub knows which domain to answer for. Getting one without the other is the usual reason a domain shows a 404 for a day.

Domain shapeRecord typePoints to
Apex, example.comFour A records (and AAAA for IPv6)The GitHub Pages IP addresses listed in the current documentation
Subdomain, www.example.comCNAMEyourusername.github.io
Apex on a provider with flatteningALIAS or ANAMEyourusername.github.io

The order that avoids a broken day

  • Add the DNS records first and wait for propagation. Check with nslookup example.com before touching GitHub.
  • Then set the custom domain in the repository Pages settings. GitHub writes a CNAME file into the repository — leave it there, and do not let a build step overwrite it.
  • Wait for the certificate. It can take up to a day. Do not enable Enforce HTTPS until GitHub reports the certificate is issued, or you will lock the site behind a protocol it cannot serve yet.
  • Pick one canonical host — apex or www — and make sure the other redirects to it. GitHub handles this once both are configured, and your canonical tags must match your choice.

If a build step wipes the CNAME file, the domain silently detaches. Generators that clean the output directory are the usual culprit. Either commit the file into the source that gets copied, or configure the generator to emit it.

04 · BEING FOUND

Nine things every static site needs before it can rank.

A static site has no framework doing this for you. Each item is a line of HTML or a small file, and skipping them is why a perfectly good site gets no traffic.

  • A unique title and meta description per page. Roughly 50 to 60 characters for the title, 120 to 160 for the description. Duplicates across pages actively hurt.
  • A canonical link on every page, absolute, matching the host you chose. This is what prevents the apex, the www and the github.io address being treated as three sites.
  • An XML sitemap, listing exactly your indexable pages and nothing else, referenced from robots.txt.
  • A robots.txt that allows crawling and declares the sitemap by absolute URL.
  • Open Graph and Twitter card tags with an absolute image URL. This is what decides whether a shared link looks like a page or like a bare string.
  • Structured data in JSON-LD. Article, FAQPage and BreadcrumbList are the three that most content sites can genuinely justify.
  • Exactly one h1 per page, describing that page rather than the site.
  • A real 404 page — Pages serves 404.html from the root automatically.
  • Internal links. An orphan page that nothing links to is a page search engines will find last and value least.
HEAD · THE MINIMUM PER PAGE
<title>Page title, distinct from every other page</title>
<meta name="description" content="One sentence that would make someone click.">
<link rel="canonical" href="https://example.com/page.html">
<meta property="og:title" content="Page title">
<meta property="og:description" content="Same sentence, roughly.">
<meta property="og:url" content="https://example.com/page.html">
<meta property="og:image" content="https://example.com/card.png">
<meta name="twitter:card" content="summary_large_image">

Automate the check. A short script that verifies every page has a unique title, a correct canonical, a sitemap entry and no broken internal links catches the mistakes that are invisible in a browser. This site runs exactly that on every change, and it has caught more problems than any manual review.

05 · PERFORMANCE

What you can control when you cannot set headers.

You do not control caching or compression on Pages, so performance work happens entirely in the documents themselves.

  • Serve modern image formats. WebP or AVIF, with dimensions on every image element so the layout does not shift while they load.
  • Set width and height on every image. The cheapest fix for layout shift there is, and it costs two attributes.
  • Lazy-load anything below the fold with loading="lazy", and leave above-the-fold images eager.
  • Preconnect to font hosts, use display=swap, and load only the weights you actually use. Fonts are usually the largest render-blocking cost on a simple site.
  • Inline small CSS, defer all JavaScript. On a static site, most scripts have no reason to block rendering.
  • Avoid third-party embeds. One analytics script and one embedded video can outweigh your entire page.
  • Measure on a throttled connection. Lighthouse on a fast desktop connection tells you almost nothing about the visitor on mobile data.

Put a content security policy in a meta tag. Since you cannot send headers, http-equiv="Content-Security-Policy" is the available route. Restricting default-src, object-src and base-uri costs nothing on a static site and closes a real class of injection.

06 · ALTERNATIVES

Three hosts that solve what Pages cannot.

HostGives youMove when
Cloudflare PagesCustom headers and redirects, edge functions, generous bandwidth, preview deploymentsYou need real headers, redirect rules or a small amount of server-side logic
NetlifyRedirects, form handling, functions, split testingYou want form submissions without building a backend
VercelFramework-native builds, edge rendering, image optimisationThe site stops being static and becomes an application
A small VPSEverything, and the responsibility for itYou need a database or long-running processes

Do not move for performance. Pages is behind a CDN and is fast. Move when you need behaviour it structurally cannot provide — headers, redirects, or code executing on request. For a documentation site, a portfolio or a static content site, the absence of moving parts is the feature.

07 · QUICK ANSWERS

GitHub Pages, briefly.

Is GitHub Pages free for a custom domain?

Yes. Custom domains and automatic HTTPS certificates are included at no cost for public repositories. You pay only for the domain registration itself. Private repositories can publish Pages sites on paid plans, but the site is still publicly visible unless you use a plan with access control.

Why is my custom domain showing a 404?

Usually one of three things: DNS has not propagated yet, the CNAME file is missing from the published output because a build step deleted it, or the domain was set in settings before the DNS records existed. Check the domain resolves with nslookup first, then confirm the CNAME file is present in the deployed branch.

Can I run a backend on GitHub Pages?

No. Pages serves static files only, with no server-side execution, no database and no ability to set response headers. Client-side JavaScript calling an external API works fine. If you need form handling, authentication or server logic, use a host with edge functions such as Cloudflare Pages or Netlify.

Do I need GitHub Actions to deploy?

Only if your site needs a build step. If the repository already contains finished HTML, publish directly from a branch or a docs folder and nothing can fail to build. Use an Actions workflow when a static site generator or bundler has to run first.

Why is my GitHub Pages site not appearing in Google?

Most commonly it lacks the basics: unique titles and descriptions, an absolute canonical link on every page, an XML sitemap declared in robots.txt, and internal links so pages are not orphaned. Also verify the property in Google Search Console and confirm the canonical host matches the one you actually serve.