Why: unlimited bandwidth (free forever), global CDN, automatic free SSL, free custom domains, per-branch preview deployments – and it reads the _headers and _redirects files SiteKit emits natively, with zero extra config (see below).
Cloudflare Pages serves a pre-built static folder – it does not run Swift, so it cannot build your SiteKit site on push. The flow is always the same two stages:
- Build your site into the
_Site/directory (locally, or in CI):swift run -c release Site build
- Upload that
_Site/folder to Cloudflare Pages.
There are two ways to do stage 2 – pick one:
- Path 1 – manual one-off (fastest way to a first live site): build locally, upload with the Wrangler CLI. Best for the first deploy or an occasional manual publish.
- Path 2 – automated push-to-deploy (recommended for ongoing): GitHub Actions installs Swift, runs the build, and uploads
_Site/on every push. Set it up once, then justgit push.
Bonus you get for free on Cloudflare: SiteKit writes _Site/_headers (long-cache for assets + security headers) and, when you configure redirects, _Site/_redirects. Cloudflare Pages consumes both formats natively – you don't configure caching or redirects anywhere in the dashboard; uploading _Site/ is enough.
# 1. Build the site → produces ./_Site/
swift run -c release Site build
# 2. Install the Cloudflare CLI (once)
npm install -g wrangler # or: brew install wrangler
# 3. Authenticate (opens a browser to authorize)
wrangler login
# 4. Deploy the built folder
wrangler pages deploy _Site --project-name=my-siteThat's it – your site is live at https://my-site.pages.dev.
- You do not need to create the project first. The first
wrangler pages deploy --project-name=my-sitecreates the project automatically if it doesn't exist. (If you prefer to pre-create it:wrangler pages project create my-site.) - The project name becomes the subdomain (
my-site.pages.dev) and cannot be changed later – choose it deliberately. - To publish updates later, just re-run steps 1 and 4.
Here GitHub Actions does the Swift build and the upload. The full workflow file (installing Swift, running swift run -c release Site build) lives in ../ci/github-actions.md – this section covers the Cloudflare-specific pieces it needs: an API token, your Account ID, and the deploy step.
Via Wrangler (if authenticated): wrangler whoami prints it.
Via dashboard: it's in the dashboard URL (https://dash.cloudflare.com/<ACCOUNT_ID>/...) or any domain's Overview page → right sidebar.
At dash.cloudflare.com/profile/api-tokens:
- Create Token → scroll to Custom token → Get started
- Token name:
GitHub Actions - Pages Deploy - Permissions: Account → Cloudflare Pages → Edit
- Account Resources: Include → select your account
- Continue to summary → Create Token – copy it immediately (shown only once)
Do not use the "Global API Key" on that page – it grants full account access. The scoped token above is all the deploy needs.
(Even in "agent does it" mode, the user must create this token themselves in the browser – the agent should explain the steps and wait for the value.)
gh secret set CLOUDFLARE_API_TOKEN # paste the token from above
gh secret set CLOUDFLARE_ACCOUNT_ID # paste your Account IDVerify with gh secret list.
Add this as the final step of the CI workflow (it runs after the Swift build, on a push to your default branch):
Remote package (standard – _Site/ is at the repo root):
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy _Site --project-name=MY_PROJECT_NAMELocal-dev (the SiteKit dependency is a local path:, so _Site/ sits inside a subdirectory):
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy <path-to-site>/_Site --project-name=MY_PROJECT_NAMEReplace MY_PROJECT_NAME with your project name and <path-to-site> with the checkout path (e.g. Content/Website).
In SiteConfig.yaml, baseURL must be the URL the site is actually served from (no trailing slash) – e.g. https://my-site.pages.dev or https://example.com. A mismatch silently breaks canonical URLs, the sitemap, and Open Graph image URLs. If you serve the site under a subpath, include it. (See ../../siteconfig-reference.md.)
- Cloudflare Pages → your project → Custom domains → Set up a custom domain
- Enter the domain – use the bare apex where you can (e.g.
example.com) - DNS:
- Domain already on Cloudflare DNS → the record is added automatically
- External DNS (Namecheap, IONOS, etc.) → add a
CNAMEpointing toMY_PROJECT_NAME.pages.dev
- SSL provisions automatically once DNS propagates – a "pending" state for a few minutes (up to ~30) is normal.
Then update baseURL in SiteConfig.yaml to the custom domain and redeploy.
After a deploy, open the live URL and confirm:
https://MY_PROJECT_NAME.pages.dev(or your custom domain) renders/feed.xmland/sitemap.xmlload- a missing path (e.g.
/nope) shows the/404page - assets load (no broken images/CSS) – if they 404,
baseURLis likely wrong
- Empty / broken site: the upload directory must be
_Site(notdistorpublic). Deploying the wrong folder ships an empty site. - Assets 404 / wrong links:
baseURLdoesn't match the deployed URL – fix it inSiteConfig.yamland redeploy. - Preview deployments: with Path 2, pushes to non-production branches deploy to
<branch>.<project>.pages.devautomatically – handy for reviewing before merging. - Rollback: dashboard → your Pages project → Deployments → pick a previous deployment → Rollback.
- Free tier: unlimited bandwidth and a generous build/deploy allowance – ample for a typical SiteKit site; it does not silently upgrade you to a paid plan.
../SKILL.md– the full deploy orchestrator (choose CI + host, wire them together).../ci/github-actions.md– the GitHub Actions workflow that installs Swift, builds, and runs the deploy step above.../../performance.md– CDN / cache-header considerations;../../seo-aso.md– sitemap/robots discoverability.