# Sito bg_perspective, specifica di progetto

Data: 2026-09-14. Concept approvati: home (`_instagram/sito-concept/D-tavolo-mobile.html`) e pagina gioco (`_instagram/sito-concept/E-post.html`).

## Scopo

Un sito che ripubblica ogni post del canale come pagina di testo puro, con le stesse foto, gli stessi testi e i dati veri del gruppo. Una scheda per gioco, uguale per tutti. Fa da link in bio al posto di Linktree, porta traffico da Google e dalle AI su ricerche tipo "quadropolis quanto dura", e riporta chi arriva verso Instagram e Threads.

Non è un blog e non contiene recensioni: la parola in tutto il sito è "scheda".

## Stack

- SvelteKit 2 (Svelte 5), adapter Vercel, rendering lato server a ogni richiesta (niente prerender: la pubblicazione a orario dipende dall'ora).
- Turso (libSQL) come database. Un solo database `bg-perspective`, gruppo `default`, stessa regione degli altri.
- Vercel: progetto `bg-perspective`, deploy da CLI (`vercel deploy --prod --yes --scope paoloalbys-projects`) come per `bgp-immagini`. Dominio `bg-perspective.vercel.app` finché non si compra quello vero; `canonical` e sitemap leggono la base da una variabile d'ambiente `PUBLIC_BASE_URL`, così cambiare dominio è un solo valore.
- Vercel Web Analytics per le visite. Niente Google Analytics, niente banner cookie. Nessun cookie impostato dal sito.
- Foto e scatole su `bgp-immagini` (già pubblico, già distribuito a ogni montaggio), sotto `sito/<slug>/`.
- Cartella del progetto: `_instagram/sito/`. Codice Python di supporto in `_instagram/codice/sito.py`.

## Chi scrive nel database

Solo la pipeline Python. Il sito è in sola lettura, senza login e senza area admin.

Il momento del push è la messa in coda su Buffer, perché è lì che esistono tutti i pezzi: testi delle slide, didascalia con la chicca, id dei due post Buffer, data e ora di uscita. Comando:

```
python3 codice/sito.py pubblica post13 --data 2026-10-02T19:00 --ig <bufferId> --th <bufferId>
```

Cosa fa, in ordine:

1. Legge `slide.POST[post13]` ed estrae titolo, crediti, testi (Cos'è, Come gira, domanda finale con sottotitolo), dati della slide scatola (edizione italiana, testo nel gioco, meccaniche, categorie), dati della slide durata se c'è.
2. Legge `portale.dati_gioco(bgg_id, usa_cache=False)` per giocatori, durata di scatola, peso, partite, `partiteStats` (mediane per numero di giocatori), autori.
3. Legge la didascalia dal post Buffer (`get_post`) e ne estrae la chicca: il paragrafo che non è né la prima riga (numero di partite) né il "Cos'è" né la CTA né gli hashtag. In pratica il terzo paragrafo; se il riconoscimento non è sicuro il comando si ferma e chiede quale paragrafo usare.
4. Ridimensiona le quattro foto originali di `foto/` a 1600 px sul lato lungo, JPG qualità 80, senza ritaglio, in `bgp-immagini/sito/<slug>/`. Copia lì anche la scatola piatta di BGG (`portale.scatola_bgg`, 600 px) e la scatola 3D di `scatole/pronte/` (700 px, PNG con trasparenza).
5. Fa il deploy di `bgp-immagini`.
6. Scrive la riga in `giochi` su Turso (upsert su `slug`), con `pubblicato_il` = data passata e i due id Buffer.

Rilanciare il comando sullo stesso post aggiorna la riga: è così che si correggono testi o foto.

Per i post già usciti (`--gia-uscito`) il comando chiede subito a Buffer `externalLink` e riempie i permalink.

Il comando ha un solo controllo automatico: `python3 codice/sito.py verifica <slug>` rilegge la riga da Turso e la stampa; e un `demo()` in fondo al file che prova l'estrazione della chicca su una didascalia nota.

## Schema del database

Una tabella sola. Autori ed editori non hanno tabelle: le loro pagine si costruiscono raggruppando `giochi`.

```sql
create table giochi (
  slug            text primary key,        -- "cryptid", "le-rovine-perdute-di-arnak"
  post            text not null,           -- "post4", chiave in slide.POST
  bgg_id          integer not null,
  titolo          text not null,
  autori          text not null,           -- json: [{"nome":"Hal Duncan","slug":"hal-duncan","handle":"..."}]
  anno            integer,
  editore_it      text not null,
  editore_slug    text not null,
  editore_handle  text,
  giocatori_min   integer, giocatori_max integer,
  durata_min      integer, durata_max integer,     -- sulla scatola, minuti
  peso            real, peso_etichetta text,       -- 2.25, "Medio"
  categorie       text,                            -- json ["Deduttivo"]
  meccaniche      text,                            -- json ["Deduzione","Griglia esagonale"]
  testo_nel_gioco text,                            -- "Italiano", "Nessuno"
  cos_e           text not null,
  come_gira       text not null,
  apertura        text,                            -- prima riga della didascalia
  chicca          text,
  domanda         text not null,                   -- "Qual è il deduttivo che non reggi?"
  domanda_sotto   text,
  partite         integer,                         -- partiteTavoli
  durate          text,                            -- json [{"giocatori":3,"partite":9,"mediana":11}, ...]
  foto            text not null,                   -- json ["cop.jpg","cose.jpg","gira.jpg","fine.jpg"] relativi a sito/<slug>/
  alt             text,                            -- json degli alt text, stesse posizioni
  scatola_piatta  text,                            -- "scatola-bgg.jpg"
  scatola_3d      text,                            -- "scatola.png"
  buffer_ig       text, buffer_th text,
  instagram_url   text, threads_url text,
  pubblicato_il   text not null,                   -- ISO 8601 con fuso, es. "2026-10-02T19:00:00+02:00"
  aggiornato_il   text not null
);
create index giochi_pubblicato on giochi(pubblicato_il);
```

Regola di visibilità, ovunque nel sito: `where pubblicato_il <= now`. Una riga con data futura è una bozza: non compare in home, nella ricerca, nella sitemap, nel feed, nelle pagine autore/editore, e l'URL diretto risponde 404.

## Pagine

Tutte in italiano. Layout e token di colore sono quelli dei due concept (legno, carta, ambra; Fraunces + Instrument Sans da Google Fonts).

### `/` Home (concept D)

- Testata fissa: logo, icone Instagram e Threads, campo di ricerca.
- Blocco "Ultimo uscito": il gioco con `pubblicato_il` più recente. Foto di copertina, data, titolo e il paragrafo di apertura della didascalia (campo `apertura`, la riga con il numero di partite).
- "Sul tavolo": tutte le schede visibili come scatole piatte in griglia (2 colonne su telefono, 3 su tablet, 5 su desktop), ogni cella 4:5 con scatola e titolo+editore centrati insieme. Galleggiamento e rotazione come nel concept. Ordinamento con la tendina: "Più recenti" (default, per `pubblicato_il` decrescente) e "Dalla A alla Z".
- Ricerca: filtra lato client su titolo, autori, editore. I dati per il filtro arrivano già nella pagina (poche decine di righe, nessuna chiamata in più).

### `/gioco/[slug]` Pagina gioco (concept E)

Nell'ordine del concept: data, titolo, autori · anno · in Italia editore (link alle pagine autore/editore), foto di copertina, i tre numeri (giocatori, sulla scatola, al nostro tavolo), tag categoria e meccaniche, peso e testo nel gioco, Cos'è + foto, Come gira + foto, La chicca, Al nostro tavolo con il grafico delle mediane e la fascia della scatola, La scatola (3D, edizione italiana, testo, link BGG), Diccelo tu con i due bottoni, ultima foto con credito, "Sul tavolo c'è anche" con due schede.

- "Al nostro tavolo" nei tre numeri mostra min–max delle mediane in `durate`; se `durate` è vuoto la casella mostra "—" e la sezione con il grafico non si stampa.
- "Diccelo tu": se `instagram_url` è nullo il bottone punta al profilo; idem Threads.
- Le due schede vicine: stessa categoria se ce ne sono, altrimenti le due più recenti diverse da questa.
- Gli apostrofi dritti nei testi diventano tipografici in fase di rendering (una funzione sola, applicata a tutti i campi di testo).

### `/autore/[slug]` e `/editore/[slug]`

Titolo, handle Instagram se c'è, e la griglia delle schede di quell'autore o editore nello stesso stile della home, senza tendina. Se la griglia è vuota, 404.

### Tecniche

- `/sitemap.xml`: home, ogni gioco visibile, ogni autore e editore con almeno una scheda visibile.
- `/feed.xml`: RSS delle ultime 20 schede.
- `/api/permalink`: cron Vercel ogni giorno alle 19:10 Europe/Rome (in `vercel.json`, orario in UTC calcolato per l'ora legale: si scrivono entrambe le voci, 17:10 e 18:10 UTC; il doppio giro è innocuo). Cerca le righe con `buffer_ig` o `buffer_th` valorizzati e `instagram_url` o `threads_url` nulli e `pubblicato_il <= now`, chiede a Buffer `post(input:{id}) { externalLink }` e scrive i permalink. Protetto da `CRON_SECRET` come da documentazione Vercel. La chiave Buffer sta in una variabile d'ambiente, mai nel codice.
- Meta: `<title>` "Cryptid · bg_perspective", description dal `cos_e`, Open Graph con la foto di copertina, `canonical`. JSON-LD di tipo `Game` con `name`, `author`, `publisher`, `datePublished`, `numberOfPlayers` (min/max), `timeRequired`, `image`, più `BreadcrumbList`. Sulla home `WebSite` con `SearchAction`.
- `robots.txt` che permette tutto e indica la sitemap.
- Icone: favicon e icone per la home dello smartphone partono dall'immagine del profilo Instagram (i dadi di Sagrada). Da quella si generano `favicon.ico` (32), `favicon.svg` se l'immagine si presta, altrimenti PNG 32 e 16, `apple-touch-icon.png` (180) e `icon-192.png`, `icon-512.png`. Un `manifest.webmanifest` con nome "bg_perspective", `theme_color` e `background_color` del legno, `display: standalone`, così "Aggiungi alla schermata Home" apre il sito come un'app. Niente service worker: non serve offline.
- Errori: 404 con la testata del sito e il link alla home; qualsiasi errore di database mostra la stessa pagina con un messaggio breve, e non stampa lo stack.

## Variabili d'ambiente

`TURSO_DATABASE_URL`, `TURSO_AUTH_TOKEN`, `BUFFER_API_KEY`, `CRON_SECRET`, `PUBLIC_BASE_URL`. Sul Mac in `_instagram/sito/.env` (fuori dal repo, `.gitignore`), su Vercel come variabili del progetto. Il comando Python legge lo stesso `.env`.

## Recupero dello storico

Dopo il primo deploy si lanciano `pubblica --gia-uscito` per i sette post usciti (Draftosaurus, Dixit, Ratti di Wistar, Cryptid, Wingspan Pocket, Between Two Cities, Ora et Labora quando esce) e `pubblica` con data per i dieci in coda su Buffer, leggendo id e date da `dati/bgp.sqlite` tabella `post`. I diciotto montati ma non ancora in coda entrano quando si mettono in coda, non prima.

## Fuori da questa specifica

Area admin, commenti sul sito, newsletter, versione inglese, dominio proprio, Google Analytics, ricerca full-text lato server, pagine per meccanica o categoria. Tutte cose possibili sopra questo schema, nessuna necessaria per partire.

## Criticità dichiarate

- Le scatole 3D e le copertine BGG sono materiale degli editori. Uso comune per identificare il prodotto; se un editore chiede, si toglie.
- Turso e Vercel sono due servizi gratuiti con limiti larghi per questo traffico; se uno dei due cambia condizioni, il sito è statico nei fatti e si può ricostruire come pagine fisse.
- Il cron riempie i permalink fino a 70 minuti dopo l'uscita nei giorni a cavallo del cambio d'ora; nel frattempo i bottoni puntano al profilo.
