Skip to content

loSpaccaBit/landing-cms-cloudflare

Repository files navigation

landing-cms-cloudflare

CMS headless per-cliente, deployabile sul free tier di Cloudflare, per gestire landing page Astro.

License Node Cloudflare Astro Vue TypeScript

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

✨ Features

  • 🔐 Auth Zero Trust — Cloudflare Access (email OTP / JWT Cf-Access-Jwt-Assertion). Nessuna password custom, nessuna tabella users. In dev locale DEV_MODE=1 bypassa 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 (il config è 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 (build production → 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 rotabileCMS_READ_TOKEN protegge l'endpoint pubblico di lettura usato in build da Astro.

🏛️ Architecture

┌─────────────┐     /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.

📦 Stack

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)

📁 Struttura repo

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

🚀 Quick start

Prerequisiti

  • Node 20+
  • wrangler CLI: npm i -g wrangler (o npx wrangler ...)
  • Account Cloudflare (free) — solo per setup produzione
  • curl, jq, openssl — per gli script di provisioning

Test locale (mock, niente Cloudflare)

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 predev del Worker esegue scripts/sync-dev-vars.js che rigenera worker/.dev.vars dal .env root ad ogni npm run dev. Non creare .dev.vars manualmente (non è committato, vedi .gitignore).
  • DEV_MODE=1 bypassa l'autenticazione Zero Trust — mai usarlo in produzione.
  • CMS_READ_TOKEN deve coincidere tra .env (Astro) e secret Worker in dev/prod.

Setup produzione (account Cloudflare reale)

1. Provisioning nuovo account cliente

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

Lo script stampa: URL Worker, CMS_READ_TOKEN (mascherato), istruzioni Zero Trust.

2. Configura Zero Trust Access (manuale)

Segui le istruzioni stampate da provision-client.sh:

  1. https://one.dash.cloudflare.com/ → Access → Applications → Add an application.
  2. Tipo Self-hosted, dominio cms.acme.it.
  3. Identity provider One-time PIN (email OTP).
  4. 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).

3. Crea il primo sito

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

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

4. Usa il CMS

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

🎨 Theming (Tailwind + shadcn/ui)

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.tsbuildShadcnVars). 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.

Matrice di derivazione (HEX → token shadcn)

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.

Aggiungere componenti shadcn

  1. Crea astro-template/src/components/ui/<nome>.astro usando cn() da @/lib/utils e classi tailwind con i colori token (bg-primary, text-muted-foreground, ...).
  2. Per le varianti usa class-variance-authority (CVA) come in button.astro.
  3. I colori sono automaticamente dinamici: nessun valore hex hardcodato.

Built-in: button.astro (variant: default, secondary, destructive, outline, ghost, link), card.astro, badge.astro.

🔌 Plugin system

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.

6 plugin built-in

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[]

Aggiungere un nuovo blocco (es. testimonials)

  1. worker/src/plugins/testimonials/schema.ts → schema campi/tipi.
  2. editor/src/plugins/testimonials/Editor.vue → form Vue.
  3. astro-template/src/components/Testimonials.astro → renderer.
  4. worker/src/plugins/registry.ts → aggiungi entry { type: "testimonials", name: "Testimonianze", schema: {...} }.
  5. (Astro) registra in sectionMap in src/pages/index.astro.
  6. Redeploy Worker + Pages. Zero migrazione DB: il config è JSON in D1.

🔐 Security

  • Auth: via Cloudflare Zero Trust (email OTP). Nessuna password custom, nessuna tabella users. Il Worker verifica il JWT Cf-Access-Jwt-Assertion su ogni route /api/* (middleware worker/src/auth.ts).
  • Endpoint pubblico protetto: GET /api/sites/:slug/content è pubblico (serve al build Astro) ma protetto da token statico CMS_READ_TOKEN (rotabile).
  • DEV_MODE=1 bypassa l'auth — mai in produzione. In dev il segreto è generato automaticamente in worker/.dev.vars dall'hook predev.
  • Secrets: in produzione impostati con wrangler secret put, mai in wrangler.toml o committati. I Deploy Hook URL di Pages sono sensibili: memorizzati solo in D1, mai in repo pubblici.
  • Vedi SECURITY.md per policy di divulgazione vulnerabilità.

Rotazione CMS_READ_TOKEN

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

📊 Limiti free tier Cloudflare

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

📦 Deployment

Worker CMS

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

Editor Vue (static assets del Worker)

# Dalla root del repo
npm run build:editor
# Output in worker/public/ → servito dal Worker come Static Assets
# Poi: wrangler deploy (worker)

Astro template (Cloudflare Pages)

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 di provisioning

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)

🧪 Testing

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 separati

📖 Documentation

🤝 Contributing

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.

📄 License

Distribuito sotto licenza GPL-3.0. Vedi LICENSE per il testo completo.

Copyright © loSpaccaBit 2026.

🙏 Acknowledgments

About

CMS deployabile su Cloudflare free tier per landing page Astro. Per-cliente: clienti editano testi/colori/media in autonomia. Worker Hono + D1 + R2, Editor Vue 3, Astro statico con Tailwind/shadcn.

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages