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.
131 lines
5.1 KiB
Markdown
131 lines
5.1 KiB
Markdown
# 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.
|
|
|
|
```text
|
|
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**
|
|
|
|
```bash
|
|
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.
|
|
|
|
```bash
|
|
chmod 600 .env
|
|
```
|
|
|
|
**3. Start the stack**
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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.
|
|
|
|
```bash
|
|
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](CLAUDE.md).
|
|
|
|
## License
|
|
|
|
Proprietary — Copyright (c) 2026 Davide Grilli. All rights reserved. See [LICENSE](LICENSE).
|