Skip to content

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 in next.config.ts (*.wp.com, res.cloudinary.com, *.cdninstagram.com, fruitplug.co.uk).
  • No barrel imports — import directly from lucide-react by 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.