System Architecture — How VPN.com Works

2026-05-10 · Ben · architecture

System Architecture


The Stack

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.


Content Flow

Local deploy

Edit files → npm run build → node scripts/fix-affiliate-rels.mjs → wrangler pages deploy dist/

Or: bash scripts/deploy.sh

Editor → Publish

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.


Workers

All Workers run on the vpn.com zone and intercept requests before Cloudflare Pages.

WorkerDomainPurpose
vpn-translatevpn.com/*Detects browser language, redirects to /fr/, /es/, etc.
vpn-editoreditor.vpn.comContent editor SPA, R2 CRUD, deploy triggers
vpn-redirects(service binding)URL redirects from KV
/out Workervpn.com/out*Affiliate link redirects
vpn-formsvpn.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.


Translation System

all configured languages (English + the translated set in src/config/languages.ts). Everything is pre-built static HTML — no runtime translation.

SourceWhat it coversTool
Markdown contentArticle body, title, description, slugDurable Object or translate-md.mjs
UI stringsNav, buttons, labelssrc/i18n/translations.ts (manually curated)
Provider dataTaglines, plans, review contentscripts/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.

OperationModelCost
Translate one new postHaiku + Sonnet~$0.05
Weekly translation updateHaiku~$1-2
Full site re-translationHaiku~$15

Provider Data

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/.


Key URLs

URLPurpose
astro.vpn.comStaging
vpn.comProduction
editor.vpn.comContent editor (Cloudflare Access protected)

Key Files

FilePurpose
src/data/providers/*.jsonProvider data
src/data/providers.tsProvider data API
src/content/posts/*.mdBlog posts
src/content/pages/*.mdPages
src/layouts/Provider.astroProvider review template
src/layouts/Editorial.astroBlog post template
src/components/SpeedLab.astroSpeed Lab table
src/components/ContentSection.astroContent + sidebar layout
src/config/languages.tsLanguage list
src/i18n/translations.tsUI string translations
src/i18n/routes.tsRoute map for localized URLs
src/data/providers/i18n/{lang}/Provider translation overlays
src/pages/[lang]/Dynamic translated hub routes
scripts/translate-provider-json.mjsTranslate provider overlays
scripts/translate-md.mjsBulk translate markdown
scripts/translation-qa.mjsChecks for English leaks
scripts/fix-affiliate-rels.mjsPost-build affiliate link fix
public/print.cssPrint stylesheet (standalone, not bundled)
public/llms.txtAI crawler index

DNS & Routing

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.