docs: spec sistema contenuti taggati e pannello editor
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,146 @@
|
|||||||
|
# Sistema di contenuti taggati e pannello editor
|
||||||
|
|
||||||
|
Data: 2026-07-05
|
||||||
|
Stato: approvato dall'operatore (design), spec in revisione
|
||||||
|
|
||||||
|
## Obiettivo
|
||||||
|
|
||||||
|
Ogni testo e ogni immagine delle pagine vetrina del sito riceve un tag univoco e stabile. I contenuti vengono estratti dal codice e salvati in SQLite, così da poter essere modificati senza intervenire sui sorgenti. Un pannello di amministrazione (`/admin/content`) permette a un utente con ruolo dedicato di modificare testi, stili (tramite preset controllati) e immagini; una modalità "annotate" sovrappone al sito i badge con i nomi dei tag, rendendo immediato individuare l'elemento da modificare.
|
||||||
|
|
||||||
|
Il sistema serve due scopi complementari: nel breve periodo dà all'operatore un vocabolario preciso per dettare modifiche ("cambia `home.hero.title` in…"); nel medio periodo consente al cliente di aggiornare i contenuti in autonomia, senza rischio di rompere il layout.
|
||||||
|
|
||||||
|
## Decisioni prese in fase di design
|
||||||
|
|
||||||
|
| Decisione | Scelta |
|
||||||
|
|---|---|
|
||||||
|
| Scopo | Tagging + pannello editor nello stesso progetto |
|
||||||
|
| Ambito modifiche | Testi, immagini, stili tramite preset chiusi (no CSS libero) |
|
||||||
|
| Utenti | Ruolo `admin` (tutto) e ruolo `editor` (solo contenuti) |
|
||||||
|
| Pubblicazione | Pagine vetrina passano a SSR con cache in memoria invalidata al salvataggio |
|
||||||
|
| UX | Ibrido: pannello `/admin/content` + overlay annotate sul sito con link ai form |
|
||||||
|
|
||||||
|
## Architettura
|
||||||
|
|
||||||
|
### 1. Schema dei tag
|
||||||
|
|
||||||
|
Formato gerarchico `pagina.sezione.elemento`, tutto minuscolo, separatore punto, senza spazi:
|
||||||
|
|
||||||
|
- `home.hero.title`, `home.hero.subtitle`, `home.hero.cta`
|
||||||
|
- `about.team.donata.bio`, `about.intro.body`
|
||||||
|
- `programs.pilates-flow.price`, `training.fit-remote.description`
|
||||||
|
- `global.footer.tagline`, `global.header.cta` (elementi condivisi tra pagine)
|
||||||
|
|
||||||
|
I tag sono stabili: una volta assegnati non cambiano nome, perché sono chiave primaria nel DB e riferimento nelle conversazioni. I dati oggi strutturati in `src/data/*.ts` (team, pricing, agenda, method, programs, trainings, partners, site) vengono scomposti in tag granulari con lo stesso schema.
|
||||||
|
|
||||||
|
### 2. Storage (SQLite, DB esistente)
|
||||||
|
|
||||||
|
Nuova tabella nella `SCHEMA` di `src/lib/db.ts`:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE IF NOT EXISTS content_blocks (
|
||||||
|
tag TEXT PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL CHECK (type IN ('text','html','image')),
|
||||||
|
value TEXT NOT NULL DEFAULT '',
|
||||||
|
styles TEXT NOT NULL DEFAULT '{}', -- JSON, solo chiavi preset
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_by TEXT
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
- `type = 'text'`: testo semplice, escapato in render.
|
||||||
|
- `type = 'html'`: frammenti con markup minimo (grassetti, a-capo) sanificati con la pipeline già usata dal blog (`src/lib/sanitize.ts`). Usato solo dove il testo attuale contiene markup.
|
||||||
|
- `type = 'image'`: `value` contiene il percorso dell'override caricato (es. `content/home-hero-1.jpg`, relativo a `uploads/`) oppure stringa vuota se vale l'asset originale.
|
||||||
|
|
||||||
|
### 3. Seed e fonte di verità
|
||||||
|
|
||||||
|
`src/data/content-seed.ts` contiene l'inventario completo dei tag con i valori estratti dai testi attuali: `{ tag, type, value, styleOptions }`. Il campo `styleOptions` dichiara quali preset ha senso esporre per quel tag (vedi §6); non finisce nel DB, guida solo il pannello.
|
||||||
|
|
||||||
|
All'avvio (in `createDb`) un passo di sync inserisce nel DB i tag presenti nel seed ma assenti nella tabella (`INSERT OR IGNORE`). Regole:
|
||||||
|
|
||||||
|
- In produzione il DB è la fonte di verità: le modifiche del cliente non vengono mai sovrascritte dal seed.
|
||||||
|
- I tag nuovi arrivano con il codice (aggiunta al seed) e compaiono nel DB al deploy successivo.
|
||||||
|
- I tag rimossi dal seed restano orfani nel DB e vengono ignorati; nessuna cancellazione automatica.
|
||||||
|
- Il seed nel repo funge anche da backup versionato dei testi originali.
|
||||||
|
|
||||||
|
### 4. Rendering
|
||||||
|
|
||||||
|
- Le pagine vetrina (`index`, `about`, `contact`, `training`, `programs/index`, `programs/[slug]`, `404`) passano da `prerender = true` a `prerender = false` (SSR, già attivo per blog e admin con adapter node standalone).
|
||||||
|
- Nuovo modulo `src/lib/content.ts`:
|
||||||
|
- `getContent(tag)` / `getAllContent()`: lettura con cache in memoria (`Map` popolata al primo accesso con una sola query).
|
||||||
|
- `invalidateContentCache()`: svuota la cache; chiamata dall'API di salvataggio. Modifica visibile alla richiesta successiva.
|
||||||
|
- Fallback a cascata: valore nel DB → valore nel seed → stringa vuota con log di warning. Il sito non si rompe mai per un tag mancante.
|
||||||
|
- Nuovi componenti Astro:
|
||||||
|
- `<T tag="home.hero.title" as="h1" class="…" />`: renderizza il contenuto nel tag HTML indicato (`as`, default `span`), applica le classi preset derivate da `styles`, emette `data-tag` per l'overlay annotate. Per `type='text'` il valore è escapato; per `type='html'` è già sanificato al salvataggio.
|
||||||
|
- `<TImg tag="home.hero.image" … />`: se esiste override in DB serve il file da `/uploads/content/…` (route `src/pages/uploads/[...path].ts` già esistente), altrimenti l'asset originale ottimizzato dalla pipeline Astro. Le prop di layout (dimensioni, alt di default, loading) restano nel codice.
|
||||||
|
- La sitemap non cambia (le route restano le stesse); verificare che l'integrazione sitemap continui a emetterle con `output: 'server'` e prerender disattivato, altrimenti passare a una sitemap statica generata a mano.
|
||||||
|
|
||||||
|
### 5. Ruoli utente
|
||||||
|
|
||||||
|
- Migrazione additiva: `ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'admin'` (eseguita in `createDb` se la colonna manca; gli utenti esistenti restano admin).
|
||||||
|
- `getSessionUser` ritorna anche `role`; `context.locals.user` lo espone.
|
||||||
|
- Middleware: le route `/admin/content` e `/api/admin/content*` sono accessibili ad `admin` ed `editor`; tutte le altre route `/admin*` e `/api/admin*` restano riservate ad `admin` (l'editor che le richiede riceve redirect a `/admin/content` o 403 sulle API).
|
||||||
|
- Creazione utente editor: script CLI in `scripts/` (pattern già usato per l'admin), credenziali consegnate al cliente fuori banda.
|
||||||
|
|
||||||
|
### 6. Stili preset
|
||||||
|
|
||||||
|
Il DB memorizza solo chiavi simboliche; la mappa chiave → classe CSS vive nel codice (`src/lib/style-presets.ts` + classi in `global.css`). Nessun CSS arbitrario può entrare dal pannello.
|
||||||
|
|
||||||
|
| Gruppo | Chiavi | Note |
|
||||||
|
|---|---|---|
|
||||||
|
| `size` | `s` / `m` / `l` | scala relativa alla dimensione base dell'elemento |
|
||||||
|
| `color` | palette del sito (brand-dark, tan, taupe, bianco, …) | lista chiusa derivata dalle variabili CSS esistenti |
|
||||||
|
| `weight` | `normal` / `bold` | |
|
||||||
|
| `style` | `normal` / `italic` | |
|
||||||
|
| `align` | `left` / `center` / `right` | solo su elementi a blocco |
|
||||||
|
|
||||||
|
Ogni tag dichiara nel seed quali gruppi espone (`styleOptions`): un titolo hero espone size e color, una voce di prezzo magari solo color. Valori non in whitelist vengono rifiutati dall'API al salvataggio.
|
||||||
|
|
||||||
|
### 7. Pannello `/admin/content`
|
||||||
|
|
||||||
|
Pagina SSR protetta, stessa shell di `src/layouts/Admin.astro`:
|
||||||
|
|
||||||
|
- Tag raggruppati per pagina (Home, About, Programs, Training, Contact, Globali), gruppi collassabili, campo di ricerca per nome tag o contenuto.
|
||||||
|
- Per ogni tag: form con campo testo (input o textarea in base alla lunghezza), controlli preset solo per i gruppi dichiarati, upload immagine per i tag `image` (validazione mime/dimensione riusando la logica dell'upload blog) con anteprima e pulsante "ripristina originale".
|
||||||
|
- Salvataggio per singolo tag (pulsante per riga) via `fetch` alle API; feedback inline di esito.
|
||||||
|
- Ancore `id` per tag: `/admin/content#home.hero.title` scrolla ed evidenzia il form corrispondente.
|
||||||
|
- Pulsante "Vedi sul sito con tag" che apre la pagina pubblica in modalità annotate.
|
||||||
|
|
||||||
|
API:
|
||||||
|
|
||||||
|
- `GET /api/admin/content` — elenco completo (per il pannello).
|
||||||
|
- `PUT /api/admin/content/[tag]` — aggiorna `value` e/o `styles`; valida tipo, preset in whitelist, sanifica se `type='html'`; aggiorna `updated_at`/`updated_by`; invalida la cache.
|
||||||
|
- `POST /api/admin/content/[tag]/image` — upload override immagine in `uploads/content/`; `DELETE` sullo stesso path rimuove l'override e ripristina l'asset originale.
|
||||||
|
|
||||||
|
### 8. Overlay annotate
|
||||||
|
|
||||||
|
- Attivazione: query string `?annotate=1` su qualunque pagina vetrina, solo con sessione valida (admin o editor). Senza sessione il parametro è ignorato: nessuna traccia dei tag per i visitatori.
|
||||||
|
- `Base.astro` in modalità annotate inietta un piccolo CSS+JS vanilla (coerente con lo stile del progetto: niente librerie): ogni elemento con `data-tag` riceve un bordo tratteggiato e un badge con il nome del tag; il click sul badge apre `/admin/content#<tag>` in nuova scheda.
|
||||||
|
- Le animazioni reveal restano attive; il badge è posizionato in modo da non alterare il layout (position absolute su wrapper relativo o outline, da verificare in implementazione).
|
||||||
|
|
||||||
|
### 9. Gestione errori
|
||||||
|
|
||||||
|
- Tag mancante nel DB e nel seed: stringa vuota + `console.warn` lato server. La pagina renderizza comunque.
|
||||||
|
- `styles` JSON corrotto nel DB: si ignora e si usa `{}`.
|
||||||
|
- Upload immagine: stessi limiti e messaggi del blog; file orfani in `uploads/content/` tollerati (nessuna garbage collection automatica).
|
||||||
|
- API: 401 senza sessione, 403 per ruolo insufficiente, 400 con messaggio esplicito per tag inesistente, tipo errato o preset fuori whitelist.
|
||||||
|
|
||||||
|
### 10. Test
|
||||||
|
|
||||||
|
- Unit (`vitest`, pattern esistente in `tests/`): content lib (fallback a cascata, invalidazione cache, sync del seed), validazione preset, migrazione colonna `role`.
|
||||||
|
- API: autorizzazione per ruolo (editor può contenuti, non può post; anonimo 401), salvataggio e rilettura, rifiuto preset non in whitelist.
|
||||||
|
- Smoke: le pagine vetrina renderizzano in SSR con contenuti dal DB; overlay annotate presente con sessione e assente senza.
|
||||||
|
|
||||||
|
## Fasi di lavoro previste (indicative per il piano)
|
||||||
|
|
||||||
|
1. Infrastruttura: tabella, migrazione ruoli, content lib con cache, componenti `<T>`/`<TImg>`, preset.
|
||||||
|
2. Estrazione e tagging: inventario completo testi/immagini pagina per pagina, creazione seed, sostituzione nei template e nei componenti (lavoro meccanico ma esteso; è la fase più lunga).
|
||||||
|
3. Pannello + API + overlay annotate + ruolo editor.
|
||||||
|
4. Test, verifica end-to-end locale, deploy e verifica in produzione.
|
||||||
|
|
||||||
|
## Fuori ambito
|
||||||
|
|
||||||
|
- CSS libero per elemento (escluso per scelta).
|
||||||
|
- Versionamento/storico delle modifiche ai contenuti (eventuale evoluzione futura).
|
||||||
|
- Modifica della struttura delle pagine (aggiunta/rimozione sezioni) dal pannello.
|
||||||
|
- Gestione multilingua.
|
||||||
|
- Blog: resta gestito dal suo admin TipTap esistente, fuori dal sistema tag.
|
||||||
Reference in New Issue
Block a user