Introduction
GitHub Pages is a fantastic starting point for static sites. But as your needs grow—zero-downtime deploys, branch previews, global edge performance, custom headers and redirects, or serverless functions—you might want to graduate to Cloudflare Pages.
In this step-by-step guide, you’ll learn how to migrate cleanly from GitHub Pages to Cloudflare Pages with minimal or zero downtime, while preserving SEO, URLs, and performance. We’ll cover:
- Preparing your repository (Jekyll/Hugo/Eleventy/Next/Astro/etc.)
- Configuring Cloudflare Pages builds and environments
- Switching DNS with no downtime
- Preserving links via redirects and canonical URLs
- Handling SPA routing, caching, headers, and forms
- Rollbacks, previews, and common pitfalls
If you follow along, you can ship your site on Cloudflare Pages the same day.
Table of Contents
- Introduction
- 1) Decide What You’re Migrating
- 2) Prepare Your Repository
- 3) Create Your Cloudflare Pages Project
- 4) Preview and QA Before DNS Cutover
- 5) DNS and Custom Domains (No-Downtime Cutover)
- 6) Preserve URLs, Redirects, and SEO
- 7) Headers, Caching, and Performance
- 8) SPA Routing and Rewrites
- 9) Forms and Dynamic Features
- 10) CI/CD, Previews, and Rollbacks
- Troubleshooting Common Issues
- Example: Jekyll on GitHub Pages → Cloudflare Pages
- Conclusion
1) Decide What You’re Migrating
Start by inventorying your current site:
- Generator: Plain HTML/CSS, Jekyll (common on GitHub Pages), Hugo, Eleventy, Astro, Next.js (static export or SSR), SvelteKit, etc.
- Build output directory: e.g.,
_site(Jekyll),public,dist,.next, etc. - URL structure and redirects: trailing slashes, legacy paths, old blog URLs.
- Custom domain: Do you use a custom domain (e.g., www.example.com) or the default
username.github.io? - Special needs: Forms, search, protected content, serverless functions, CMS webhooks.
Knowing these details informs your build configuration and DNS plan.
2) Prepare Your Repository
GitHub Pages can serve:
- Content directly from the repo (e.g.,
/docsdirectory orgh-pagesbranch), sometimes with Jekyll auto-builds, or - Prebuilt static files pushed to a branch.
For Cloudflare Pages, you can:
- Build from source in the cloud (recommended), or
- Deploy prebuilt artifacts.
Make sure your repo builds locally first.
Jekyll specifics (ex-GitHub Pages defaults)
If you relied on GitHub Pages’ built-in Jekyll:
- Add a Gemfile so Pages can install dependencies:
# Gemfile
source "https://rubygems.org"
gem "jekyll", "~> 4.3.3"
gem "webrick", "~> 1.8" # for local serve on Ruby 3.x
# Add other plugins you use:
# gem "jekyll-seo-tag"
# gem "jekyll-sitemap"
- Lock your Ruby version (optional but recommended):
# .ruby-version
3.2.2
Ensure a standard Jekyll structure with
_config.ymland content. If you previously depended on GitHub Pages’ restricted plugins, add those plugins explicitly to your Gemfile and_config.yml.Test locally:
bundle install
bundle exec jekyll build # outputs to ./_site
If you used baseurl on GitHub Pages (e.g., baseurl: /my-repo), consider setting it to "" if moving to a root domain. Update asset paths to be absolute or relative accordingly.
Tip: If you had a
.nojekyllfile to disable Jekyll on GitHub Pages, that’s fine to keep or remove—it won’t affect Cloudflare Pages.
Other static generators
- Hugo: ensure a working
hugobuild locally; note your output dir (defaultpublic). - Eleventy:
npx @11ty/eleventybuild; output often_siteby default. - Astro:
npx astro build; outputdist. - Next.js: for static export use
next exportwithoutfolder; for SSR/edge runtime, Cloudflare Pages can run via Pages Functions automatically with the correct adapter (framework presets handle this). - SvelteKit/Nuxt/SolidStart/etc.: use their Cloudflare or static adapters; framework presets in Cloudflare handle most.
3) Create Your Cloudflare Pages Project
- In the Cloudflare dashboard, go to Pages → Create a project.
- Connect your GitHub account and select the repository.
- Choose your build settings (framework preset where available).
Build commands and output directories
Examples:
Jekyll:
- Build command:
bundle install && bundle exec jekyll build - Output directory:
_site
- Build command:
Hugo:
- Build command:
hugo --minify - Output directory:
public
- Build command:
Eleventy:
- Build command:
npm ci && npx @11ty/eleventy - Output directory:
_site
- Build command:
Astro:
- Build command:
npm ci && npm run build - Output directory:
dist
- Build command:
Next.js (static export):
- Build command:
npm ci && npm run build && npx next export - Output directory:
out
- Build command:
Next.js (SSR on Pages Functions):
- Choose the Next.js preset; Cloudflare will configure functions automatically. Output dir is handled by the preset.
Important: Ensure the “Output directory” matches what your generator produces. If Cloudflare doesn’t find it, the deployment will be empty.
Environment variables and versions
Set versions to match your local environment when needed:
Node:
- In package.json:
{ "engines": { "node": "20.x" } } - Or set an environment variable like
NODE_VERSION=20.
- In package.json:
Ruby (for Jekyll):
- Add
.ruby-versionand/or setRUBY_VERSION=3.2.2.
- Add
Hugo:
- Set
HUGO_VERSION=0.133.0(example) if you want a specific version in the builder.
- Set
Custom variables:
- Add API keys or tokens as environment variables in Pages → Settings → Environment variables.
- Use “Preview” vs “Production” environments to scope secrets.
4) Preview and QA Before DNS Cutover
Every push triggers a build at a unique *.pages.dev URL. Use this to:
- Click through critical pages, forms, and interactive components.
- Validate images, fonts, and asset URLs (especially if you changed
baseurl). - Test 404/500 pages and SPA routing.
- Check mobile and performance with Lighthouse.
- Confirm redirects and headers (see sections below).
Only move DNS once you’re satisfied.
5) DNS and Custom Domains (No-Downtime Cutover)
If you already use a custom domain
Lower TTL ahead of time
Reduce the TTL on your existing DNS records (e.g., to 300 seconds) a few hours before the cutover to speed propagation.Add the domain in Cloudflare Pages
In your Pages project → Custom domains → Add a domain. Cloudflare will:- If your DNS is on Cloudflare: suggest and apply the correct CNAME records and provision TLS automatically.
- If your DNS is elsewhere: instruct you to create a CNAME pointing
wwwto<project>.pages.dev. For apex domain (example.com), if your DNS provider doesn’t support CNAME flattening, consider moving DNS to Cloudflare or use an ALIAS/ANAME if supported.
Validate and roll
Once verification passes and the certificate is active, traffic will serve from Cloudflare’s edge. Keep your GitHub Pages site untouched until you fully verify production.No-downtime check
Because you validated on a preview URL first, the DNS change should be seamless. If needed, perform the cut during a low-traffic window and monitor.
If you currently use username.github.io
- You can’t configure a server-side 301 from
username.github.ioto your new domain. Instead:- Prefer moving to a custom domain (recommended for brand and SEO control).
- Replace the content at
username.github.iowith a lightweight HTML page that uses a<meta http-equiv="refresh">and a canonical link to hint search engines, or keep it as a separate property.
Example redirect page:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Moved</title>
<link rel="canonical" href="https://www.example.com/">
<meta http-equiv="refresh" content="0; url=https://www.example.com/">
</head>
<body>
<p>This site has moved to <a href="https://www.example.com/">https://www.example.com/</a>.</p>
</body>
</html>
6) Preserve URLs, Redirects, and SEO
Redirect rules
Cloudflare Pages supports a _redirects file at the project root (or your output directory). Use it to maintain old GitHub Pages URLs:
# 301 redirect a page
/old-post.html /blog/old-post/ 301
# Redirect entire sections
/docs/* /guides/:splat 301
# Force www
https://example.com/* https://www.example.com/:splat 301
# SPA fallback (rewrite, not redirect)
# See SPA section for a 200 rewrite version
Place _redirects alongside your built assets so it’s deployed with the site.
Canonical links and sitemaps
- Add
<link rel="canonical" href="https://www.example.com/current-url/">in your templates. - Make sure
sitemap.xmlreflects your new domain. - Keep
robots.txtupdated.
Tip: If you changed trailing slash behavior, set consistent redirects to avoid duplicate content.
7) Headers, Caching, and Performance
Use a _headers file to set HTTP headers per path:
/*
Cache-Control: public, max-age=600
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: no-referrer-when-downgrade
Permissions-Policy: geolocation=(), microphone=()
/assets/*
Cache-Control: public, max-age=31536000, immutable
/service-worker.js
Cache-Control: no-cache
- For fingerprinted assets (e.g., app.a1b2c3.js), set
immutablewith a longmax-age. - For HTML, prefer a short TTL for rapid updates.
- Cloudflare serves your content from its global edge, improving TTFB for users worldwide.
8) SPA Routing and Rewrites
For single-page apps where unknown paths should serve index.html, add a rewrite entry in _redirects:
# Serve index.html for all non-file routes (200 rewrite)
/* /index.html 200
Place this as the last rule so other redirects take precedence.
If you’re using a framework preset (e.g., React, Vue, Svelte), Cloudflare often configures this automatically; still, it’s safe to be explicit.
9) Forms and Dynamic Features
GitHub Pages is static-only; Cloudflare Pages adds optional serverless functions (Pages Functions powered by Cloudflare Workers).
- Create a
functions/directory at your repo root to add endpoints. - Example: a simple JSON echo for form submissions:
// functions/api/contact.ts
export const onRequestPost: PagesFunction = async ({ request }) => {
const data = await request.json().catch(() => ({}));
// TODO: validate, write to KV/D1/R2, or forward to a webhook
return new Response(JSON.stringify({ ok: true, received: data }), {
headers: { "Content-Type": "application/json" },
});
};
Then POST to /api/contact from your form. You can integrate with:
- Cloudflare KV/D1/R2 for storage
- Email APIs (Mailgun/Sendgrid/Postmark)
- Turnstile for CAPTCHA
Remember to secure endpoints (e.g., CSRF tokens, origin checks, or server-side validation).
10) CI/CD, Previews, and Rollbacks
- Every commit to your production branch creates a new deployment; pull requests generate immutable preview URLs.
- Environment-specific variables: configure “Preview” vs “Production” secrets.
- If a release misbehaves, roll back by promoting a previous deployment in the Pages dashboard—no code revert required.
- You can also use Branch Deployments for staging environments.
Tip: Protect your main branch and use PR previews for QA to avoid regressions in production.
Troubleshooting Common Issues
- Empty deployments: The output directory doesn’t match. Fix the build command or output path.
- Broken asset paths after migration: Revisit
baseurl(Jekyll) or use absolute paths. Consider<base href="/">for static exports. - 404s for SPA deep links: Add the SPA rewrite rule in
_redirects. - Mixed content (HTTP assets on HTTPS pages): Update hardcoded
http://asset URLs tohttps://or protocol-relative//. - Build timeouts or missing tools: Pin versions via environment variables or files. Add
npm ci/bundle installto build commands. - Custom domain not validating: Ensure DNS record points to
<project>.pages.devas instructed; wait for TLS certificate to issue (can take a few minutes).
Example: Jekyll on GitHub Pages → Cloudflare Pages
Prepare:
- Add Gemfile,
.ruby-version, and ensurebundle exec jekyll buildworks locally. - Confirm
_config.ymlhasbaseurl: ""if moving to the domain root.
- Add Gemfile,
Connect to Cloudflare Pages:
- Create project → Connect GitHub repo.
- Build command:
bundle install && bundle exec jekyll build - Output directory:
_site - Set
RUBY_VERSION=3.2.2(or your version) in environment variables.
Add redirects and headers (optional):
_redirectsfor old URLs._headersfor security and caching.
Validate preview:
- Visit the
*.pages.devpreview deployment, check navigation, assets, and SEO tags.
- Visit the
Cut over DNS:
- Pages → Custom domains → Add www and/or apex domain.
- If DNS is on Cloudflare, allow it to create records automatically. Otherwise, create the CNAME it suggests.
- Wait for TLS to provision and verify traffic.
Monitor and finalize:
- Keep GitHub Pages content in place until you confirm a stable production on Cloudflare.
- Update your sitemap in Search Console and monitor for crawl errors.
Conclusion
Migrating from GitHub Pages to Cloudflare Pages is straightforward and unlocks powerful benefits: global edge speed, preview deployments, robust redirects and headers, and optional serverless functions—all while keeping your repo-centric workflow.
The safest path is:
- Validate builds locally,
- Stand up a Pages preview,
- Configure redirects, headers, and environment variables,
- Then perform a measured DNS cutover for zero downtime.
With careful attention to URL preservation, caching, and QA, your site will be faster, more flexible, and easier to evolve on Cloudflare Pages.