diff --git a/.env.example b/.env.example index 2257648..501b4b8 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,6 @@ # --- Domains (Caddy) --- PUBLIC_DOMAIN=blog.localhost -CMS_DOMAIN=cms.blog.localhost ACME_EMAIL=admin@example.com # --- Database --- @@ -25,4 +24,5 @@ STRAPI_URL=http://cms:1337 # 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. -PUBLIC_STRAPI_URL=https://cms.blog.localhost +# Same origin as PUBLIC_SITE_URL: Caddy proxies /admin and /uploads to Strapi. +PUBLIC_STRAPI_URL=https://blog.localhost diff --git a/CLAUDE.md b/CLAUDE.md index 87a277a..442fdd0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,17 +16,25 @@ sitemap e `robots.txt` dinamici, ricerca, e i test (nessun framework ancora conf ## Architettura ```text -Browser → Caddy ─┬─ dominio pubblico → Nuxt 4 (SSR) → REST Strapi - └─ sottodominio CMS → Strapi 5 → PostgreSQL +Browser → Caddy ─┬─ /admin, /uploads → Strapi 5 → PostgreSQL + └─ tutto il resto → Nuxt 4 (SSR) → REST Strapi ``` +Caddy instrada per **path**, non per sottodominio: `PUBLIC_DOMAIN` serve sia il sito che, +sotto `/admin` e `/uploads`, il pannello Strapi (Strapi 5 annida l'intera admin UI e la sua +API sotto `/admin`, senza toccare `/api`). Un solo dominio, un solo certificato TLS. + - 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 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`. +- **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). - 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** è tradotta in en/it/es/fr con `@nuxtjs/i18n`: stringhe in diff --git a/README.md b/README.md index 729695b..701217b 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ Blog platform: a public website built with Nuxt, and a private Strapi CMS where articles are written. Everything runs behind Caddy via Docker Compose. ```text -Browser → Caddy ─┬─ public domain → Nuxt (website) - └─ cms subdomain → Strapi (CMS) → PostgreSQL +Browser → Caddy ─┬─ /admin, /uploads → Strapi (CMS) → PostgreSQL + └─ everything else → Nuxt (website) ``` There are no front-end accounts: sign-up is disabled and only administrators write @@ -46,7 +46,8 @@ The first build takes a few minutes. `docker-compose.dev.yml` publishes the port `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 +**4. Create the administrator account** at http://localhost:1337/admin (dev bypasses Caddy, +so the CMS is reached directly on its port). 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** @@ -72,16 +73,15 @@ 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 +**1. Point the DNS at the server.** One `A` record (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 | +| `example.com` | the website and, at `/admin`, 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. +Wait for the record to resolve before starting the stack — Caddy requests the +certificate 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 @@ -92,10 +92,9 @@ reachable inside the Docker network — do not publish their ports. | 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` | +| `PUBLIC_STRAPI_URL` | `https://example.com` — same origin, Caddy proxies `/admin` and `/uploads` to Strapi | | `STRAPI_URL` | leave it as `http://cms:1337` — internal address, never public | Then generate **fresh** secrets on that machine with the same loop as in development — @@ -112,11 +111,11 @@ already ignored. docker compose up -d --build ``` -This time Caddy is included: it serves the website on `PUBLIC_DOMAIN`, the CMS on -`CMS_DOMAIN`, obtains and renews the TLS certificates on its own, and adds HSTS and the -other security headers. +This time Caddy is included: it serves both the website and, under `/admin` and +`/uploads`, the CMS on `PUBLIC_DOMAIN`, obtains and renews the TLS certificate on its own, +and adds HSTS and the other security headers. -**5. Create the administrator account** at `https://cms.example.com/admin`, immediately, +**5. Create the administrator account** at `https://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. diff --git a/caddy/Caddyfile b/caddy/Caddyfile index a9ff3a4..5e7fae4 100644 --- a/caddy/Caddyfile +++ b/caddy/Caddyfile @@ -15,15 +15,21 @@ {$PUBLIC_DOMAIN} { import security_headers encode zstd gzip - reverse_proxy frontend:3000 -} -{$CMS_DOMAIN} { - import security_headers - encode zstd gzip - # Uploaded media can be large; Strapi's own limit still applies. - request_body { - max_size 100MB + # /admin and /uploads go to Strapi; everything else to the frontend. + # Strapi 5 nests the whole admin panel (UI + its own API) under /admin, + # so this single prefix is enough - it never touches /api, which stays + # reserved for the frontend's own Nitro endpoints. + @cms path /admin* /uploads* + handle @cms { + # Uploaded media can be large; Strapi's own limit still applies. + request_body { + max_size 100MB + } + reverse_proxy cms:1337 + } + + handle { + reverse_proxy frontend:3000 } - reverse_proxy cms:1337 } diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index d450a12..e87bcd7 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -9,8 +9,6 @@ services: cms: ports: - "127.0.0.1:1337:1337" - environment: - STRAPI_URL: http://localhost:1337 frontend: ports: diff --git a/docker-compose.yml b/docker-compose.yml index 91a8036..f03d062 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,7 +36,6 @@ services: TRANSFER_TOKEN_SALT: ${TRANSFER_TOKEN_SALT} JWT_SECRET: ${JWT_SECRET} ENCRYPTION_KEY: ${ENCRYPTION_KEY} - STRAPI_URL: https://${CMS_DOMAIN} volumes: - cms-uploads:/app/public/uploads @@ -71,7 +70,6 @@ services: - 8.8.8.8 environment: PUBLIC_DOMAIN: ${PUBLIC_DOMAIN} - CMS_DOMAIN: ${CMS_DOMAIN} ACME_EMAIL: ${ACME_EMAIL} volumes: - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro