Adding a feature¶
Walk-through using a concrete example: adding the /about page.
1. Create the route¶
App Router convention — a folder matching the URL segment, containing page.tsx.
apps/web/app/about/page.tsx:
import { Header } from "@/components/layout/Header";
import { Footer } from "@/components/layout/Footer";
export const metadata = {
title: "About",
description: "Who Fruit Plug is and why we source exotic fruit.",
};
export default function AboutPage() {
return (
<>
<Header />
<main className="mx-auto max-w-3xl px-4 py-16 sm:px-6">
<h1 className="fp-display text-5xl">About Fruit Plug</h1>
<p className="mt-6 text-muted-foreground">
London's #1 exotic and tropical fruit supplier...
</p>
</main>
<Footer />
</>
);
}
Save the file. The browser at https://dev.fruitplug.co.uk/about works immediately — no rebuild.
2. Reuse existing components¶
Before writing new UI:
components/brand/Logo.tsx— the wordmarkcomponents/layout/Header.tsx— sticky top bar with nav + cartcomponents/layout/Footer.tsx— four-column footer
Import via @/ alias (configured in tsconfig.json):
3. Follow the brand¶
- Use tokens from
globals.css(see brand/guidelines) - Use Lucide outline icons with
strokeWidth={1.75} - Script font only for the display heading (
fp-displayclass)
4. Fetch data from Woo¶
For product/order/customer data, hit the Woo Store API from a Server Component:
async function getProducts() {
const res = await fetch(`${process.env.WP_STORE_API_URL}/products`, {
next: { revalidate: 300 }, // 5-min ISR
});
return res.json();
}
export default async function ShopPage() {
const products = await getProducts();
return <ProductGrid products={products} />;
}
For loyalty / custom-box / push endpoints, hit fruitplug-api:
const res = await fetch(`${process.env.WP_REST_URL}/fruitplug/v1/loyalty/me`, {
headers: { Cookie: cookies().toString() },
cache: "no-store",
});
5. Client interactivity¶
Only opt into "use client" when you need state, effects, or event handlers:
"use client";
import { useState } from "react";
export function AddToCartButton({ id }: { id: number }) {
const [adding, setAdding] = useState(false);
// ...
}
Keep the parent Server Component and pass down the client island as a child.
6. Test¶
# TypeScript
pnpm typecheck
# Lint
pnpm lint
# Production build (smoke-test standalone output)
pnpm build && pnpm start
7. Commit + deploy¶
The GitHub Action builds a new image, pushes it to GHCR, and SSH-deploys it to pwa-host. dev.fruitplug.co.uk will show the update in ~2 min.