3.6 KiB
Web App
The public website: a Next.js App Router app that server-renders the pages editors publish in Web Manager. Everything visitors see comes from the backend's public API. Package: @crux-kit/web-app.
Architecture — request flow, layers, backend boundary, rendering registries, i18n, SEO — is documented in the Next.js web architecture.
| Tech | Version |
|---|---|
| Next.js | 16 (App Router, React Server Components) |
| React | 19 |
| next-intl | 4 — locale routing and translations |
| Tailwind | 4 — CSS-first, no config file |
| TypeScript | 5.9, strict, noEmit |
| Vitest | 4 — node environment, no DOM tests |
Configuration
| Variable | Required | Purpose |
|---|---|---|
BACKEND_API_URL |
yes — throws without it | Backend origin. No /web-manager/public suffix. |
SITE_URL |
in production | Canonical origin. Missing → http://localhost:3000 + a logged error. |
REVALIDATE_SECRET |
for purges to work | Must equal the backend's WEB_REVALIDATE_SECRET. |
Copy .env.example to .env. In Docker the backend URL must use the compose service name (http://backend:3333), not localhost.
Development
npm run dev:web
npm run test:web
npm run build:web
build runs tsc --noEmit before next build, so a type error fails the build.
Under Docker the web service runs npm run dev on port 3000 with .next on an anonymous volume. That volume survives rebuilds and is the usual cause of a stale bundle — recreate with docker compose up -d --force-recreate --renew-anon-volumes web.
Path alias: #* maps to the app root. vitest.config.ts mirrors it by hand (Vitest ignores tsconfig paths) and stubs server-only.
Tests
tests/ covers the pure logic (normalization, cache tags, registries, metadata, sitemap, site URL, structured data) in a node environment — component rendering is not covered and would need @testing-library/react. registry.test.ts keeps a hand-mirrored list of the backend's block and page types and asserts the registries match exactly — the tripwire for a block the CMS can publish but the site cannot draw.
Adding Things
| Task | Where |
|---|---|
| A new CMS block or page type | Spans three apps — follow Web Manager → Extending |
| A non-CMS route | A folder under app/[locale]/; keep the logic in web-runtime/ |
| A shared UI primitive | shared/ui/ — content-agnostic only |
| A new locale | Add it to site.config.ts and add the five i18n/translations/*/<locale>.json files |
| A new backend read | A method on WebRuntimeAPI with its tags, plus a normalizer for the response |