Le Duy Khuong

Chuỗi: seo-cloudflare · Phần 2

Năng suất & công cụ dev

Environment Variables & Secrets

Build-time env vars, security

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:

PrefixAvailabilityVí dụ
NEXT_PUBLIC_*Client-side (browser) + ServerNEXT_PUBLIC_GA4_ID
No prefixServer-side ONLYDATABASE_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
VariableComponent dùngBaked into
NEXT_PUBLIC_GA4_IDGA4Script.tsxJS bundle
NEXT_PUBLIC_POSTHOG_KEYPostHogProvider.tsxJS bundle
NEXT_PUBLIC_POSTHOG_HOSTPostHogProvider.tsxJS bundle
NEXT_PUBLIC_GSC_VERIFICATIONapp/layout.tsx metadataHTML <meta>
NEXT_PUBLIC_CF_ANALYTICS_TOKENapp/layout.tsxJS bundle
NEXT_PUBLIC_SITE_URLlib/seo.ts, lib/site.tsHTML + 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?

VariableExposed?OK?Lý do
GA4 IDPublic by design — mọi website expose GA ID
PostHog keyPublic key — PostHog design cho client-side
GSC verificationVerification token, not secret
CF Analytics tokenClient-side analytics token
Site URLPublic 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:3000

Local dev: Analytics vars rỗng → GA4Script return null, AnalyticsProvider skip loading. Hostname check (leduykhuong.com) cũng prevent analytics on localhost.

Double protection:

  1. Env vars rỗng → scripts không render
  2. 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 out

Option 2: Inline env vars

NEXT_PUBLIC_GA4_ID=G-XXX NEXT_PUBLIC_SITE_URL=https://leduykhuong.com npm run build

Option 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.com

Common 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 -5

Câ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.local

Câ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 -10

Câ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.local git-ignored, .env-secret is 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.

LDK

Le Duy Khuong

AI Transformation & Digital Strategy. Writing about agentic systems, engineering leadership, and building in public.