Frontend — Next.js 16 PWA¶
Stack¶
| Layer | Choice | Version |
|---|---|---|
| Framework | Next.js (App Router) | 16.2.4 |
| React | 19.2.4 | |
| Styling | Tailwind CSS v4 (@tailwindcss/postcss) |
^4 |
| Icons | lucide-react (outline, strokeWidth={1.75}) |
latest |
| Body font | Montserrat (Google Fonts) | 400–700 |
| Display font | Caveat Brush (or self-hosted Grit if uploaded) | 400 |
| State — cart | Zustand | ^5 |
| State — server | React async Server Components + fetch cache |
— |
| HTTP | Native fetch (+ our typed @fruitplug/woo-client) |
— |
| PWA | public/sw.js + manifest.webmanifest |
minimal |
| Runtime (prod) | Node 22 Alpine in Docker (standalone output) | — |
| Runtime (dev) | next dev --webpack in Docker with 800 ms polling |
— |
Repo layout¶
fruitplug-web/
├── apps/
│ └── web/
│ ├── app/ App Router routes
│ │ ├── layout.tsx Fonts · metadata · <AppSplash> · <InstallPrompt> · SW register
│ │ ├── page.tsx Home (hero · stats · feature cards)
│ │ ├── globals.css Brand tokens · Tailwind @theme · .fp-display · .fp-stripe
│ │ ├── api/cart/ Route Handlers proxying to Woo Store API
│ │ ├── api/box/ Route Handlers proxying to fruitplug-api (price, save)
│ │ ├── healthz/route.ts Liveness JSON
│ │ ├── shop/ /shop + /shop/[category]
│ │ ├── p/[slug]/ Product detail pages
│ │ ├── cart/ Full cart page (SSR + client)
│ │ ├── build-your-box/ Custom Box Builder (/ and /[slug])
│ │ └── … placeholder pages
│ ├── components/
│ │ ├── brand/Logo.tsx
│ │ ├── layout/ Header · Footer · PlaceholderPage
│ │ ├── product/ ProductCard · ProductGrid · ProductActions
│ │ ├── cart/ CartView · CartCount
│ │ ├── box-builder/ BoxBuilder (client component)
│ │ └── pwa/ InstallPrompt · AppSplash · ServiceWorkerRegister
│ ├── lib/
│ │ ├── woo-proxy.ts Cart-Token + Nonce session handling
│ │ ├── fruitplug-api.ts Server-side proxy for our custom WP plugin
│ │ └── box-templates.ts Seed configs for the box builder
│ ├── stores/cart.ts Zustand store
│ ├── public/
│ │ ├── brand/ logo.png · mascot.png · favicon-192.png · avatar.jpg
│ │ ├── icons/ Generated PWA icons (7 files)
│ │ ├── fonts/ (local Grit goes here if uploaded)
│ │ ├── sw.js Minimal service worker
│ │ └── manifest.webmanifest
│ ├── scripts/
│ │ ├── generate-icons.mjs Logo → icon set via sharp
│ │ ├── generate-mascot.mjs Chroma-key + trim → transparent mascot
│ │ └── inspect-sources.mjs Dev helper
│ ├── Dockerfile Multi-stage prod build
│ ├── next.config.ts standalone · outputFileTracingRoot · webpack polling
│ └── package.json
├── packages/
│ └── woo-client/ Typed Woo Store API client (`@fruitplug/woo-client`)
├── infra/ Dockerfiles · Caddy snippet · systemd unit · A2 tooling
├── wp-plugin/
│ └── fruitplug-api/ PHP plugin (ships to A2)
└── docs/ This wiki
Routes live today¶
| Route | Type | What |
|---|---|---|
/ |
SSR | Home — hero with wordmark, stats band, feature cards |
/shop |
SSR + ISR 5min | All 46 products, category chips |
/shop/[category] |
SSR + ISR | Filtered by category (fruits, boxes, subscription, merchandise, rare-special) |
/p/[slug] |
SSR + ISR | Product detail with gallery, variation chips, Add-to-Cart, related |
/cart |
SSR + client | Real Woo cart; quantity + remove |
/build-your-box |
Static | Overview: pick Tropical / Exotic / Plug-In |
/build-your-box/[slug] |
SSR + client | Custom Box Builder with sections + credit budget |
/api/cart, /api/cart/add, /api/cart/update, /api/cart/remove |
Route Handler | Cart ops proxied to Woo |
/healthz |
Route Handler | JSON liveness |
/cart, /account, /subscribe, /recipes, /about, /faq, /privacy, /checkout |
Placeholder | Phase 1 scope markers |
Conventions¶
- Server Components by default. Only opt into
"use client"when you need state, effects, or events. - Brand tokens from
globals.css— use Tailwind (bg-primary,text-muted-foreground) or raw CSS vars (var(--fp-magenta)). - Icons are Lucide, stroke 1.75, size via Tailwind
h-4 w-4/h-5 w-5. No emoji in UI. - Images via
next/image— remote hosts allowed innext.config.ts(*.wp.com,res.cloudinary.com,*.cdninstagram.com,fruitplug.co.uk). - No barrel imports — import directly from
lucide-reactby name for tree-shaking. - Fetch + tags — server-side reads use
next: { tags: [...], revalidate: 300 }so we can revalidate on webhook events (e.g. product updated in wp-admin).
Typed Woo client¶
packages/woo-client wraps the Store API in typed functions:
import { getProducts, getProductBySlug, addToCart } from "@fruitplug/woo-client";
const products = await getProducts({ category: "fruits", per_page: 100 });
const p = await getProductBySlug("mangosteen");
// ...
Environment: the client reads WP_STORE_API_URL on the server; in the browser, everything goes through our /api/cart/* routes instead. See lib/woo-proxy.ts.
What's next¶
See the Changelog "Deferred / planned" section — Stripe checkout, JWT auth wiring, PWA push notifications, Instagram reel embedding, loyalty UI surfacing.