Document the stack, the dev workflow and the production deployment
README gets step-by-step instructions for both: secrets generation and the local stack on one side, DNS records, firewall, environment variables, TLS and backups on the other. CLAUDE.md records the decisions that are not visible in the code: why the browser never reaches Strapi, why the content model stays this small, and where the public permissions are granted.
This commit is contained in:
@@ -10,17 +10,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## Stato
|
## Stato
|
||||||
|
|
||||||
Repository a **stato bootstrap**: contiene solo questo file. Tutto ciò che segue descrive il
|
Struttura, content type, pagine blog, Docker e Caddy sono in piedi. Restano da fare:
|
||||||
target, non l'esistente. Verifica sempre cosa esiste prima di assumere.
|
sitemap e `robots.txt` dinamici, ricerca, e i test (nessun framework ancora configurato).
|
||||||
|
|
||||||
Ordine di costruzione:
|
|
||||||
|
|
||||||
1. `frontend/` — Nuxt 4 (`npx nuxi@latest init frontend --package-manager npm --no-install-git`)
|
|
||||||
2. `cms/` — Strapi 5 (`npx create-strapi@latest cms --typescript --dbclient=postgres --no-example --no-git-init`)
|
|
||||||
3. `docker-compose.yml` + `.env.example` — Postgres, cms, frontend
|
|
||||||
4. Content types Strapi + permessi ruolo Public (solo `find`/`findOne`)
|
|
||||||
5. Client Strapi in Nuxt + pagine blog
|
|
||||||
6. `caddy/Caddyfile` + hardening
|
|
||||||
|
|
||||||
## Architettura
|
## Architettura
|
||||||
|
|
||||||
@@ -32,8 +23,15 @@ Browser → Caddy ─┬─ dominio pubblico → Nuxt 4 (SSR) → REST Strapi
|
|||||||
- Strapi è la **sola** fonte di verità editoriale. Niente altro backend (no Express/Nest/Fastify):
|
- 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.
|
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.
|
- I visitatori pubblici non si autenticano mai. Solo editor/admin usano l'auth Strapi.
|
||||||
- Nuxt parla con Strapi **server-side** (`STRAPI_URL` interno Docker). Il token privilegiato non
|
- **Il browser non parla mai con Strapi.** Le pagine chiamano gli endpoint Nitro in
|
||||||
raggiunge mai il browser: se serve, sta in `runtimeConfig` (non `public`).
|
`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`.
|
||||||
|
- 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.
|
||||||
|
- `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.
|
- Postgres non è esposto pubblicamente. Media su volume persistente, mai binari nel DB.
|
||||||
|
|
||||||
## Comandi
|
## Comandi
|
||||||
@@ -52,11 +50,17 @@ npm run develop # Strapi con content-type builder attivo
|
|||||||
npm run build # admin panel
|
npm run build # admin panel
|
||||||
npm run start # produzione
|
npm run start # produzione
|
||||||
|
|
||||||
# stack
|
# stack completo (con Caddy e domini reali)
|
||||||
docker compose up -d --build
|
docker compose up -d --build
|
||||||
docker compose logs -f cms
|
docker compose logs -f cms
|
||||||
|
|
||||||
|
# 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.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
|
Nessun test framework è ancora configurato. Se ne aggiungi uno, documenta qui il comando per
|
||||||
lanciare **un singolo test**.
|
lanciare **un singolo test**.
|
||||||
|
|
||||||
@@ -64,10 +68,13 @@ lanciare **un singolo test**.
|
|||||||
|
|
||||||
| Type | Campi |
|
| Type | Campi |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Article | title, slug (UID da title), excerpt, content (rich text), cover, author (rel), category (rel), tags (rel n:n), publishedDate, seoTitle, seoDescription, seoImage |
|
| Article | title, slug (UID da title), content (Markdown), cover, category (rel) |
|
||||||
| Category | name, slug |
|
| Category | name, slug |
|
||||||
| Tag | name, slug |
|
|
||||||
| Author | name, biography, image |
|
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.
|
||||||
|
|
||||||
Draft & Publish attivo su Article. Le URL pubbliche usano lo **slug**, mai l'id numerico.
|
Draft & Publish attivo su Article. Le URL pubbliche usano lo **slug**, mai l'id numerico.
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +1,132 @@
|
|||||||
# Blog
|
# Blog
|
||||||
|
|
||||||
Blog platform: a public website built with Nuxt, and a private Strapi CMS where
|
Blog platform: a public website built with Nuxt, and a private Strapi CMS where the
|
||||||
editors write the articles. Everything runs behind Caddy via Docker Compose.
|
articles are written. Everything runs behind Caddy via Docker Compose.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Browser → Caddy ─┬─ public domain → Nuxt (website)
|
Browser → Caddy ─┬─ public domain → Nuxt (website)
|
||||||
└─ cms subdomain → Strapi (CMS) → PostgreSQL
|
└─ cms subdomain → Strapi (CMS) → PostgreSQL
|
||||||
```
|
```
|
||||||
|
|
||||||
> Status: bootstrap. The application code is not in place yet.
|
There are no front-end accounts: sign-up is disabled and only administrators write
|
||||||
|
content. An article is a title, a Markdown body, a cover image and a category.
|
||||||
|
|
||||||
## Getting started
|
## Development
|
||||||
|
|
||||||
Requires Docker and Node.js 20+.
|
No domain and no TLS needed — the services are published on localhost instead of going
|
||||||
|
through Caddy. Requires Docker.
|
||||||
|
|
||||||
|
**1. Create the environment file**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Generate the secrets** and paste them into `.env`. Strapi refuses to start with the
|
||||||
|
placeholder values.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl rand -base64 32 # run once per secret
|
||||||
|
```
|
||||||
|
|
||||||
|
Fill in `POSTGRES_PASSWORD`, both values of `APP_KEYS`, `API_TOKEN_SALT`,
|
||||||
|
`ADMIN_JWT_SECRET`, `TRANSFER_TOKEN_SALT`, `JWT_SECRET` and `ENCRYPTION_KEY`. The domain
|
||||||
|
and URL variables can stay as they are for local use.
|
||||||
|
|
||||||
|
**3. Start the stack**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build database cms frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
The first build takes a few minutes. `docker-compose.dev.yml` publishes the ports on
|
||||||
|
`127.0.0.1` and leaves Caddy out; it must always be passed explicitly, so it can never be
|
||||||
|
picked up by accident in production.
|
||||||
|
|
||||||
|
**4. Create the administrator account** at http://localhost:1337/admin. This is the first
|
||||||
|
run, so the form creates the account — pick your own credentials.
|
||||||
|
|
||||||
|
**5. Write something.** In the admin panel: create a **Category**, then an **Article**
|
||||||
|
(the body field is Markdown), then press **Publish** — the website only shows published
|
||||||
|
content.
|
||||||
|
|
||||||
|
**6. Open the website** at http://localhost:3000 — home, `/blog`, `/blog/<slug>` and
|
||||||
|
`/category/<slug>`.
|
||||||
|
|
||||||
|
Useful commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose logs -f cms # follow the CMS logs
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml restart frontend
|
||||||
|
docker compose down # stop, keep the data
|
||||||
|
docker compose down -v # stop and WIPE the database and media
|
||||||
|
```
|
||||||
|
|
||||||
|
To iterate on the code without rebuilding an image every time, run a package directly —
|
||||||
|
`cd frontend && npm run dev`, or `cd cms && npm run develop`. The frontend defaults to
|
||||||
|
`http://localhost:1337` for Strapi, so it works against the containerised CMS as is;
|
||||||
|
override it with `NUXT_STRAPI_URL` if needed. Strapi reads its own `cms/.env`.
|
||||||
|
|
||||||
|
## Production
|
||||||
|
|
||||||
|
**1. Point the DNS at the server.** Two `A` records (and `AAAA` if you have IPv6) on the
|
||||||
|
public IP of the machine:
|
||||||
|
|
||||||
|
| Record | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `example.com` | the website |
|
||||||
|
| `cms.example.com` | the Strapi admin panel |
|
||||||
|
|
||||||
|
Wait for the records to resolve before starting the stack — Caddy requests the
|
||||||
|
certificates on the first boot and a failed challenge means a retry delay.
|
||||||
|
|
||||||
|
**2. Open the firewall** for ports `80` and `443` only. Port `80` is required: Caddy uses
|
||||||
|
it for the ACME challenge and to redirect to HTTPS. PostgreSQL, Strapi and Nuxt are only
|
||||||
|
reachable inside the Docker network — do not publish their ports.
|
||||||
|
|
||||||
|
**3. Configure the environment.** Copy `.env.example` to `.env` on the server and set:
|
||||||
|
|
||||||
|
| Variable | Value |
|
||||||
|
|---|---|
|
||||||
|
| `PUBLIC_DOMAIN` | `example.com` |
|
||||||
|
| `CMS_DOMAIN` | `cms.example.com` |
|
||||||
|
| `ACME_EMAIL` | a mailbox you read — Let's Encrypt sends expiry warnings there |
|
||||||
|
| `PUBLIC_SITE_URL` | `https://example.com` |
|
||||||
|
| `PUBLIC_STRAPI_URL` | `https://cms.example.com` |
|
||||||
|
| `STRAPI_URL` | leave it as `http://cms:1337` — internal address, never public |
|
||||||
|
|
||||||
|
Then generate **fresh** secrets for that machine (`openssl rand -base64 32`), different
|
||||||
|
from the development ones. Keep `.env` out of version control; it is already ignored.
|
||||||
|
|
||||||
|
> Changing `APP_KEYS`, `ADMIN_JWT_SECRET` or `JWT_SECRET` later logs everyone out.
|
||||||
|
> Changing `ENCRYPTION_KEY` after content exists makes already-encrypted values
|
||||||
|
> unreadable. Set them once, then back up the file somewhere safe.
|
||||||
|
|
||||||
|
**4. Start everything**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env # fill in the secrets, never commit this file
|
|
||||||
docker compose up -d --build
|
docker compose up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
Then open the CMS URL to create the first admin account and start publishing.
|
This time Caddy is included: it serves the website on `PUBLIC_DOMAIN`, the CMS on
|
||||||
The public site picks up published articles automatically.
|
`CMS_DOMAIN`, obtains and renews the TLS certificates on its own, and adds HSTS and the
|
||||||
|
other security headers.
|
||||||
|
|
||||||
## Working on it
|
**5. Create the administrator account** at `https://cms.example.com/admin`, immediately,
|
||||||
|
before anyone else finds the URL — the first visitor to that form is the one who gets the
|
||||||
|
account. Then publish as in development.
|
||||||
|
|
||||||
|
**6. Back up what is not in git**: the `postgres-data` volume (all content) and the
|
||||||
|
`cms-uploads` volume (all images). Nothing else on the server holds state.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd frontend && npm run dev # website
|
docker compose exec -T database pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" > backup.sql
|
||||||
cd cms && npm run develop # CMS
|
docker run --rm -v blog_cms-uploads:/data -v "$PWD:/out" alpine tar czf /out/uploads.tar.gz -C /data .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Updating a running site**: pull the new code, then `docker compose up -d --build`.
|
||||||
|
Strapi applies its own schema changes at startup; take a backup first.
|
||||||
|
|
||||||
Architecture, conventions and constraints are documented in [CLAUDE.md](CLAUDE.md).
|
Architecture, conventions and constraints are documented in [CLAUDE.md](CLAUDE.md).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
Reference in New Issue
Block a user