2026-03-205 phút đọcVI
Environment Variables & Secrets trên Cloudflare Pages
Mở đầu
leduykhuong.com cần nhiều API keys: GA4 Measurement ID, PostHog API key, Google Search Console verification token, Cloudflare Analytics token. Những giá trị này KHÔNG được hardcode trong code — chúng phải là environment variables.
Nhưng static export tạo ra thách thức: không có server runtime. Env vars phải được inject tại build time. Hiểu cách này hoạt động quan trọng cho cả security và functionality.
Mục tiêu: Hiểu build-time vs runtime env vars, NEXT_PUBLIC_ prefix convention, và cách quản lý secrets cho CF Pages deployment.
Build-time vs Runtime — Khác biệt quan trọng
Server-rendered app (Vercel, Node.js)
Request → Server reads process.env.SECRET → Response
Env vars available tại runtime. Thay đổi env var → restart server → take effect.
Static export (CF Pages)
Build: process.env.NEXT_PUBLIC_GA4_ID → baked into JS bundle → Deploy
Request → CDN serves static JS (env vars already embedded)
Env vars baked into code tại build time. Thay đổi env var → rebuild + redeploy.
Implication: Mọi env var trên leduykhuong.com phải có giá trị TRƯỚC khi npm run build. Nếu quên set → value = undefined → feature không hoạt động trên production.
NEXT_PUBLIC_ Prefix — Client-side Exposure
Next.js convention:
| Prefix | Availability | Ví dụ |
|---|---|---|
NEXT_PUBLIC_* | Client-side (browser) + Server | NEXT_PUBLIC_GA4_ID |
| No prefix | Server-side ONLY | DATABASE_URL |
Cho static export: ONLY NEXT_PUBLIC_* vars matter. Vì không có server, non-prefixed vars không accessible at runtime.
leduykhuong.com env vars
# .env hoặc .env.local (NOT committed to git)
NEXT_PUBLIC_GA4_ID=G-XXXXXXXXXX
NEXT_PUBLIC_POSTHOG_KEY=phc_XXXXXXXXXXXX
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
NEXT_PUBLIC_GSC_VERIFICATION=abc123def456
NEXT_PUBLIC_CF_ANALYTICS_TOKEN=xyz789
NEXT_PUBLIC_SITE_URL=https://leduykhuong.com| Variable | Component dùng | Baked into |
|---|---|---|
NEXT_PUBLIC_GA4_ID | GA4Script.tsx | JS bundle |
NEXT_PUBLIC_POSTHOG_KEY | PostHogProvider.tsx | JS bundle |
NEXT_PUBLIC_POSTHOG_HOST | PostHogProvider.tsx | JS bundle |
NEXT_PUBLIC_GSC_VERIFICATION | app/layout.tsx metadata | HTML <meta> |
NEXT_PUBLIC_CF_ANALYTICS_TOKEN | app/layout.tsx | JS bundle |
NEXT_PUBLIC_SITE_URL | lib/seo.ts, lib/site.ts | HTML + JSON-LD |
Security — Cái gì exposed?
NEXT_PUBLIC_* values visible trong browser:
# Ai cũng có thể thấy GA4 ID
curl -s https://leduykhuong.com | grep 'G-'
# → G-XXXXXXXXXX
# PostHog key cũng visible
curl -s https://leduykhuong.com | grep 'phc_'Đây có phải security risk?
| Variable | Exposed? | OK? | Lý do |
|---|---|---|---|
| GA4 ID | ✅ | ✅ | Public by design — mọi website expose GA ID |
| PostHog key | ✅ | ✅ | Public key — PostHog design cho client-side |
| GSC verification | ✅ | ✅ | Verification token, not secret |
| CF Analytics token | ✅ | ✅ | Client-side analytics token |
| Site URL | ✅ | ✅ | Public domain |
Tất cả NEXT_PUBLIC_* vars trên leduykhuong.com đều safe to expose. Chúng là client-side tokens, designed to be public.
Cái gì KHÔNG được expose
# ❌ NEVER prefix these with NEXT_PUBLIC_
# DATABASE_URL=postgres://...
# API_SECRET_KEY=sk-...
# ADMIN_PASSWORD=...leduykhuong.com không có server-side secrets (static site, no database, no admin). Nếu tương lai thêm API routes → secrets phải KHÔNG có NEXT_PUBLIC_ prefix.
Local Development — .env.local
# .env.local (git-ignored, local only)
NEXT_PUBLIC_GA4_ID=
NEXT_PUBLIC_POSTHOG_KEY=
NEXT_PUBLIC_CF_ANALYTICS_TOKEN=
NEXT_PUBLIC_SITE_URL=http://localhost:3000Local dev: Analytics vars rỗng → GA4Script return null, AnalyticsProvider skip loading. Hostname check (leduykhuong.com) cũng prevent analytics on localhost.
Double protection:
- Env vars rỗng → scripts không render
- AnalyticsProvider hostname check → scripts không init
Production Build — Inject Env Vars
Option 1: .env.local file (manual deploy)
# Create .env.local with production values
cat > .env.local << 'EOF'
NEXT_PUBLIC_GA4_ID=G-XXXXXXXXXX
NEXT_PUBLIC_POSTHOG_KEY=phc_XXXXXXXXXXXX
NEXT_PUBLIC_SITE_URL=https://leduykhuong.com
EOF
# Build
npm run build
# Deploy
npx wrangler pages deploy outOption 2: Inline env vars
NEXT_PUBLIC_GA4_ID=G-XXX NEXT_PUBLIC_SITE_URL=https://leduykhuong.com npm run buildOption 3: CF Pages build settings (nếu dùng CD)
CF Pages Dashboard → Settings → Environment variables:
- Set key-value pairs
- CF Pages inject chúng during build
leduykhuong.com dùng Option 1 — .env.local chứa production values, git-ignored.
Verify Env Vars trong Build Output
Sau build, verify env vars đã inject đúng:
# Check GA4 ID baked in
grep -r 'G-' out/ --include="*.js" | head -1
# → Should find GA4 ID
# Check site URL in sitemap
head -5 out/sitemap.xml
# → URLs should start with https://leduykhuong.com
# Check GSC verification
grep 'google-site-verification' out/index.html
# → Should show verification token
# Check canonical URLs
grep 'canonical' out/vi/blog/learning-in-public.html
# → Should use https://leduykhuong.comCommon error: Build mà quên set NEXT_PUBLIC_SITE_URL → canonical URLs sai → SEO broken.
.env-secret Pattern
leduykhuong.com repo có .env-secret tại root — chứa TẤT CẢ env vars cho mọi projects trong monorepo (Anthropic API, OpenAI, Supabase, Upstash, Vercel, etc.).
# .env-secret (git-ignored, local only)
# === leduykhuong-site ===
NEXT_PUBLIC_GA4_ID=G-XXXXXXXXXX
NEXT_PUBLIC_POSTHOG_KEY=phc_XXXXXXXXXXXX
...
# === Other projects ===
ANTHROPIC_API_KEY=<your-key-here>
OPENAI_API_KEY=<your-key-here>
Workflow: Copy relevant vars từ .env-secret vào project .env.local trước build.
Security: .env-secret trong .gitignore + never committed. Backup nằm ngoài git.
Thực hành
Bài tập 1: Check exposed vars
# After build
grep -rn 'NEXT_PUBLIC' out/ --include="*.js" | head -5Câu hỏi: Env var values có trong JS output không? Cái nào visible?
Bài tập 2: Build without env vars
# Rename .env.local temporarily
mv .env.local .env.local.bak
npm run build
# Check sitemap — what URL does it use?
head -5 out/sitemap.xml
# Restore
mv .env.local.bak .env.localCâu hỏi: Khi không có NEXT_PUBLIC_SITE_URL, sitemap URL là gì? (Hint: check fallback in code)
Bài tập 3: Verify analytics conditional loading
# Check GA4 conditional
grep -B2 -A5 'GA4_ID' out/_next/static/**/*.js 2>/dev/null | head -10Câu hỏi: Code có check null trước khi load GA4? Pattern nào?
Tóm tắt
- Build-time injection — Static export bakes env vars vào output. Thay đổi → rebuild + redeploy.
NEXT_PUBLIC_*— Only prefixed vars available client-side. Unprefixed = server-only (irrelevant for static).- All leduykhuong.com vars safe to expose — GA4 ID, PostHog key, GSC verification = public tokens by design.
- Double protection — Empty env vars (dev) + hostname check (AnalyticsProvider) = no analytics on localhost.
.env.localgit-ignored,.env-secretis monorepo-level secret store.- Verify after build — Always check sitemap URLs, canonical, analytics IDs in output.
Bài tiếp theo
Bài 22: Managed Features & Gotchas — CF Pages tự quản lý nhiều thứ (SSL, compression, caching). Bài cuối cover những gì CF Pages làm tự động, gotchas khi deploy Next.js static export, và troubleshooting common issues.