MITOM

← Danh sách bài viết

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:

1. Kiến trúc tổng thể

🔹 Repo A – Private

Chứa:

→ Repo này là trung tâm điều khiển: build & deploy.

🔹 Repo B – Public (anhnnp.github.io)

Chỉ chứa:

→ 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:

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 SecretACTIONS_DEPLOY_KEY = private key

6. Tạo Secrets cho các nền tảng

Luôn đặt trong Repo A:

SecretDùng cho
ACTIONS_DEPLOY_KEYPush dist → Repo B
VERCEL_TOKENDeploy Vercel
VERCEL_SCOPEusername/team
VERCEL_PROJECT_NAMEtên project
RENDER_DEPLOY_HOOK_URLTrigger render build
DIGITALOCEAN_ACCESS_TOKENTrigger DO deploy
DO_APP_IDApp ID platform
CLOUDFLARE_API_TOKENDeploy Cloudflare
CLOUDFLARE_ACCOUNT_IDCF account
CLOUDFLARE_PROJECT_NAMEProject CF Pages
NETLIFY_AUTH_TOKENNetlify
NETLIFY_SITE_IDNetlify
NEOCITIES_API_TOKENNeocities
SURGE_TOKENSurge
SURGE_DOMAINSurge 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:

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

(3) Vercel

(4) DigitalOcean App Platform

(5) Cloudflare Pages

(6) Neocities

(7) Netlify

(8) Surge

(9) Wasmer

9. Xử lý cache bằng cache busting

Cơ chế:

10. Cách quản lý baseURL tốt nhất

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:

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:

→ 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”:

Cách nhẹ hơn:

4. Multi language / multi region (nâng cao)

Nếu sau này bạn có:

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
  • Triển khai Blog Astro + CI/CD Đa Nền Tảng

    Triển khai Blog Astro + CI/CD Đa Nền Tảng

    Bài viết mình tổng hợp toàn bộ quá trình triển khai blog cá nhân với Astro (sử dụng Sveltia CMS), multi deploy, CI/CD GitHub Actions, cache busting, multi host (GitHub Pages + Vercel + Render + Netlify + DO + Cloudflare + Neocities + Surge + Wasmer)… từ A → Z.

  • So Sánh toàn diện LocalWP, MAMP, Laragon, XAMPP và WAMP Server Trên Windows

    So Sánh toàn diện LocalWP, MAMP, Laragon, XAMPP và WAMP Server Trên Windows

    Bạn đang tìm môi trường local để phát triển PHP hoặc WordPress trên Windows? Bài viết này sẽ giúp bạn hiểu rõ sự khác biệt giữa LocalWP, Laragon, MAMP, XAMPP, và WAMP Server – để chọn ra công cụ tối ưu nhất cho quy trình làm việc của bạn.

  • Công cụ mã nguồn mở kích hoạt Windows và Office kèm tính năng debug nâng cao

    Công cụ mã nguồn mở kích hoạt Windows và Office kèm tính năng debug nâng cao

    Hỗ trợ các phương thức kích hoạt như HWID, Ohook, KMS38 và Online KMS. Công cụ này cung cấp khả năng xử lý sự cố mạnh mẽ và kích hoạt sản phẩm Microsoft hiệu quả, đáp ứng nhiều nhu cầu người dùng theo cách linh hoạt, có thể tùy biến.

  • Plugin tối ưu ảnh tốt nhất cho Wordpress là gì? Optimole vs ShortPixel vs Imagify vs EWWW vs Smush

    Plugin tối ưu ảnh tốt nhất cho Wordpress là gì? Optimole vs ShortPixel vs Imagify vs EWWW vs Smush

    Đâu là plugin tối ưu ảnh tốt nhất cho Wordpress WooCommerce & blog nhiều ảnh? So sánh thực chiến Optimole, ShortPixel, Imagify, EWWW, Smush - Mình sẽ đi từ phương pháp test → kết quả → phân tích từng plugin → xếp hạng & khuyến nghị theo use case (online store, blog nhiều ảnh).

  • Những cách đơn giản để kích hoạt Windows 11 miễn phí mà không cần product key

    Những cách đơn giản để kích hoạt Windows 11 miễn phí mà không cần product key

    Chia sẻ kích hoạt Windows 11 bằng key KMS client miễn phí gồm các bước hoàn toàn giống với hướng dẫn cách kích hoạt Windows 10 miễn phí, chỉ khác là lần này mình thực hiện trên Windows 11.

  • Miễn phí 9 nền tảng headless CMS lưu trữ trên Git cho các Blog website đơn giản

    Miễn phí 9 nền tảng headless CMS lưu trữ  trên Git cho các Blog website đơn giản

    Đây là một chủ đề cực kỳ hữu ích cho các dev hiện đại, đặc biệt là những ai đang chuyển từ WordPress sang mô hình headless CMS. Nếu CMS truyền thống là tất cả trong một (cả backend lẫn frontend), thì headless CMS chỉ tập trung vào phần quản lý nội dung (backend), còn phần hiển thị (frontend) sẽ do bạn tự do chọn framework hoặc build theo ý muốn.