Nội dung được biên soạn lại từ Anh Nguyen - kèm nguồn bài viết (nếu có)
(Từ A → Z: GitHub Pages, Vercel, Render, DO App Platform, Cloudflare Pages, Netlify, Neocities, Surge, Wasmer)
Đây là bài tổng hợp lại toàn bộ quá trình mình triển khai blog cá nhân với Astro (sử dụng Sveltia CMS), build từ Repo A Private, publish sang Repo B Public (anhnnp.github.io), sau đó tự động multi deploy ra hàng loạt nền tảng hosting khác nhau – tất cả chỉ với 1 lần push.

Bài viết phù hợp với anh em muốn:
- Giữ source code private
- Chỉ public /dist/ ra các host tĩnh
- Dùng GitHub Actions làm CI/CD trung tâm
- Deploy 1 lần & xuất bản ở 5 - 10 nền tảng khác nhau
- Chuẩn SEO, có sitemap đúng baseURL theo từng host
- Chống cache bằng cache-busting (?v=RUN-SHA)
1. Kiến trúc tổng thể
🔹 Repo A – Private
Chứa:
- Source Astro
- Markdown
- Theme
- Sveltia CMS
- Code build (vite)
→ Repo này là trung tâm điều khiển: build & deploy.
🔹 Repo B – Public (anhnnp.github.io)
Chỉ chứa:
- /dist/ sau build
- index.html, asset, sitemap, css/js đã fingerprint và cache-busting
- Không chứa source
→ Repo B được dùng làm “artifact host” cho GitHub Pages và các nền tảng dạng “pull from repo” như Render, DigitalOcean, Wasmer.
🔹 CI/CD Flow
Repo A (private)
│
├── GitHub Actions build Astro
│
├── Chạy cache-busting assets
│
├── Push dist/ ➝ Repo B (public)
│
├── Trigger:
│ ├── Render Deploy Hook
│ ├── DO App Deployment
│ ├── Wasmer Auto Deploy
│
├── Direct Upload:
│ ├── Vercel
│ ├── Cloudflare Pages
│ ├── Netlify
│ ├── Neocities
│ ├── Surge
│
└── Multi-deploy hoàn tất.
2. Chuẩn bị Astro project
Tạo project
npm create astro@latest
npm install
Cài thêm modules cần thiết
npm i -D cross-env
npm i @astrojs/sitemap unocss
3. Cấu hình astro.config.js để linh hoạt BASE_URL
Đây là phiên bản chuẩn giúp thay đổi baseURL dựa trên biến môi trường:
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
import UnoCSS from 'unocss/astro';
function slash(url) {
if (!url) return undefined;
return url.endsWith('/') ? url : `${url}/`;
}
export default defineConfig({
site: slash(process.env.BASE_URL) || 'https://anhnnp.github.io/',
trailingSlash: 'always',
integrations: [sitemap(), UnoCSS({ injectReset: true })],
});
→ Lúc build, CI đặt BASE_URL tương ứng với host:
- GitHub Pages →
BASE_URL=https://anhnnp.github.io/ - Render →
BASE_URL=https://anhnnp.onrender.com/ - Vercel →
BASE_URL=https://anhnnp.vercel.app/
4. Package.json: chuẩn hoá build
"scripts": {
"dev": "astro dev",
"build": "cross-env NODE_ENV=production astro build",
"preview": "astro preview"
}
5. Tạo Deploy Key cho Repo B
🔹 Repo B (anhnnp.github.io)
Add Deploy Key (Allow Write)
→ dán public key (.pub)
🔹 Repo A (private)
Add Secret
→ ACTIONS_DEPLOY_KEY = private key
6. Tạo Secrets cho các nền tảng
Luôn đặt trong Repo A:
| Secret | Dùng cho |
|---|---|
| ACTIONS_DEPLOY_KEY | Push dist → Repo B |
| VERCEL_TOKEN | Deploy Vercel |
| VERCEL_SCOPE | username/team |
| VERCEL_PROJECT_NAME | tên project |
| RENDER_DEPLOY_HOOK_URL | Trigger render build |
| DIGITALOCEAN_ACCESS_TOKEN | Trigger DO deploy |
| DO_APP_ID | App ID platform |
| CLOUDFLARE_API_TOKEN | Deploy Cloudflare |
| CLOUDFLARE_ACCOUNT_ID | CF account |
| CLOUDFLARE_PROJECT_NAME | Project CF Pages |
| NETLIFY_AUTH_TOKEN | Netlify |
| NETLIFY_SITE_ID | Netlify |
| NEOCITIES_API_TOKEN | Neocities |
| SURGE_TOKEN | Surge |
| SURGE_DOMAIN | Surge domain |
7. Workflow CI/CD HOÀN CHỈNH
“Build mỗi host một baseURL khác nhau” – chuyên nghiệp nhất
Bản workflow này:
- Build SỐ LẦN TƯƠNG ỨNG số host (để site đúng URL từng nền tảng)
- Auto cache-busting (?v=RUN-SHA)
- Push dist → Repo B
- Trigger Render & DO
- Upload trực tiếp lên Vercel, Netlify, Cloudflare, Neocities, Surge
FULL WORKFLOW (bản hoàn chỉnh nhất)
📍 Vị trí file: .github/workflows/deploy-multi-static.yml
📍 Tên file: Multi host Build & Deploy (Astro)
name: Multi-host Build & Deploy (Astro)
on:
push:
branches: [ main ]
workflow_dispatch:
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: true
jobs:
build-deploy:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- target: github
base_url: https://anhnnp.github.io/
- target: vercel
base_url: https://anhnnp.vercel.app/
- target: render
base_url: https://anhnnp.onrender.com/
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- name: Set BASE_URL for host
run: echo "BASE_URL=${{ matrix.base_url }}" >> $GITHUB_ENV
- name: Build Astro
run: npm run build
env:
NODE_ENV: production
BASE_URL: ${{ matrix.base_url }}
# Cache-busting
- name: Add version query-string
shell: bash
run: |
VERSION="${GITHUB_RUN_NUMBER}-${GITHUB_SHA::7}"
find dist -type f -name "*.html" -print0 | while IFS= read -r -d '' f; do
perl -0777 -i -pe "s/href=\"(?!https?:\/\/)([^\"?]+\.css)\"/href=\"\$1?v=$VERSION\"/g;
s/src=\"(?!https?:\/\/)([^\"?]+\.js)\"/src=\"\$1?v=$VERSION\"/g;
s/src=\"(?!https?:\/\/)([^\"?]+\.(?:png|jpe?g|gif|webp|svg))\"/src=\"\$1?v=$VERSION\"/g;" "$f"
done
touch dist/.nojekyll
# Deploy GitHub Pages
- name: Deploy to GitHub Pages repo (Repo B)
if: matrix.target == 'github'
uses: peaceiris/actions-gh-pages@v3
with:
deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
external_repository: anhnnp/anhnnp.github.io
publish_dir: ./dist
publish_branch: main
# Vercel
- name: Deploy to Vercel
if: matrix.target == 'vercel'
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_SCOPE: ${{ secrets.VERCEL_SCOPE }}
VERCEL_PROJECT_NAME: ${{ secrets.VERCEL_PROJECT_NAME }}
run: |
npm i -g vercel
vercel deploy dist --prod --yes \
--token "$VERCEL_TOKEN" \
--scope "$VERCEL_SCOPE" \
--name "$VERCEL_PROJECT_NAME"
# Render
- name: Trigger Render Deploy Hook
if: matrix.target == 'render'
env:
RENDER_DEPLOY_HOOK_URL: ${{ secrets.RENDER_DEPLOY_HOOK_URL }}
run: curl -fsS -X POST "$RENDER_DEPLOY_HOOK_URL"
Nếu bạn muốn thêm Netlify / Cloudflare / DO / Neocities / Surge → mình đã viết đầy đủ ở các phần trên, chỉ cần ghép thêm vào matrix hoặc sau bước deploy.
⚠️ (Nếu bạn muốn bản tối giản thì hãy xoá những deploy config không cần thiết để chỉ sử dụng các nền tảng các muốn cho lên thôi)
8. Triển khai từng host
(1) GitHub Pages
Repo B → Settings → Pages → Deploy from branch → main.
(2) Render
- Create Static Site
- Source = Repo B
- Build command = blank
- Publish directory =
/
(3) Vercel
- Create Project → “Continue without Git”
- Name = blog
- Output dir = dist
- Deploy bằng CLI + API Token.
(4) DigitalOcean App Platform
- Create App → Static Site
- Source = Repo B
- Build = blank
- Output path =
/
(5) Cloudflare Pages
- Project name = blog
- Deploy từ workflow bằng wrangler-action.
(6) Neocities
- Tạo API key → secret
NEOCITIES_API_TOKEN.
(7) Netlify
- Deploy bằng
netlify deploy.
(8) Surge
surge ./dist domain --token TOKEN.
(9) Wasmer
- GitHub Integration → Repo B.
9. Xử lý cache bằng cache busting
Cơ chế:
- Mỗi build sinh version:
VERSION = GITHUB_RUN_NUMBER + short SHA - Tự động update link:
main.css?v=42-a1b2c3d - Browser/CDN coi đây là asset mới → không cache sai.
10. Cách quản lý baseURL tốt nhất
- Build riêng từng host bằng matrix → đúng canonical từng nơi.
- Sitemap / RSS luôn đúng domain host.
11. Kết luận
Sau tất cả, bạn chỉ cần push lên Repo A
Từ giờ trở đi:
git add .
git commit -m "update bài mới"
git push
→ GitHub Actions tự động:
- Build 1 lần / mỗi host
- Chống cache
- Push dist → Repo B
- Trigger Render / DO
- Deploy thẳng Vercel / Cloudflare / Netlify / Neocities / Surge
- Wasmer tự cập nhật
Blog Astro của bạn luôn đồng bộ trên tất cả nền tảng.

