diff --git a/.env.example b/.env.example index 501b4b8..e3a6478 100644 --- a/.env.example +++ b/.env.example @@ -4,25 +4,20 @@ PUBLIC_DOMAIN=blog.localhost ACME_EMAIL=admin@example.com -# --- Database --- -POSTGRES_DB=blog -POSTGRES_USER=blog -POSTGRES_PASSWORD=change-me - -# --- Strapi --- -# Generate each secret with: openssl rand -base64 32 -APP_KEYS=change-me-1,change-me-2 -API_TOKEN_SALT=change-me -ADMIN_JWT_SECRET=change-me -TRANSFER_TOKEN_SALT=change-me -JWT_SECRET=change-me -ENCRYPTION_KEY=change-me +# --- PocketBase --- +# Bootstraps (or updates, on restart) the initial superuser account — the +# only login the admin UI accepts. Pick a real password (10+ chars); never +# committed as anything but this placeholder. +POCKETBASE_ADMIN_EMAIL=admin@example.com +POCKETBASE_ADMIN_PASSWORD=change-me-1234 # --- Frontend --- -# Server-side only: internal Docker address of Strapi. -STRAPI_URL=http://cms:1337 +# Server-side only: internal Docker address of PocketBase. +POCKETBASE_URL=http://pocketbase:8090 # Public base URL of the website, used for canonical URLs and Open Graph. PUBLIC_SITE_URL=https://blog.localhost -# Public base URL of Strapi, used to build absolute media URLs in the browser. -# Same origin as PUBLIC_SITE_URL: Caddy proxies /admin and /uploads to Strapi. -PUBLIC_STRAPI_URL=https://blog.localhost +# Public base URL of PocketBase, used to build absolute cover-image URLs. +# Same origin as PUBLIC_SITE_URL: Caddy proxies /_/* and /api/* there (the +# admin UI is at PUBLIC_POCKETBASE_URL/_/, reachable from PUBLIC_SITE_URL/admin +# too — see caddy/Caddyfile). +PUBLIC_POCKETBASE_URL=https://blog.localhost diff --git a/CLAUDE.md b/CLAUDE.md index ac2b4f6..dce63e9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,39 +16,45 @@ sitemap e `robots.txt` dinamici, ricerca, e i test (nessun framework ancora conf ## Architettura ```text -Browser → Caddy ─┬─ /admin e i path dei plugin Strapi → Strapi 5 → PostgreSQL - └─ tutto il resto → Nuxt 4 (SSR) → REST Strapi +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: `PUBLIC_DOMAIN` serve sia il sito che il -pannello Strapi. Ogni plugin Strapi monta la propria API admin sul proprio path di primo -livello, non tutto sotto `/admin` (es. `/content-manager`, `/upload`, `/i18n`...): l'elenco -completo dei prefissi da instradare a Strapi vive nel `Caddyfile`. Se aggiungi un plugin -Strapi, aggiungi il suo prefisso lì. Niente di questo tocca `/api`, riservato agli endpoint -Nitro del frontend. Un solo dominio, un solo certificato TLS. +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)). -- Strapi è la **sola** fonte di verità editoriale. Niente altro backend (no Express/Nest/Fastify): - se serve logica server, sta in Nitro (`frontend/server/`) o in un controller Strapi. -- I visitatori pubblici non si autenticano mai. Solo editor/admin usano l'auth Strapi. -- **Il browser dei visitatori pubblici non parla mai con Strapi.** Le pagine chiamano gli - endpoint Nitro in `frontend/server/api/`, che sono l'unico posto dove si costruiscono - query Strapi. Così `NUXT_STRAPI_URL` resta l'indirizzo interno Docker, niente CORS e - niente token nel client. Se aggiungi una vista, aggiungi l'endpoint lì e tipizza il - ritorno in `shared/types/blog.ts`. Fanno eccezione, per costruzione: l'admin panel - (`/admin`, uso editor/admin autenticato) e le immagini cover, che il browser carica - direttamente da `PUBLIC_STRAPI_URL` (`/uploads/...`, sola lettura, nessun'autenticazione - richiesta né concessa). +- 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): `` è 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 — la localizzazione di Strapi è disattivata. -- `cms/src/index.ts` (`bootstrap`) dà al ruolo Public solo `find`/`findOne` su Article e Category, - e disattiva la registrazione pubblica: non esistono utenti front-end, solo amministratori. - Qualsiasi permesso in più va motivato. -- Postgres non è esposto pubblicamente. Media su volume persistente, mai binari nel DB. + 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 @@ -61,17 +67,15 @@ npm run build # build produzione npm run typecheck # nuxi typecheck — obbligatorio prima di dichiarare fatto npm run lint -# cms/ -npm run develop # Strapi con content-type builder attivo -npm run build # admin panel -npm run start # produzione +# 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 cms +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 database cms frontend +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 @@ -84,15 +88,16 @@ lanciare **un singolo test**. | Type | Campi | |---|---| -| Article | title, slug (UID da title), content (Markdown), cover, category (rel) | +| Article | title, slug, content (Markdown), cover, coverAlt, category (rel), publishedAt, authorName | | Category | name, slug | -Deliberatamente minimale: **niente Author** (l'unico autore è l'admin), niente tag, nessun campo -SEO separato. La meta description è ricavata dall'inizio del body (`summarise` in -`frontend/server/utils/strapi.ts`), la data è il `publishedAt` di Draft & Publish, l'immagine -social è la cover. Non reintrodurre questi campi senza che servano davvero. +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. -Draft & Publish attivo su Article. Le URL pubbliche usano lo **slug**, mai l'id numerico. +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 @@ -101,9 +106,9 @@ Rotte: `/`, `/blog`, `/blog/[slug]`, `/category/[slug]`. - SSR o prerender per tutto ciò che è indicizzabile. Mai pagine blog client-only senza motivo scritto. - `