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:
- Create and test an Astro project locally.
- Configure the build and push the code to GitHub.
- Connect GitHub to Cloudflare Pages.
- 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.devaddress.
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
localhostproduces 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.yamlfor reproducible dependency resolution. - Do not commit
dist/,node_modules/, or.envfiles 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:
- Select Workers & Pages.
- Select Create application → Pages → Connect to Git.
- Grant Cloudflare access to the GitHub repository.
- Select the repository and click Begin setup.
Enter these settings:
| Field | Value |
|---|---|
| Project name | my-blog (or your preferred name) |
| Production branch | main |
| Framework preset | Astro |
| Build command | pnpm build |
| Build output directory | dist |
| 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 inPUBLIC_*. - 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:
- Open Custom domains.
- Add the root domain, such as
example.com. - Also add
www.example.comif visitors should be able to usewww. - 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
wwwpointing tomy-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.complus 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:
-
/returns200and the layout looks right. - A real post returns
200at the correct URL with the correct canonical. -
/rss.xmlreturns200and includes the latest post. -
/sitemap-index.xmlreturns200with canonical-domain URLs. - A nonexistent page returns
404, not the homepage. - HTTPS is valid, with no certificate warning.
-
wwwredirects to the main domain with301, 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 onnoindex.
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
| Symptom | Common cause | Fix |
|---|---|---|
pnpm: not found | Build image has not enabled pnpm/Corepack | Set PNPM_VERSION, add corepack enable, or use npm if the project does not require pnpm |
| Node version build error | .nvmrc or NODE_VERSION mismatch | Check logs and set a Node major supported by Pages |
| Build succeeds but site returns 404 | Incorrect output or root directory | Set output to dist, root to / (or the correct subdirectory) |
| CSS/JS/images return 404 | Incorrect site, base, or trailingSlash | Check astro.config, keep paths consistent, inspect actual HTML URLs |
New domain shows 522/SSL pending | DNS is wrong or certificate is not issued | Check domain status and CNAME/ALIAS records, then allow more time |
| Content unchanged after push | Skipped build, wrong branch, or cache | Check for [skip ci], inspect the latest deployment, hard-refresh |
| RSS/sitemap points to localhost | Missing site in Astro configuration | Set the real domain and rebuild |
| Search returns no results | Pagefind ran before Astro built | Run astro build before indexing dist |
www redirect loop | Reversed redirect or wrong SSL/TLS mode | Keep the redirect one-way, www → apex; do not add the reverse rule |
| Environment variable missing in browser | Not PUBLIC_*, or a static site | Use 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
/adminand management pages;noindexis 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:
- Create an Astro project, run
pnpm build, and inspectdist/. - Set
site, pin Node/pnpm, and commit the lockfile. - Push to GitHub.
- Connect the repository to Pages; use
pnpm buildand output directorydist. - Attach a domain, enable HTTPS, choose the canonical URL, and 301 redirect
www. - 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.
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