Skip to content
Phạm Nam

Search

Type a keyword to search the posts.

Go back
en

Deploying Astro to Cloudflare Pages: from setup to a custom domain

A practical guide to deploying a static Astro blog on Cloudflare Pages: setup, builds, GitHub integration, custom domains, admin protection, and troubleshooting.

11 min read—

Astro is a good fit for a personal blog: content is written in Markdown, pages are prerendered as static HTML, JavaScript is kept light, and deployment to a CDN is straightforward. Cloudflare Pages completes the picture: connect a GitHub repository, build on each commit, and distribute static files through Cloudflare’s network.

This article records the workflow I was using for this blog, from creating an Astro project to serving a custom domain over HTTPS. The steps are not tied to a particular template; you can use them for a blog, portfolio, or documentation site.

This article focuses on a static site, the simplest and least expensive approach. If you need SSR, API routes, or dynamic request handling, see the section near the end. The configuration described here reflects the original article dated October 24, 2026; it is not a claim that the current production site still uses Pages.

What you will build

The overall flow (labels in the original diagram are kept unchanged):

Máy local ──git push──> GitHub ──webhook──> Cloudflare Pages
                                             │
                                             ├─ pnpm build
                                             ├─ tạo thư mục dist/
                                             └─ deploy lên CDN
                                                       │
Tên miền riêng ──HTTPS──> Cloudflare CDN <─────────────┘

Your local machine pushes to GitHub; a webhook triggers Pages to build dist/ and deploy it to the CDN, which serves your custom domain over HTTPS. There are four main parts:

  1. Create and test an Astro project locally.
  2. Configure the build and push the code to GitHub.
  3. Connect GitHub to Cloudflare Pages.
  4. Attach a domain, enable HTTPS, and verify the deployment.

1. Prerequisites

You need:

  • Node.js, either LTS or the version required by your project. This blog pins Node 24.22.1.
  • pnpm—this blog uses pnpm 12.15.0. If you do not have it, enable it through Corepack:
    corepack enable
    corepack prepare pnpm@12.15.0 --activate
  • A GitHub account.
  • A Cloudflare account.
  • A domain if you want a custom address. Without one, Cloudflare provides a *.pages.dev address.

Quick checks:

node -v
pnpm -v
git --version

2. Create an Astro project

Create a new project:

pnpm create astro@latest my-blog
cd my-blog
pnpm install
pnpm dev

When prompted, choose Blog for a ready-made layout, RSS, and Markdown setup, or Minimal to build from scratch. You can also pass the template directly:

pnpm create astro@latest my-blog --template blog

Open http://localhost:4321 to preview it. Then try a production build:

pnpm build
pnpm preview

On success, Astro creates dist/. That directory is what Cloudflare Pages deploys. Check it:

ls dist

You should see index.html, asset directories, and possibly rss.xml and sitemap-index.xml, depending on the template.

3. Configure Astro for production

Open astro.config.mjs (or .ts) and set site to your actual domain:

import { defineConfig } from "astro/config";

export default defineConfig({
  site: "https://example.com",
  trailingSlash: "always",
});

Why set site?

  • Astro and integrations such as RSS, sitemap, and canonical URL generation use it to build absolute URLs.
  • Leaving it blank or set to localhost produces incorrect production URLs.
  • If you later change domains, update site, canonicals, and redirects.

trailingSlash: "always" is optional, but choose a style and keep it consistent. This blog uses trailing slashes, such as /posts/ten-bai/. For URLs without a trailing slash, use "never" and check internal links.

Pin Node and pnpm versions

Create .nvmrc at the project root:

24.22.1

Keep the packageManager field in package.json:

{
  "packageManager": "pnpm@12.15.0",
  "engines": {
    "node": ">=24.22.1 <25"
  }
}

Cloudflare Pages reads .nvmrc (or .node-version) to select Node. Build image v3 does not infer the pnpm version from pnpm-lock.yaml, so we will set PNPM_VERSION later. The packageManager field still helps keep local environments aligned.

Add basic security headers

Create public/_headers:

