Skip to main content

Web overview

Status: Scaffolded 2026-09-24

vegan-grove-web is the public face and the browser client: vegangrove.org for anyone, /app for signed-in members. It is a Next 15 App Router application deployed on Amplify Hosting (web app deployment). It renders public content on the server for reach and privacy (no client-side data fetching for public pages means no member token on public pages) and behaves as a normal client app under /app.

Stack​

ConcernChoice
FrameworkNext 15, App Router, TypeScript, src/ directory, Turbopack
StylingTailwind 4, shadcn/ui components generated into the repo, --vg-* tokens in globals.css
Themenext-themes, class strategy, dark by default, light must work
MapMapLibre GL with the OpenFreeMap liberty style, loaded with dynamic(..., { ssr: false }). Its tile worker is self-hosted under public/maplibre/ (copied on prebuild) because Turbopack hashes the worker and its shared chunk separately, which breaks the worker's relative import and leaves the map blank.
Lint, formatBiome (the create-next-app ESLint option was declined)
E2EPlaywright smoke in test:e2e, outside validate
Fontssystem UI and system monospace stacks; next/font is not used

What is deliberately absent​

  • No analytics. No GA, PostHog, Vercel Analytics, or Speed Insights. Aggregate numbers come from GET /api/stats. See ADR 0007.
  • No external fonts or icon CDNs. A font request to a third party is a page-view log at that third party. Icons are inlined SVG from a local set.
  • No server-side secrets. The web app has no database, no AWS credentials, no API key. Everything it needs it asks the API for with the member's own session. The one environment variable that matters is NEXT_PUBLIC_API_BASE_URL.
  • No token in JavaScript. The session is an httpOnly cookie (auth and sessions).
  • No pages/ directory, no getServerSideProps; everything is App Router.

Layout​

src/
app/
(site)/ public pages under the shared header and footer, plus login and signup
app/ member area: the layout validates the session, then feed, messages, friends, companion, settings
api/session/route.ts the only code that sees the session token; sets and clears vg_session
llms.txt/ robots.txt/ sitemap.xml/ route handlers built from src/lib/site.ts
globals.css the --vg-* tokens mapped onto shadcn variables; no other file defines a color
layout.tsx html, ThemeProvider (dark by default), system font stacks
not-found.tsx
components/
ui/ shadcn components on Base UI, regenerated with the shadcn CLI
places/ places-map.tsx (MapLibre, client only) and its dynamic-import island
auth/ app/ events/ media/ forms, member nav, cards, the youtube-nocookie trailer embed
screen.tsx the Context / Action / Support layout every page uses
lib/
api.ts apiFetch, the one HTTP client for browser and server
api.server.ts forwards the cookie as a Bearer header for server components
session.ts cookie name and options, shared with the middleware
loaders.ts public-page loaders that never turn an API outage into a 500
middleware.ts redirects /app/* without a cookie to /login?next=
public/maplibre/ MapLibre worker files, copied on predev and prebuild, gitignored
scripts/ copy-maplibre-worker.mjs
tests/ smoke.spec.ts (Playwright)
next.config.ts CSP, HSTS, nosniff, X-Frame-Options, Referrer-Policy, Permissions-Policy
amplify.yml Node 24, env allowlist into .env.production, npm ci, next build

The routes page lists every path and what it serves. Amplify deploy covers amplify.yml and the headers.

Scripts​

npm run dev # next dev --turbopack
npm run validate # biome check, tsc --noEmit, next build
npm run test:e2e # playwright smoke against the dev server

Rendering rules​

Public pages are server components that call the API without a session and cache with revalidate. Pages under /app are server components that forward the cookie as a Bearer header for the first render, then hand interactive parts to client components that call the API through the same src/lib/api.ts in the browser. The map is the only page that is client-only from the top, because MapLibre needs window.