davide bd26cc8ea6 Renumber article slugs to sequential article-N
One-time PocketBase migration that replaces every article's slug with
article-1, article-2, ... ordered by publishedAt (oldest first),
requested explicitly by the site owner in place of the existing
descriptive slugs.
2026-09-14 14:44:07 +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%