davide 91181f2170 Add unit, integration and e2e test suites
tests/{unit,integration,e2e}/, at the repo root (not under frontend/,
since e2e exercises the frontend and PocketBase together) — all
against real code rather than mocks:

- unit/: Vitest (nuxt environment) — pure functions in server/utils
  and shared/utils, plus components/composables via
  @nuxt/test-utils/runtime's mountSuspended.
- integration/: Vitest (node environment) — @nuxt/test-utils/e2e's
  setup() builds and runs the real Nitro server against a real
  ephemeral PocketBase instance, exercising the actual /content/* and
  /admin routes end to end.
- e2e/: Playwright, a real browser against the built app and another
  ephemeral PocketBase, covering the user-facing flows (home, blog
  archive, article detail incl. JSON-LD, category filter, 404s, the
  /admin redirect).

The Vitest/Playwright tooling (and node_modules) stays in frontend/,
the repo's only npm project; its configs just point at ../tests/.
Playwright resolves packages from the node_modules nearest each spec
file, so `npm run test:e2e` first symlinks tests/node_modules to
frontend/node_modules (pretest:e2e, idempotent, gitignored).

tests/support/pocketbase.ts is the shared piece behind integration
and e2e: it downloads the same pinned PocketBase binary
pocketbase/Dockerfile uses, starts it against the real
pocketbase/pb_migrations/ (not a copy), and seeds it from
tests/support/fixtures.ts — so these tests run against the exact
schema and API rules that ship to production, catching the class of
bug that only shows up when PocketBase actually enforces them (e.g. a
wrong filter/fields query string silently returning nothing, or too
much).

CLAUDE.md and docs/frontend.md document the new npm scripts and the
single-test commands for each layer.
2026-09-11 11:48:43 +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%