/*
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  X-Frame-Options: DENY
  Permissions-Policy: camera=(), microphone=(), geolocation=()

/admin/*
  X-Robots-Tag: noindex, nofollow
  Cache-Control: no-store

Cloudflare Pages reads this file and applies the headers to the static site. This is only basic protection; use Cloudflare Access and CMS authentication for administration pages as well.

Check the build one last time

pnpm build

If you use Pagefind for search, run it after Astro finishes building:

pnpm build
pnpm dlx pagefind --site dist

Do not run Pagefind before astro build: there is no HTML to index yet.

4. Push the code to GitHub

If the project does not have Git initialized:

git init
git add .
git commit -m "feat: khoi tao blog Astro"
git branch -M main
git remote add origin https://github.com/<user>/<repo>.git
git push -u origin main

A few notes:

  • The repository can be private; Pages can connect if you grant Cloudflare’s GitHub App access.
  • Always commit pnpm-lock.yaml for reproducible dependency resolution.
  • Do not commit dist/, node_modules/, or .env files containing secrets.
  • If you use GitHub Actions, you can still let Pages build the site; an extra workflow is unnecessary unless you want tests to run first.

5. Connect Cloudflare Pages

In the Cloudflare dashboard:

  1. Select Workers & Pages.
  2. Select Create application → Pages → Connect to Git.
  3. Grant Cloudflare access to the GitHub repository.
  4. Select the repository and click Begin setup.

Enter these settings:

FieldValue
Project namemy-blog (or your preferred name)
Production branchmain
Framework presetAstro
Build commandpnpm build
Build output directorydist
Root directory/ (or the relevant subdirectory in a monorepo)

Cloudflare may fill in npm run build when you select Astro. Change it to pnpm build if your repository uses pnpm.

Environment variables

For a static site, environment variables apply only at build time. To pin versions:

NODE_VERSION=24.22.1
PNPM_VERSION=12.15.0

Set only what is needed. Remember:

  • Astro variables prefixed with PUBLIC_ are embedded in client-side JavaScript. Never put API keys, tokens, or passwords in PUBLIC_*.
  • A static site has no server runtime in which to hide secrets. If you need secrets, consider SSR/Workers or a separate backend.
  • Pages supports separate Production and Preview variables; do not share secrets between the environments.

Click Save and Deploy and watch the build log. On success, you receive a URL such as:

https://my-blog.pages.dev

Open it and check several pages before attaching your domain.

Alternative: deploy directly with Wrangler

If you do not want to connect GitHub yet, build locally and upload:

pnpm build
pnpm dlx wrangler pages deploy dist --project-name my-blog

This is quick but does not automatically build on pushes. Git integration is more convenient for a regularly updated blog.

6. Attach a custom domain

In your Pages project:

  1. Open Custom domains.
  2. Add the root domain, such as example.com.
  3. Also add www.example.com if visitors should be able to use www.
  4. Follow the DNS instructions Cloudflare displays.

If the domain uses Cloudflare nameservers, record creation is usually almost automatic. If DNS is hosted elsewhere:

  • Create a CNAME for www pointing to my-blog.pages.dev.
  • For the root (apex) domain, use ALIAS/ANAME or your DNS provider’s CNAME flattening mechanism. Cloudflare supports CNAME at the apex.
  • Wait for the custom domain to become Active and the SSL certificate to be issued.

Do not immediately assume the site is broken if a newly added domain shows 522 or an SSL error. DNS and certificates can take a few minutes; first check the status in Pages.

Choose a canonical URL

Choose one of these:

https://example.com
https://www.example.com

Then 301 redirect the other to it. In Cloudflare, use Rules → Redirect Rules:

  • Condition: hostname equals www.example.com.
  • Destination: https://example.com plus the current path.
  • Enable Preserve query string.
  • Use status 301 for a permanent redirect.

Check it:

curl -I "https://www.example.com/posts/ten-bai/?x=1"

Expect 301 and a Location pointing to https://example.com/posts/ten-bai/?x=1.

Make sure site in astro.config uses the canonical URL so canonical links, RSS, and sitemaps do not generate www addresses.

7. Updates and rollback

After connecting Git, pushes to main trigger production builds. Other branches can create preview deployments with separate URLs, useful for checking changes before merging.

Good habits:

  • Push small changes frequently. Build failures surface sooner.
  • Use preview branches for substantial layout or configuration changes.
  • Do not edit files directly in dist/. They are generated output and will be overwritten on the next build.
  • Never commit secrets. If one is committed, treat it as exposed and rotate it immediately; deleting the file is not enough.

Skip a build when appropriate

Cloudflare Pages skips builds when commit messages contain tokens such as [skip ci]. This blog uses [skip ci] for documentation-only commits and draft saves from the CMS.

For example:

git commit -m "docs: cap nhat ghi chu [skip ci]"

Use this only when no deployment is needed. If an important change must reach production, do not add the token.

Rollback

In Pages → Deployments, select a previous production deployment and click Rollback. Cloudflare serves that build again.

Rollback changes the served deployment, not the repository. If the bug remains in source, the next build brings it back. Revert the commit or fix the source and redeploy for a lasting solution.

8. Post-deployment checks

Do not check only the homepage. A short checklist:

  • / returns 200 and the layout looks right.
  • A real post returns 200 at the correct URL with the correct canonical.
  • /rss.xml returns 200 and includes the latest post.
  • /sitemap-index.xml returns 200 with canonical-domain URLs.
  • A nonexistent page returns 404, not the homepage.
  • HTTPS is valid, with no certificate warning.
  • www redirects to the main domain with 301, preserving path and query.
  • Images, CSS, and JavaScript load without console 404 errors.
  • The OG image appears correctly when sharing a link.
  • Search works if you use Pagefind.
  • Mobile layout and Lighthouse results are acceptable; check at least the homepage and one long post.
  • /admin, if you have a CMS, is protected rather than relying on noindex.

Quick checks with curl:

curl -I https://example.com/
curl -I https://example.com/rss.xml
curl -I https://example.com/sitemap-index.xml
curl -I https://example.com/khong-ton-tai/

9. Common problems

SymptomCommon causeFix
pnpm: not foundBuild image has not enabled pnpm/CorepackSet PNPM_VERSION, add corepack enable, or use npm if the project does not require pnpm
Node version build error.nvmrc or NODE_VERSION mismatchCheck logs and set a Node major supported by Pages
Build succeeds but site returns 404Incorrect output or root directorySet output to dist, root to / (or the correct subdirectory)
CSS/JS/images return 404Incorrect site, base, or trailingSlashCheck astro.config, keep paths consistent, inspect actual HTML URLs
New domain shows 522/SSL pendingDNS is wrong or certificate is not issuedCheck domain status and CNAME/ALIAS records, then allow more time
Content unchanged after pushSkipped build, wrong branch, or cacheCheck for [skip ci], inspect the latest deployment, hard-refresh
RSS/sitemap points to localhostMissing site in Astro configurationSet the real domain and rebuild
Search returns no resultsPagefind ran before Astro builtRun astro build before indexing dist
www redirect loopReversed redirect or wrong SSL/TLS modeKeep the redirect one-way, www → apex; do not add the reverse rule
Environment variable missing in browserNot PUBLIC_*, or a static siteUse PUBLIC_* only for public values; handle secrets on a backend

An easily missed mistake: do not add an SPA fallback such as /* /index.html 200 to a multi-page Astro site. It turns invalid URLs into the homepage and breaks SEO and 404 handling. Astro already generates static HTML files; no fallback is needed unless you are actually building an SPA.

10. When do you need Workers or SSR?

Pages is enough for a static blog. Consider Workers/SSR only when:

  • Pages need data that changes on each request.
  • You need API routes, server-side authentication, webhooks, or form handling.
  • You want to use Cloudflare bindings (D1, KV, R2, AI) directly in Astro.

Then add Astro’s Cloudflare adapter, switch output to server mode, and deploy through Workers rather than uploading only static files. Set secrets through wrangler secret or the dashboard, not PUBLIC_* variables.

Do not add an SSR adapter merely to be “modern.” Static sites build quickly, have fewer runtime failures, cost less, and are easier to cache.

11. Security and cost

A static blog on Cloudflare Pages will often fit within the Free plan:

  • No always-on server to patch.
  • Content served through the CDN.
  • Costs mainly come from the domain and external media storage, if any.

A few principles:

  • Keep secrets out of the repository. Even in a private repository, they remain in Git history.
  • Use Cloudflare Access for /admin and management pages; noindex is not security.
  • Enable 2FA for GitHub and Cloudflare.
  • Pin dependencies and update through branches/previews rather than directly changing production.
  • Back up content and media; deployment rollback is not a backup.
  • Monitor build failures so a broken site does not go unnoticed for days.

12. Applying this to this blog

At the time of the original article, this blog used AstroPaper with Astro, pnpm, and Pagefind. The configuration was (the original labels are retained):

Node:            24.22.1
pnpm:            12.15.0
Build command:   pnpm build
Output:          dist
Domain chính:    https://phamnam.me
www:             301 → https://phamnam.me

The repository’s pnpm build script does more than run astro build: it also checks types, creates the Pagefind search index, and copies required assets. On Pages, a single pnpm build command is therefore sufficient; no extra manual build step is needed.

The /admin/ area sits behind Cloudflare Access, and the CMS has its own GitHub login. This is a two-layer model: Access blocks entry at the edge, while the CMS checks repository write permissions. Neither layer replaces the other.

Summary

The short workflow:

  1. Create an Astro project, run pnpm build, and inspect dist/.
  2. Set site, pin Node/pnpm, and commit the lockfile.
  3. Push to GitHub.
  4. Connect the repository to Pages; use pnpm build and output directory dist.
  5. Attach a domain, enable HTTPS, choose the canonical URL, and 301 redirect www.
  6. Check RSS, sitemap, 404 handling, canonicals, and mobile layout after each deployment.

Cloudflare Pages does not automatically make your blog better, but it handles delivery well: Git-based builds, a global CDN, HTTPS, and rollback are available out of the box. The rest—content, structure, and the reading experience—is still your responsibility.

References


Bình luận

Bình luận được duyệt trước khi hiển thị.

Đang tải bình luận…

Để lại bình luận