CMS headless per-cliente, deployabile sul free tier di Cloudflare, per gestire landing page Astro.
landing-cms-cloudflare è un CMS per-cliente che gira interamente sul free
tier di Cloudflare. Ogni cliente ha il proprio account Cloudflare e, dopo una
provisioning iniziale, modifica in autonomia testi, link, colori, immagini e
impostazioni SEO delle proprie landing page Astro — senza passare dallo
sviluppatore. Il deploy del template Astro avviene via Deploy Hook di Cloudflare
Pages, triggerato dall'editor con un click.
Il progetto segue un modello per-cliente: 1 account Cloudflare per cliente, con il proprio Worker CMS, la propria D1, il proprio bucket R2 e i propri siti su Pages. Questo massimizza i limiti del free tier (ogni cliente ha i propri 100k req/giorno, 5 GB D1, 10 GB R2, 500 build/mese) e garantisce isolamento dati e costi. Il template è clonato-configure-ship per ogni nuovo cliente.
L'intero stack è serverless e statico: l'API CMS è un Worker Hono, i contenuti stanno in D1, i media in R2, l'editor è una Vue 3 SPA servita come static assets dal Worker, e le landing sono output statico Astro su Pages. Zero server da gestire, zero costi fissi per il cliente (free tier).
- 🔐 Auth Zero Trust — Cloudflare Access (email OTP / JWT
Cf-Access-Jwt-Assertion). Nessuna password custom, nessuna tabellausers. In dev localeDEV_MODE=1bypassa l'auth. - 🎨 Editor Vue 3 — SPA con form dinamici generati dal registry dei plugin. Build Vite → static assets serviti dal Worker stesso.
- 🔌 Plugin system — 6 blocchi built-in (
hero,features,gallery,text,cta,footer). Aggiungere un blocco richiede solo schema + form + renderer, zero migrazione DB (ilconfigè JSON in D1). - 🌐 Multi-sito — Un singolo Worker CMS gestisce N siti sullo stesso account cliente. Ogni sito ha il proprio slug, tema, contenuti e deploy su Pages.
- 🖼️ R2 media — Upload immagini verso bucket R2 pubblico via custom domain (
media.cliente.it). URL serviti dal Worker. - ⏱️ Deploy preview / publish con debounce — Il cliente clicca Preview (build ambiente
preview→*.pages.dev) o Publish (buildproduction→ custom domain). Debounce 60s evita build concorrenti. - ↩️ Rollback — Le versioni di contenuto sono versionate in D1 (
content_versions); un publish può ripristinare una versione precedente. - 🎭 Theming shadcn dinamico — Il cliente edita 4 colori HEX + font; a build-time Astro li converte nei ~21 token CSS di shadcn (HSL + contrasto WCAG automatico).
- 🔑 Token statico rotabile —
CMS_READ_TOKENprotegge l'endpoint pubblico di lettura usato in build da Astro.
┌─────────────┐ /api/* (Zero Trust JWT) ┌──────────────────┐
│ Editor Vue │ ──────────────────────────────► │ Worker Hono (TS) │
│ (SPA) │ ◄──────────────────────────────│ Static Assets │
└─────────────┘ └────────┬─────────┘
served as static assets by the Worker │
├── D1 (contenuti, siti, settings, versioni)
└── R2 (media pubblici via custom domain)
┌─────────────────────────────────────────────┐ ▲
│ Astro Template (static) │ │ GET /api/sites/:slug/content?token=CMS_READ_TOKEN
│ build-time fetch → static output → Pages │ ──────────┘
│ theme: 4 HEX + font → shadcn CSS vars │
└─────────────────────────────────────────────┘
│
▼
Cloudflare Pages (preview / production)
Deploy Hook triggerato dal Worker
Modello per-cliente: 1 account Cloudflare per cliente → provisioning di un
Worker CMS + D1 cms + bucket R2 + custom domains (cms.cliente.it,
media.cliente.it). Lo stesso Worker gestisce poi N siti su Pages, ciascuno
con il proprio custom domain di produzione.
| Flusso | Descrizione |
|---|---|
| Edit (runtime) | Editor Vue ↔ Worker Hono ↔ D1/R2, protetto da Zero Trust. |
| Build (one-shot) | Astro a build-time fetcha GET /api/sites/:slug/content con CMS_READ_TOKEN → output statico. |
| Deploy (trigger) | Worker chiama il Deploy Hook di Pages → preview o production. |
| Componente | Tecnologia |
|---|---|
| API CMS | Worker Hono (TypeScript) su Cloudflare Workers |
| Storage contenuti | D1 (SQLite, multi-sito) |
| Storage media | R2 (bucket pubblico via custom domain) |
| Editor | Vue 3 SPA (Vite build → static assets serviti dal Worker) |
| Landing | Astro static output, deployato su Cloudflare Pages |
| Styling | Tailwind CSS + shadcn/ui (theming dinamico da 4 HEX + font) |
| Auth | Cloudflare Zero Trust Access (email OTP, JWT Cf-Access-Jwt-Assertion) |
| Deploy / provisioning | wrangler CLI + script bash (provision-client.sh, new-site.sh) |
cms-cloudflare-template/
├─ worker/ # Worker CMS (API Hono + static assets editor)
│ ├─ src/
│ │ ├─ plugins/ # Registry + schema dei blocchi (hero, features, ...)
│ │ ├─ auth.ts # Middleware Zero Trust JWT
│ │ └─ index.ts
│ ├─ public/ # Output build editor (npm run build:editor)
│ ├─ wrangler.toml
│ └─ package.json
├─ editor/ # Vue 3 SPA sorgente (Vite)
│ ├─ src/
│ │ └─ plugins/ # Editor.vue per ogni blocco
│ └─ package.json
├─ astro-template/ # Template landing (deployato per sito su Pages)
│ ├─ src/
│ │ ├─ components/ # Renderer blocchi + ui/ shadcn
│ │ ├─ lib/ # cms.ts, theme.ts, utils.ts
│ │ └─ pages/index.astro
│ ├─ tailwind.config.mjs
│ └─ package.json
├─ scripts/
│ ├─ provision-client.sh # Crea D1 + R2 + Worker + secrets (account cliente)
│ ├─ new-site.sh # Crea Pages project + deploy hook + righe D1
│ ├─ schema.sql # Schema D1 canonico
│ ├─ seed-local.js # Seed D1 locale per dev mock (sito "test")
│ └─ sync-dev-vars.js # Rigenera worker/.dev.vars dal .env root (hook predev)
├─ schema.sql # copia canonica dello schema D1
├─ .env.example # UNICO .env per tutto il monorepo
├─ package.json # npm workspaces (worker, editor, astro-template)
├─ CONTRIBUTING.md
├─ CHANGELOG.md
├─ SECURITY.md
├─ CODE_OF_CONDUCT.md
└─ README.md
- Node 20+
- wrangler CLI:
npm i -g wrangler(onpx wrangler ...) - Account Cloudflare (free) — solo per setup produzione
curl,jq,openssl— per gli script di provisioning
Flusso completo in dev senza account Cloudflare reale: il Worker usa D1
locale (wrangler d1 ... --local), l'auth è bypassata (DEV_MODE=1), i media
usano URL remoti (MEDIA_BASE_URL vuoto). Lo script seed-local.js popola un
sito "test" con 6 sezioni di esempio.
# 1. Crea l'unico .env del monorepo (dalla root del repo)
cp .env.example .env
# Edit .env: lascia vuoti ACCOUNT_ID/CF_API_TOKEN, imposta:
# CMS_READ_TOKEN=<qualsiasi valore, es. dev-secret>
# MEDIA_BASE_URL= (vuoto → URL assoluti nei contenuti)
# DEV_MODE=1 (bypass auth Zero Trust in dev)
# CMS_API_URL=http://localhost:8787
# SITE_SLUG=test (matcha il sito del seed)
# 2. Worker (primo terminale, dalla root)
cd worker
npm install
npx wrangler d1 create cms # prima volta: copia database_id in wrangler.toml
npm run db:init # wrangler d1 execute cms --local --file=./schema.sql
cd .. && npm run seed # node scripts/seed-local.js → sito "test" con 6 sezioni
cd worker && npm run dev # predev rigenera .dev.vars dal .env, poi wrangler dev
# Worker su http://localhost:8787
# 3. Editor (secondo terminale, dalla root)
cd editor
npm install
npm run dev
# Editor su http://localhost:5173 (proxy /api → :8787, DEV_MODE bypassa l'auth)
# 4. Astro template (terzo terminale, opzionale)
cd astro-template
npm install
npm run dev
# Astro su http://localhost:4321 (legge .env root, renderizza il sito SITE_SLUG=test)Note dev:
- L'hook
predevdel Worker eseguescripts/sync-dev-vars.jsche rigeneraworker/.dev.varsdal.envroot ad ogninpm run dev. Non creare.dev.varsmanualmente (non è committato, vedi.gitignore). DEV_MODE=1bypassa l'autenticazione Zero Trust — mai usarlo in produzione.CMS_READ_TOKENdeve coincidere tra.env(Astro) e secret Worker in dev/prod.
Crea l'infrastruttura base (D1, R2, Worker, secrets) su un account cliente.
export ACCOUNT_ID=...
export CF_API_TOKEN=... # permessi: Workers/D1/R2/DNS/Pages/Zero Trust
export CLIENTE_NAME="ACME"
export MEDIA_DOMAIN="media.acme.it"
export CMS_DOMAIN="cms.acme.it"
./scripts/provision-client.shLo script stampa: URL Worker, CMS_READ_TOKEN (mascherato), istruzioni Zero Trust.
Segui le istruzioni stampate da provision-client.sh:
- https://one.dash.cloudflare.com/ → Access → Applications → Add an application.
- Tipo Self-hosted, dominio
cms.acme.it. - Identity provider One-time PIN (email OTP).
- Aggiungi le email dei clienti/collaboratori autorizzati.
Il Worker verifica il JWT Cf-Access-Jwt-Assertion su ogni route /api/*.
Assicurati che il Worker abbia un custom domain cms.acme.it
(Workers & Pages → <worker> → Settings → Domains & Routes).
export CMS_API_URL="https://cms.acme.it"
export CMS_READ_TOKEN="<token generato in provisioning>"
./scripts/new-site.sh acme-home "ACME Home" acme.comLo script: crea il Pages project, i Deploy Hook preview/production, imposta le
env vars di Pages, aggiunge il custom domain, inserisce le righe D1 (sites,
site_settings con tema di default, site_deployments) ed esegue la prima
build Astro + deploy.
Accedi a https://cms.acme.it (Zero Trust chiederà email OTP), seleziona il
sito, modifica sezioni/tema/media, clicca Preview (bozza su *.pages.dev)
e infine Publish (ricostruisce l'ambiente production e pubblica sul custom
domain).
Il sistema di theming è progettato per il cliente finale: edita solo 4 colori
HEX + font nell'editor (più logo_url, favicon_url). A build-time Astro
converte questi valori nei ~21 token CSS di shadcn e li inietta in :root come
CSS variables globali (src/lib/theme.ts → buildShadcnVars). I componenti
usano classi tailwind/shadcn standard (bg-primary, text-foreground,
<Button>, <Card>) e i colori diventano dinamici automaticamente — nessun
valore HEX hardcodato nei componenti.
Input dal CMS (Theme):
primary_color, secondary_color, text_color, bg_color, font_family.
| CSS var | derivazione |
|---|---|
--background / --card / --popover |
hexToHsl(bg_color) |
--foreground / --card-foreground / --popover-foreground |
hexToHsl(text_color) |
--primary / --ring |
hexToHsl(primary_color) |
--primary-foreground |
foregroundFor(primary_color) (bianco/nero per contrasto WCAG) |
--secondary / --muted / --accent |
hexToHsl(secondary_color) |
--secondary-foreground |
foregroundFor(secondary_color) |
--accent-foreground |
hexToHsl(text_color) |
--muted-foreground |
withLightness(text_color, 45) (grigio sul tono del testo) |
--border / --input |
withLightness(text_color, 90) (bordo tenue) |
--destructive / --destructive-foreground |
fissi (0 84.2% 60.2% / 0 0% 98%) |
--radius |
0.5rem (fisso) |
--font-sans |
font_family (fallback system) |
Algoritmi in astro-template/src/lib/theme.ts: hexToHsl (RGB→HSL
spacchettato), relativeLuminance (WCAG sRGB linearizzato), foregroundFor
(soglia luminanza 0.4 → bianco/nero leggibile), withLightness (mantiene h/s,
imposta l). Se il CMS non fornisce tema o valori vuoti, si applicano i fallback
in astro-template/src/styles/global.css.
- Crea
astro-template/src/components/ui/<nome>.astrousandocn()da@/lib/utilse classi tailwind con i colori token (bg-primary,text-muted-foreground, ...). - Per le varianti usa
class-variance-authority(CVA) come inbutton.astro. - I colori sono automaticamente dinamici: nessun valore hex hardcodato.
Built-in: button.astro (variant: default, secondary, destructive, outline,
ghost, link), card.astro, badge.astro.
I blocchi sono modulari: ogni plugin definisce uno schema (tipi/required
dei campi) nel Worker, un form nell'editor Vue e un renderer Astro.
GET /api/plugins ritorna la lista degli schemi per generare dinamicamente i
form e la validazione lato editor.
| Plugin | Campi principali |
|---|---|
hero |
title, subtitle, image_url, cta_text, cta_url |
features |
title, items[] (title, description, image_url) |
gallery |
title, images[] (url, alt) |
text |
title, body (HTML), alignment (left/center/right) |
cta |
title, description, button_text, button_url, background_color |
footer |
copyright_text, links[], social_links[] |
worker/src/plugins/testimonials/schema.ts→ schema campi/tipi.editor/src/plugins/testimonials/Editor.vue→ form Vue.astro-template/src/components/Testimonials.astro→ renderer.worker/src/plugins/registry.ts→ aggiungi entry{ type: "testimonials", name: "Testimonianze", schema: {...} }.- (Astro) registra in
sectionMapinsrc/pages/index.astro. - Redeploy Worker + Pages. Zero migrazione DB: il
configè JSON in D1.
- Auth: via Cloudflare Zero Trust (email OTP). Nessuna password custom,
nessuna tabella
users. Il Worker verifica il JWTCf-Access-Jwt-Assertionsu ogni route/api/*(middlewareworker/src/auth.ts). - Endpoint pubblico protetto:
GET /api/sites/:slug/contentè pubblico (serve al build Astro) ma protetto da token staticoCMS_READ_TOKEN(rotabile). - DEV_MODE=1 bypassa l'auth — mai in produzione. In dev il segreto è
generato automaticamente in
worker/.dev.varsdall'hookpredev. - Secrets: in produzione impostati con
wrangler secret put, mai inwrangler.tomlo committati. I Deploy Hook URL di Pages sono sensibili: memorizzati solo in D1, mai in repo pubblici. - Vedi
SECURITY.mdper policy di divulgazione vulnerabilità.
# 1. Genera nuovo token
openssl rand -hex 32
# 2. Aggiorna il secret del Worker
cd worker && echo "<nuovo>" | wrangler secret put CMS_READ_TOKEN
# 3. Aggiorna le Pages env vars (preview + production) per ogni sito
# via CF API PATCH deployment_configs, oppure dashboard Pages → Settings → Env vars
# 4. Ridistribuisci il Worker e ri-triggera un publish dei siti (o attendi il prossimo build)Documenta il nuovo token in un password manager.
Il modello per-cliente massimizza il free tier: ogni cliente ha i propri limiti.
| Risorsa | Limite free | Impatto su questo progetto | Mitigazione |
|---|---|---|---|
| Workers requests | 100k/giorno | Worker non serve traffico pubblico (solo editor + API build) | OK |
| D1 reads | 5M/giorno | Uso CMS moderato | OK |
| D1 storage | 5 GB | Multi-sito, contenuti piccoli (JSON) | OK |
| R2 storage | 10 GB | Immagini media | Monitorare; comprimere upload |
| R2 operations Class A | 1M/mese | Upload media (write) | OK |
| R2 operations Class B | 10M/mese | GET immagini pubbliche | OK |
| Pages builds | 500/mese | Preview + publish | Debounce 60s + publish esplicito. Cliente attivo ~30–60/mese |
| Pages build concorrenti | 1/account | Deploy sequenziali multi-sito | Accettabile; avviso in UI se in coda |
| Zero Trust users | 50 | Cliente + collaboratori | OK |
cd worker
npx wrangler deploy
# imposta i secrets (prima volta o rotazione)
echo "<token>" | wrangler secret put CMS_READ_TOKEN
echo "<token>" | wrangler secret put CF_API_TOKEN# Dalla root del repo
npm run build:editor
# Output in worker/public/ → servito dal Worker come Static Assets
# Poi: wrangler deploy (worker)cd astro-template
npm run build
npx wrangler pages deploy ./dist --project-name <slug>Configura le env/secret nel Pages project (ambienti production e preview):
npx wrangler pages secret put CMS_API_URL --project-name <slug>
npx wrangler pages secret put CMS_READ_TOKEN --project-name <slug>
npx wrangler pages secret put SITE_SLUG --project-name <slug>In alternativa, il Worker CMS triggera il rebuild via Deploy Hook.
| Script | Scopo |
|---|---|
scripts/provision-client.sh |
Crea D1 + bucket R2 + Worker + secrets su un account cliente (una tantum per cliente) |
scripts/new-site.sh <slug> <name> <domain> |
Crea Pages project + Deploy Hook preview/production + env vars + righe D1 per un nuovo sito |
scripts/seed-local.js |
Popola D1 locale con un sito "test" (6 sezioni) per dev mock |
scripts/sync-dev-vars.js |
Rigenera worker/.dev.vars dal .env root (hook predev automatico) |
Lo stato attuale è manuale (vedi CONTRIBUTING.md).
Verifiche di base consigliate prima di una release:
# Typecheck (per workspace)
npm run typecheck --workspace <pkg> # dove disponibile
# Build editor + astro (sanity check)
npm run build:editor
npm run build:astro
# Seed D1 locale e smoke test del flusso edit→build→deploy
npm run seed
cd worker && npm run dev # poi editor + astro nei terminali separatiCONTRIBUTING.md— come contribuire, convenzioni, flusso devCHANGELOG.md— storico versioniSECURITY.md— policy di sicurezza e divulgazioneCODE_OF_CONDUCT.md— codice di condottaastro-template/README.md— dettagli template Astro (theming, componenti, deploy)
I contributi sono benvenuti: apri una issue per discutere la feature/bug, poi una
PR verso main. Segui le convenzioni in CONTRIBUTING.md
(commit message, branching, typecheck prima della PR). Per cambiamenti al
modello dati (schema D1) o ai plugin, aggiorna schema.sql e i relativi
renderer Astro nella stessa PR.
Distribuito sotto licenza GPL-3.0. Vedi LICENSE per il
testo completo.
Copyright © loSpaccaBit 2026.
- Cloudflare — Workers, D1, R2, Pages, Zero Trust
- Hono — web framework per edge runtime
- Vue.js — editor SPA
- Astro — static site generation
- shadcn/ui — componenti UI themabili
- Tailwind CSS — utility-first styling