editor.vpn.com (Cloudflare Worker)
↕ reads/writes
R2 Bucket (vpn-content) — draft storage only
↕ publishes to
Git Repo (spp-ben/vpn-astro) — source of truth
↕ builds to
Astro → dist/ (static HTML)
↕ deployed to
Cloudflare Pages (astro.vpn.com → vpn.com)
↕ sits behind
vpn-translate Worker (language redirect)
Git is the single source of truth. R2 holds editor drafts only — the build always reads from git.
Edit files → npm run build → node scripts/fix-affiliate-rels.mjs → wrangler pages deploy dist/
Or: bash scripts/deploy.sh
Edit on editor.vpn.com → Save (draft in R2) → Publish
→ Durable Object translates content (if translate: true)
→ Commits all language files to GitHub atomically
→ Deploy triggered → Live
The Durable Object handles slug/focusKeyword translation via Sonnet and body translation via Haiku. R2 saves never affect the live site until published.
All Workers run on the vpn.com zone and intercept requests before Cloudflare Pages.
| Worker | Domain | Purpose |
|---|---|---|
vpn-translate | vpn.com/* | Detects browser language, redirects to /fr/, /es/, etc. |
vpn-editor | editor.vpn.com | Content editor SPA, R2 CRUD, deploy triggers |
vpn-redirects | (service binding) | URL redirects from KV |
/out Worker | vpn.com/out* | Affiliate link redirects |
vpn-forms | vpn.com/api/* | Form submissions |
Request flow: User visits vpn.com/vpn/nordvpn/ → vpn-translate checks browser language → if French: 302 to /fr/vpn/nordvpn/ → otherwise: pass through to Cloudflare Pages static HTML.
all configured languages (English + the translated set in src/config/languages.ts). Everything is pre-built static HTML — no runtime translation.
| Source | What it covers | Tool |
|---|---|---|
| Markdown content | Article body, title, description, slug | Durable Object or translate-md.mjs |
| UI strings | Nav, buttons, labels | src/i18n/translations.ts (manually curated) |
| Provider data | Taglines, plans, review content | scripts/translate-provider-json.mjs |
Language config: src/config/languages.ts — single source of truth.
Languages: 16 translated locales — fr, de, es, pt, it, nl, ja, ar, da, fa, zh, ko, id, sv, tr, vi (+ en). The list in languages.ts is authoritative; if this line drifts, trust the config.
Translated pages use translated slugs — /fr/vpn/qu-est-ce-qu-un-vpn/, not /fr/vpn/what-is-a-vpn/. Route map (src/i18n/routes.ts) resolves URLs. Components use resolveUrl() — never a bare /{lang}/ prefix.
Dynamic routes: src/pages/[lang]/ generates translated hub pages from the language config. 6 files replace what used to be 54 per-language wrappers.
| Operation | Model | Cost |
|---|---|---|
| Translate one new post | Haiku + Sonnet | ~$0.05 |
| Weekly translation update | Haiku | ~$1-2 |
| Full site re-translation | Haiku | ~$15 |
All provider data in src/data/providers/{slug}.json. Contains: name, tagline, affiliate link, server/country counts, Speed Lab data, Trust Score (100 pts across 11 criteria), plans, review sections, JSON-LD.
Providers ranked by Trust Score, not speed. Adding a provider: JSON + content page + 4 SVGs. See /ops/adding-providers/.
| URL | Purpose |
|---|---|
astro.vpn.com | Staging |
vpn.com | Production |
editor.vpn.com | Content editor (Cloudflare Access protected) |
| File | Purpose |
|---|---|
src/data/providers/*.json | Provider data |
src/data/providers.ts | Provider data API |
src/content/posts/*.md | Blog posts |
src/content/pages/*.md | Pages |
src/layouts/Provider.astro | Provider review template |
src/layouts/Editorial.astro | Blog post template |
src/components/SpeedLab.astro | Speed Lab table |
src/components/ContentSection.astro | Content + sidebar layout |
src/config/languages.ts | Language list |
src/i18n/translations.ts | UI string translations |
src/i18n/routes.ts | Route map for localized URLs |
src/data/providers/i18n/{lang}/ | Provider translation overlays |
src/pages/[lang]/ | Dynamic translated hub routes |
scripts/translate-provider-json.mjs | Translate provider overlays |
scripts/translate-md.mjs | Bulk translate markdown |
scripts/translation-qa.mjs | Checks for English leaks |
scripts/fix-affiliate-rels.mjs | Post-build affiliate link fix |
public/print.css | Print stylesheet (standalone, not bundled) |
public/llms.txt | AI crawler index |
vpn.com → vpn-translate Worker → Cloudflare Pages
astro.vpn.com → CNAME → vpn-astro.pages.dev
editor.vpn.com → Cloudflare Worker (Cloudflare Access protected)
At production cutover: the vpn-translate Worker changes its origin from Cloudways to Cloudflare Pages. No DNS changes needed.