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.
Wires up @nuxt/eslint (the official Nuxt module) so `npm run lint`,
already documented in CLAUDE.md's pre-completion checklist, actually
exists. It auto-generates a flat config aware of Nuxt's auto-imports,
so it doesn't flag composables and utils that Nuxt injects without an
explicit import.
The interface was translated in en/it/es/fr via @nuxtjs/i18n. Drop the
module, the locale files, and the language switcher: the UI is now
static Italian text, with translation for foreign visitors left to the
Google Translate browser extension instead of the app.
The marks come from the simple-icons package (CC0) and are inlined at build
time — the earlier hand-drawn glyphs were wrong, the X mark in particular is
not a plain cross. Importing the three SVGs individually rather than the
package index keeps the other icons out of the bundle: 10 kB, not 5 MB.
Rendered monochrome in the surrounding text colour, with the text label kept
next to each one.
The interface is translated through @nuxtjs/i18n: strings live in
i18n/locales/*.json, routes are prefixed except for the default English, and
useLocaleHead emits the lang attribute, the hreflang alternates and og:locale.
The switcher is a native select rather than a custom dropdown: keyboard
support, the platform picker on mobile and correct labelling come for free.
Article content stays single-language — this translates the site chrome only.
The browser never talks to Strapi: pages call Nitro endpoints under
server/api/, which are the only place Strapi queries are built. That keeps the
Strapi URL internal to the Docker network and avoids CORS entirely.
Article bodies are rendered from Markdown to HTML in the endpoint, so the
content is server-rendered and crawlable, marked stays out of the client
bundle, and the meta description is derived from the opening of the body.
Pages carry canonical URLs, Open Graph tags and BlogPosting structured data,
and handle empty, missing and failed states.