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
2026-09-11 11:25:14 +02:00
2026-09-11 11:25:14 +02:00
2026-09-11 11:25:14 +02:00
2026-09-11 11:25:14 +02:00

Blog

Blog platform: a public website built with Nuxt, and a private PocketBase CMS where the articles are written. Everything runs behind Caddy via Docker Compose.

Browser → Caddy ─┬─ /_/*, /api/*      → PocketBase (CMS)
                 └─ everything else   → Nuxt (website), which also redirects /admin → /_/

There are no front-end accounts: only the superuser writes content. An article is a title, a Markdown body, a cover image and a category.

Development

No domain and no TLS needed — the services are published on localhost instead of going through Caddy. Requires Docker.

1. Create the environment file

cp .env.example .env

2. Set the admin credentials. Replace POCKETBASE_ADMIN_EMAIL and POCKETBASE_ADMIN_PASSWORD in .env with your own — this is the superuser account PocketBase creates (or updates) on every start. The domain and URL variables can stay as they are for local use.

chmod 600 .env

3. Start the stack

docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build pocketbase 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. Open the admin UI at http://localhost:3000/admin (redirects to PocketBase's dashboard — this works in dev too, without Caddy, since the redirect is handled by the frontend itself) and log in with the credentials from .env.

5. Write something. In the admin UI: create a categories record, then an articles record (the content field is Markdown), then set publishedAt — the website only shows articles whose publishedAt is set and not in the future.

6. Open the website at http://localhost:3000 — home, /blog, /blog/<slug> and /category/<slug>.

Useful commands:

docker compose logs -f pocketbase                            # 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 CMS database and media

To iterate on the frontend without rebuilding an image every time: cd frontend && npm run dev. It defaults to http://localhost:8090 for PocketBase, so it works against the containerised CMS as is; override it with NUXT_POCKETBASE_URL if needed.

Production

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, and, at /admin, the PocketBase admin UI

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. The admin UI shares port 443 with the site (reachable by anyone who knows /admin, protected only by the superuser login, so keep the password strong); PocketBase itself is never published directly, only Caddy's proxy to it.

3. Configure the environment. Copy .env.example to .env on the server and set:

Variable Value
PUBLIC_DOMAIN example.com
ACME_EMAIL a mailbox you read — Let's Encrypt sends expiry warnings there
PUBLIC_SITE_URL https://example.com
PUBLIC_POCKETBASE_URL https://example.com — same origin, Caddy proxies /_/ and /api/ there
POCKETBASE_URL leave it as http://pocketbase:8090 — internal address, never public
POCKETBASE_ADMIN_EMAIL / POCKETBASE_ADMIN_PASSWORD your real superuser credentials

Keep .env out of version control; it is already ignored.

Changing POCKETBASE_ADMIN_PASSWORD later and restarting rotates the superuser password immediately (the entrypoint upserts it on every boot) — a credential change, so treat it with the same care as any production credential rotation.

4. Start everything

docker compose up -d --build

This time Caddy is included: it serves both the website and, under /admin, the CMS admin UI on PUBLIC_DOMAIN, obtains and renews the TLS certificate on its own, and adds HSTS and the other security headers.

5. Log into the admin UI at https://example.com/admin with the credentials from .env, then publish as in development.

6. Back up what is not in git: the pocketbase-data volume (database and uploaded media together). Nothing else on the server holds state.

docker run --rm -v blog_pocketbase-data:/data -v "$PWD:/out" alpine tar czf /out/pocketbase-data.tar.gz -C /data .

Updating a running site: pull the new code, then docker compose up -d --build. PocketBase applies new pb_migrations/ files at startup; take a backup first.

Architecture, conventions and constraints are documented in CLAUDE.md.

License

Proprietary — Copyright (c) 2026 Davide Grilli. All rights reserved. See LICENSE.

S
Description
No description provided
Readme
1.2 MiB
Languages
TypeScript 60.1%
Vue 29.7%
JavaScript 5%
CSS 4%
Dockerfile 0.9%
Other 0.3%