Mở rộng hơn nữa
Template README.md cho Repo A
# anhnnp Astro Blog – CI/CD
## Kiến trúc
- Repo A (private): Source Astro + Sveltia + content
- Repo B (public): `anhnnp.github.io` → chứa **dist** đã build
### Flow
1. Push code lên branch `main` (Repo A)
2. GitHub Actions:
- Set BASE_URL theo host (GitHub/Vercel/Render)
- `npm run build`
- Cache-busting (?v=RUN-SHA)
3. Deploy:
- Push `dist/` → Repo B (`anhnnp.github.io`)
- Trigger Render Deploy Hook
- Deploy Vercel bằng CLI
- (Tuỳ chọn) Netlify, Cloudflare, Neocities, Surge, DO, Wasmer
## CI/CD
Workflow chính: `.github/workflows/deploy-multi-static.yml`
- Trigger: `on: push` vào `main`
- Jobs:
- `build-deploy` (matrix cho github / vercel / render)
## Secrets cần có (Repo A)
- `ACTIONS_DEPLOY_KEY` – deploy Repo B
- `VERCEL_TOKEN`, `VERCEL_SCOPE`, `VERCEL_PROJECT_NAME`
- `RENDER_DEPLOY_HOOK_URL`
- `DIGITALOCEAN_ACCESS_TOKEN`, `DO_APP_ID`
- `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`, `CLOUDFLARE_PROJECT_NAME`
- `NETLIFY_AUTH_TOKEN`, `NETLIFY_SITE_ID`
- `NEOCITIES_API_TOKEN`
- `SURGE_TOKEN`, `SURGE_DOMAIN`
## Dev commands
```bash
npm install
npm run dev # local
npm run build # build production
Tối ưu SEO cho multi domain là gì?
Vì bạn deploy cùng 1 content ra nhiều domain:
https://anhnnp.github.io/https://anhnnp.onrender.com/https://anhnnp.vercel.app/- (và có thể thêm Netlify, Cloudflare…)
→ Về SEO, nếu để vậy thì Google có thể coi là trùng lặp nội dung (duplicate content) giữa nhiều domain.
Chiến lược SEO tốt nhất?
1. Chọn 1 canonical domain
Ví dụ: bạn chọn:
https://anhnnp.github.io/là canonical.
Tức là: Toàn bộ <link rel="canonical"> trong HTML, sitemap.xml, OpenGraph,… đều trỏ về domain này.
Trong Astro, dùng site = canonical:
// astro.config.js
export default defineConfig({
site: 'https://anhnnp.github.io/',
...
});
Khi đó, dù bạn deploy lên Vercel/Render, canonical URL vẫn trỏ về github.io → Google hiểu “đây là bản chính”, các bản kia coi như “bản mirror/CDN”.
2. Sử dụng <link rel="canonical">
Trong layout chính (vd: src/layouts/Layout.astro):
---
const { title = 'Blog', url } = Astro.props;
const canonical = new URL(Astro.url.pathname, Astro.site);
---
<html lang="vi">
<head>
<meta charset="utf-8" />
<title>{title}</title>
<link rel="canonical" href={canonical.toString()} />
</head>
<body>
<slot />
</body>
</html>
Astro tự cung cấp Astro.site từ astro.config.js → canonical luôn đúng.
3. Có nên chặn index các domain phụ không?
Option “hardcore SEO”:
- Cho Google chỉ index canonical bằng cách:
→ Làm vậy Render/Vercel chỉ làm “CDN / mirror cho người dùng”, không cạnh tranh SEO với domain chính.
-
Trên các host phụ (Render, Vercel…), cấu hình
robots.txtnhư:User-agent: * Disallow: / -
Hoặc thêm meta:
-
Cách nhẹ hơn:
- Không chặn index, nhưng canonical luôn trỏ về 1 nơi → Google vẫn hiểu đâu là bản gốc.
4. Multi language / multi region (nâng cao)
Nếu sau này bạn có:
anhnnp.com(EN)anhnnp.vn(VI)
Thì có thể dùng hreflang:
<link rel="alternate" href="https://anhnnp.com/post-x" hreflang="en" />
<link rel="alternate" href="https://anhnnp.vn/post-x" hreflang="vi" />
Nhưng hiện tại bạn chỉ đang nhân bản 1 ngôn ngữ nhiều host → dùng canonical là đủ.
Trải nghiệm tốt nhất từ thực hành — Anh Nguyen
← Danh sách bài viết