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.
This commit is contained in:
@@ -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): `<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 — 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.
|
||||
- `<script setup lang="ts">`, Composition API. Convenzioni Nuxt standard (`pages/`, `components/`,
|
||||
`composables/`, `layouts/`, `server/`).
|
||||
- Un solo punto di accesso a Strapi: un composable/util tipizzato. Non sparpagliare `$fetch` nelle pagine.
|
||||
- Un solo punto di accesso a PocketBase: un composable/util tipizzato. Non sparpagliare `$fetch` nelle pagine.
|
||||
- Tipizza esplicitamente il confine API. Niente `any` — `unknown` + narrowing.
|
||||
- Query Strapi: richiedi solo i campi e le relazioni che servono (`fields`, `populate` mirati).
|
||||
- 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.
|
||||
@@ -125,13 +130,14 @@ nuovo se non esiste una sezione adatta. Non lasciare `docs/` disallineata col co
|
||||
Correttezza e integrità dati → sicurezza → semplicità → SEO/a11y → performance.
|
||||
|
||||
Nessuna dipendenza, astrazione o servizio senza un bisogno concreto e attuale. Preferisci i
|
||||
built-in Strapi al reimplementare funzioni CMS in Nuxt.
|
||||
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 sui ruoli Strapi.
|
||||
- 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,
|
||||
|
||||
Reference in New Issue
Block a user