Prebit Developer Docs
Storefront Framework (Boron)

Tutorial: Build a Product Page + Cart

Server Component fetch, variant selection, and a progressively-enhanced add-to-cart form — end to end.

This walks through exactly what prebit-template-store's app/products/[slug]/page.tsx does, step by step.

1. Fetch on the server

// app/products/[slug]/page.tsx
import { getProduct, getProductMetadata, NotFoundError } from "@prebit/boron/server";
import { notFound } from "next/navigation";
import type { Metadata } from "next";

interface PageProps {
  params: Promise<{ slug: string }>;
}

async function loadProduct(slug: string) {
  return getProduct(slug).catch((err) => {
    if (err instanceof NotFoundError) return null;
    throw err;
  });
}

export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
  const { slug } = await params;
  const product = await loadProduct(slug);
  return product ? getProductMetadata(product) : {};
}

No hook, no client component needed just to read data — getProduct is a plain async function, called directly in a Server Component.

2. Render, with real SEO metadata already handled

export default async function ProductPage({ params }: PageProps) {
  const { slug } = await params;
  const product = await loadProduct(slug);
  if (!product) notFound();

  return (
    <Product product={product}>
      <Img src={product.imageUrl} alt={product.name} width={600} height={600} priority />
      <Money amount={product.discountedPrice || product.price} />
      {product.variants.length > 0 && <VariantSelector />}
    </Product>
  );
}

generateMetadata above already returned real title/description/ openGraph — nothing extra to wire up for SEO.

3. Add to cart, working with JS disabled

import { addToCartAction } from "@prebit/boron/actions";

<form
  action={async (formData: FormData) => {
    "use server";
    await addToCartAction(formData);
  }}
>
  <input type="hidden" name="productId" value={product.id} />
  <button type="submit">Add to cart</button>
</form>

This is a real HTML form submission under the hood — disable JavaScript in your browser's devtools and it still works, because the Server Action handles the mutation and Next.js re-renders the page with the result.

4. Show the cart count in the header

// app/_components/CartLink.tsx
"use client";
import { useCart } from "@prebit/boron/client";

export function CartLink() {
  const { cart } = useCart();
  return <a href="/cart">Cart ({cart.itemCount})</a>;
}

This is the one place JavaScript is required — a live cart badge needs client-side state. Everything else on this page works without it.

5. Recommendations, same pattern as the product itself

import { getProductRecommendations } from "@prebit/boron/server";

const recommended = await getProductRecommendations(slug, { limit: 4 });

Where to go from here

  • components/ for <Pagination>, <OptimisticCart>, and building your own ProductCard from the primitives.
  • server/ for cache tags and generateStaticParams() if you want SSG/ISR instead of the template's default force-dynamic rendering.
  • The full working version: prebit-template-store's own app/products/[slug]/page.tsx.

On this page