Files
blog/CLAUDE.md
T
davide 49c42ecc96 Replace Strapi CMS with PocketBase
Strapi + Postgres are gone in favor of PocketBase: a single Go binary
with embedded SQLite, built-in admin UI and per-collection API rules.
No content existed yet, so this is a clean swap with no data migration.

Collections and rules are defined as code in pocketbase/pb_migrations/
and applied automatically on first boot. Draft & Publish has no native
PocketBase equivalent, so it's reproduced with a nullable `publishedAt`
field enforced by listRule/viewRule, matching the old Strapi semantics.

Routing flips: PocketBase's admin UI and REST/file API are hardwired to
`/_/` and `/api/*` at the domain root (its own dashboard assets and API
calls reference those paths directly, so a stripped path prefix like
`/admin/*` would break them). `/api` is therefore reserved for
PocketBase now, and the frontend's Nitro endpoints move to `/content/*`
(frontend/server/routes/content/, not server/api/). A `/admin` vanity
route in Nitro (not Caddy) redirects to `/_/`, so it works the same in
dev, where Caddy isn't part of the stack, and in production.

frontend/server/utils/strapi.ts becomes pocketbase.ts; queries.ts is
rewritten for PocketBase's filter/sort/fields/expand query syntax.
StrapiImage becomes MediaImage (no width/height — PocketBase file
fields don't store dimensions, and the cover images already reserve
their aspect ratio via CSS, so this is not a regression).

docs/*.md, CLAUDE.md and README.md are updated in the same commit.
2026-09-11 11:25:14 +02:00

8.3 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 e Caddy sono in piedi. Restano da fare: sitemap e robots.txt dinamici, ricerca, e i test (nessun framework ancora configurato).

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.
  • L'interfaccia è solo in italiano, stringhe statiche nei componenti (niente @nuxtjs/i18n, niente file di traduzione): <html lang="it"> è fisso in nuxt.config.ts. La traduzione per i visitatori stranieri è delegata all'estensione Google Translate del browser, non è gestita dall'app. I contenuti restano monolingua.
  • 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

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

Nessun test framework è ancora configurato. Se ne aggiungi uno, documenta qui il comando per lanciare un singolo test.

Modelli di contenuto

Type Campi
Article title, slug, content (Markdown), cover, coverAlt, category (rel), publishedAt, authorName
Category name, slug

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

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