# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Interazione - L'utente scrive in italiano. **Rispondi in italiano.** - Codice, commenti, nomi di variabili, commit message, log e documentazione tecnica: **in inglese.** - Nei commit **non aggiungere `Co-Authored-By: Claude`** né altri trailer di attribuzione. ## Stato Struttura, content type, pagine blog, Docker, Caddy e i test (unit/integration/e2e) sono in piedi. Restano da fare: sitemap e `robots.txt` dinamici, ricerca. ## Architettura ```text Browser → Caddy ─┬─ /_/* e /api/* → PocketBase (admin UI + API) └─ tutto il resto → Nuxt 4 (SSR) → REST PocketBase (interno) ("/admin" fa redirect a /_/, gestito da Nitro) ``` Caddy instrada per **path**, non per sottodominio o porta: `PUBLIC_DOMAIN` serve sia il sito che il pannello PocketBase. `/_/*` (dashboard) e `/api/*` (REST/file API) vanno **senza prefisso** a PocketBase — la sua dashboard referenzia se stessa con quei path assoluti, quindi non si possono instradare con uno strip-prefix (es. `/admin/*` riscritto): romperebbe gli asset/le chiamate della dashboard. Per questo `/api` è riservato a PocketBase, non a Nitro: gli endpoint del frontend vivono sotto `/content/*` (`frontend/server/routes/content/`, non `server/api/`). `/admin` è una route Nitro (`frontend/server/routes/admin.get.ts`) che fa redirect a `/_/`, non una regola Caddy: così funziona identico anche in sviluppo, dove Caddy non fa parte dello stack. Un solo dominio, un solo certificato TLS. Se cambi questa scelta di routing, spiega il trade-off prima (vedi [Vincoli](#vincoli)). - PocketBase è la **sola** fonte di verità editoriale (CMS + database SQLite in un solo processo). Niente altro backend (no Express/Nest/Fastify): se serve logica server, sta in Nitro (`frontend/server/`) o in una regola/migration PocketBase. - I visitatori pubblici non si autenticano mai. Solo il superuser usa l'auth PocketBase. - **Il browser dei visitatori pubblici non parla mai con PocketBase per i contenuti.** Le pagine chiamano gli endpoint Nitro in `frontend/server/routes/content/`, che sono l'unico posto dove si costruiscono query PocketBase. Così `NUXT_POCKETBASE_URL` resta l'indirizzo interno Docker, niente CORS e niente token nel client. Se aggiungi una vista, aggiungi l'endpoint lì (sotto `/content/*`, mai `/api/*`) e tipizza il ritorno in `shared/types/blog.ts`. Fanno eccezione, per costruzione: l'admin panel (`/_/`, uso superuser autenticato, raggiungibile anche da `/admin`) e le immagini cover, che il browser carica direttamente da `PUBLIC_POCKETBASE_URL` (`/api/files/...`, sola lettura, nessun'autenticazione richiesta né concessa). - Il Markdown dell'articolo è convertito in HTML **nell'endpoint**, non nel componente: il contenuto è già nell'HTML SSR e `marked` resta fuori dal bundle client. - Il sito è **bilingue** (italiano, sorgente, e inglese), fatto in casa senza `@nuxtjs/i18n` né file di traduzione runtime: due lingue, ~20 stringhe statiche, nessun plurale — la libreria costerebbe più codice di quanto risolva (il fallback su contenuti non tradotti, la localizzazione degli endpoint Nitro e gran parte della SEO restano comunque da scrivere a mano). `` è dinamico (per pagina, via `useSeo`), non più fisso. Le pagine inglesi vivono sotto prefisso `/en/*`, come wrapper sottili attorno agli stessi componenti "view" delle pagine italiane — vedi `docs/frontend.md#english-content`. Le stringhe UI statiche vivono **solo** in `frontend/shared/utils/i18n.ts`. Gli **articoli** hanno campi paralleli opzionali (`titleEn`/`contentEn`/`coverAltEn`, `nameEn` su categorie), tradotti **a mano** dall'editor in admin — nessuna traduzione automatica. Vedi `docs/content-model.md#english-content` per lo schema e la politica di fallback (404 pulito sul dettaglio non tradotto, filtro nelle liste). - `pocketbase/pb_migrations/*.js` definisce collection e regole: `listRule`/`viewRule` pubblici solo su `articles` (solo pubblicati, via `publishedAt`) e `categories`; `createRule`/ `updateRule`/`deleteRule` sempre `null` (solo superuser). Qualsiasi permesso in più va motivato. - SQLite (PocketBase) non è esposto pubblicamente sulla rete — solo tramite l'API PocketBase stessa. Media sul volume persistente `pocketbase-data`, mai binari fuori da lì. ## Comandi Package manager: quello del lockfile. Se non esiste ancora → `npm`. ```bash # frontend/ npm run dev # dev server npm run build # build produzione npm run typecheck # nuxi typecheck — obbligatorio prima di dichiarare fatto npm run lint npm run test:unit # Vitest, ambiente Nuxt: funzioni pure + componenti/composable npm run test:integration # Vitest, server Nitro reale + PocketBase effimero reale npm run test:e2e # Playwright, browser reale contro build + PocketBase effimero reale npm run test # i tre, in sequenza # singolo test: npx vitest run --config vitest.unit.config.ts tests/unit/queries.utils.test.ts npx vitest run --config vitest.integration.config.ts -t "lists only published articles" npx playwright test tests/e2e/home.spec.ts # pocketbase/ (nessun npm script: binario singolo, le migration si applicano da sole all'avvio) docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build pocketbase frontend # stack completo (con Caddy e domini reali) docker compose up -d --build docker compose logs -f pocketbase # stack locale senza domini né TLS: porte su localhost, niente Caddy docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build pocketbase frontend ``` `docker-compose.dev.yml` va passato **sempre esplicitamente**: non è un `override.yml` proprio per non finire per sbaglio in produzione esponendo le porte. `tests/` sta nella root del repo (non sotto `frontend/`), perché l'e2e esercita frontend e PocketBase insieme — gli strumenti (config Vitest/Playwright, `node_modules`) restano comunque in `frontend/`, l'unico progetto npm del repo, e puntano a `../tests/`. Playwright risolve i pacchetti dal `node_modules` più vicino al file di test: per questo `npm run test:e2e` crea prima (script `pretest:e2e`, idempotente) il symlink `tests/node_modules → ../frontend/node_modules`. I test di integration/e2e avviano un vero binario PocketBase effimero (scaricato una volta in `.cache/pocketbase/` nella root, versione allineata a `pocketbase/Dockerfile`) contro le vere migration in `pocketbase/pb_migrations/` — niente PocketBase mockato. `npm run test:e2e` richiede Chromium installato una tantum: `npx playwright install chromium` (senza `--with-deps`, che richiede `sudo`; se mancano librerie di sistema per il browser, installarle a parte). ## Modelli di contenuto | Type | Campi | |---|---| | Article | title, slug, content (Markdown), cover, coverAlt, category (rel), publishedAt, authorName, titleEn, contentEn, coverAltEn | | Category | name, slug, nameEn | Deliberatamente minimale: niente tag, nessun campo SEO separato. La meta description è ricavata dall'inizio del body (`summarise` in `frontend/server/utils/pocketbase.ts`), la data è `publishedAt`, l'immagine social è la cover. Non reintrodurre campi senza che servano davvero. PocketBase non ha Draft & Publish nativo: `publishedAt` vuoto = bozza, valorizzato (e non nel futuro) = pubblicato, imposto dalla `listRule`/`viewRule` della collection `articles`, non da codice applicativo. Le URL pubbliche usano lo **slug**, mai l'id del record. ## Frontend Rotte: `/`, `/blog`, `/blog/[slug]`, `/category/[slug]`, ciascuna con equivalente `/en/...`. - SSR o prerender per tutto ciò che è indicizzabile. Mai pagine blog client-only senza motivo scritto. - `