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.
| Works | Does not work |
|---|---|
| HTML, CSS, JavaScript, images, fonts, downloads | Server-side code of any kind |
| Static site generators, built before deployment | Databases, sessions, server-side authentication |
| Client-side applications calling external APIs | Form handling without a third-party endpoint |
| Custom domains with automatic HTTPS | Custom 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.
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 shape | Record type | Points to |
|---|---|---|
| Apex, example.com | Four A records (and AAAA for IPv6) | The GitHub Pages IP addresses listed in the current documentation |
| Subdomain, www.example.com | CNAME | yourusername.github.io |
| Apex on a provider with flattening | ALIAS or ANAME | yourusername.github.io |
The order that avoids a broken day
- Add the DNS records first and wait for propagation. Check with
nslookup example.combefore touching GitHub. - Then set the custom domain in the repository Pages settings. GitHub writes a
CNAMEfile 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.htmlfrom the root automatically. - Internal links. An orphan page that nothing links to is a page search engines will find last and value least.
<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.
| Host | Gives you | Move when |
|---|---|---|
| Cloudflare Pages | Custom headers and redirects, edge functions, generous bandwidth, preview deployments | You need real headers, redirect rules or a small amount of server-side logic |
| Netlify | Redirects, form handling, functions, split testing | You want form submissions without building a backend |
| Vercel | Framework-native builds, edge rendering, image optimisation | The site stops being static and becomes an application |
| A small VPS | Everything, and the responsibility for it | You 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.
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.
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.
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.
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.
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.