Files
blog/CLAUDE.md
T
davide 33b49c5dbb Add hand-rolled English support (/en/*) alongside the Italian site
PocketBase gains optional, manually-authored English fields on articles
and categories (same record, same slug), and the frontend serves an /en
counterpart of every dynamic route via thin page wrappers around shared
view components — no i18n library, consistent with the project's existing
minimalism stance for a two-locale site with ~20 UI strings.

An article/category with no translation 404s cleanly on its own /en
detail page and is filtered out of /en listings, and the header's
language-switch link falls back to the English blog index rather than a
dead link; resolving that requires a global route middleware, since the
layout's header renders before the page content in document order and so
can't react to state a page component sets during its own async setup.
2026-09-11 13:53:17 +02:00

10 KiB

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

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

  • 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). <html lang> è 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.

# 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.
  • <script setup lang="ts">, Composition API. Convenzioni Nuxt standard (pages/, components/, composables/, layouts/, server/).
  • Un solo punto di accesso a PocketBase: un composable/util tipizzato. Non sparpagliare $fetch nelle pagine.
  • Tipizza esplicitamente il confine API. Niente anyunknown + narrowing.
  • Query PocketBase: richiedi solo i campi e le relazioni che servono (fields, expand mirati). Gestisci sempre 404, lista vuota, errore API.
  • Ogni articolo indicizzabile: title unico, meta description, canonical, Open Graph, JSON-LD BlogPosting, gerarchia heading semantica. Il contenuto deve esistere nell'HTML server-rendered.
  • Accessibilità non negoziabile: focus visibile, input etichettati, alt significativi, link descrittivi, navigazione da tastiera.

Il riferimento Hostinger è solo ispirazione visiva. Non copiare codice o asset.

Documentazione

docs/ descrive struttura e comportamento del repo (architettura, content model, frontend) a un livello più approfondito di questo file. Se implementi nuove funzionalità, endpoint, content type o cambi l'architettura (nuovo servizio, nuova rotta Caddy, nuovo modo di scambiare dati tra frontend e CMS), aggiorna il file docs/*.md pertinente nello stesso commit, o creane uno nuovo se non esiste una sezione adatta. Non lasciare docs/ disallineata col codice.

Priorità

Correttezza e integrità dati → sicurezza → semplicità → SEO/a11y → performance.

Nessuna dipendenza, astrazione o servizio senza un bisogno concreto e attuale. Preferisci i built-in PocketBase (regole per-collection, migration) al reimplementare funzioni CMS in Nuxt.

Vincoli

  • Mai committare .env, segreti, token, credenziali, chiavi. Mantieni .env.example sanificato.
  • Mai hard-codare domini di produzione, URL privilegiati o credenziali. Vanno in env var.
  • Non indebolire auth, CORS, TLS o security header per comodità. Least privilege sulle regole PocketBase.
  • Richiedono approvazione esplicita: operazioni distruttive, migrazioni irreversibili, modifiche a dati o configurazione di produzione, cambi di credenziali.
  • Non riformattare file non correlati, non fare refactor collaterali, non riscrivere la history, non force-push.
  • Le decisioni architetturali di questo file non si cambiano in silenzio: spiega il trade-off prima.

Prima di dichiarare completo

Lint → test → typecheck → build dell'app toccata, con gli script del package.json relativo. Non affermare che un check è passato se non l'hai eseguito. Se hai toccato architettura o funzionalità, verifica di aver aggiornato docs/ (vedi Documentazione). Chiudi riassumendo cosa è cambiato e quali rischi restano aperti.