Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/dlampatricio/florale/llms.txt

Use this file to discover all available pages before exploring further.

The product detail page at /producto/[id] is a dynamic Next.js server component that renders a full-page view of a single artisanal product. It resolves the product ID from the URL, fetches the product alongside all other products and categories in parallel, computes a related-products list from the same category, and delegates rendering to the ProductDetailClient component. If no product matches the requested ID, the page calls notFound() to show Next.js’s built-in 404 screen.

Route structure

The file lives at app/(app)/producto/[id]/page.tsx. The [id] segment is a UUID that maps directly to the id column in the Supabase products table. Because the params object is a Promise in Next.js 15’s async params API, the page awaits it before using the value.
// app/(app)/producto/[id]/page.tsx
export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;

  const [product, allProducts, allCategories] = await Promise.all([
    getProductById(id),
    getProducts(),
    getCategories(),
  ]);

  if (!product) notFound();

  const relatedProducts = allProducts.filter(
    (p) => p.categoryId === product.categoryId && p.id !== product.id
  );
  const category = allCategories.find((c) => c.id === product.categoryId);

  return (
    <ProductDetailClient
      product={product}
      relatedProducts={relatedProducts}
      category={category ?? null}
    />
  );
}

Parallel data fetching

Three Supabase requests fire concurrently so none of them blocks the others:
CallPurpose
getProductById(id)Primary product data for the detail view
getProducts()Full product list used to derive related products
getCategories()Category list used to find the current product’s category label
getProductById is the only fetch that directly uses the URL parameter. The other two calls return stable, ordered lists that are filtered server-side — no additional Supabase query is needed for related products.

generateMetadata for SEO

generateMetadata is an exported async function co-located with the page. Next.js calls it before rendering to inject <title>, <meta description>, and social-graph tags into the document <head>. It also awaits params and calls getProductById. Because all fetches in lib/products.ts use cache: 'no-store', each call goes directly to the Supabase REST API without caching.
import type { Metadata } from 'next';

export async function generateMetadata({
  params,
}: {
  params: Promise<{ id: string }>;
}): Promise<Metadata> {
  const { id } = await params;
  const product = await getProductById(id);

  if (!product) {
    return { title: 'Producto no encontrado — Florale' };
  }

  return {
    title: `${product.name} — Florale`,
    description: product.description || 'Producto artesanal único en Florale.',
    openGraph: {
      title: `${product.name} — Florale`,
      description: product.description || 'Producto artesanal único en Florale.',
      images: product.image
        ? [{ url: product.image, width: 1200, height: 630 }]
        : [],
    },
    twitter: {
      card: 'summary_large_image',
      title: `${product.name} — Florale`,
      description: product.description || 'Producto artesanal único en Florale.',
      images: product.image ? [product.image] : [],
    },
  };
}

Metadata fields produced

<title>

{product.name} — Florale. Falls back to "Producto no encontrado — Florale" if the ID is invalid.

<meta description>

product.description if present; otherwise the default string "Producto artesanal único en Florale.".

Open Graph image

The product’s Supabase Storage URL, declared at 1200 × 630 px. Omitted if product.image is empty.

Twitter card

summary_large_image card type, same title, description, and image as Open Graph.

Handling missing products

When getProductById returns undefined (the product doesn’t exist, or its ID is malformed), the page calls Next.js’s notFound() helper, which immediately stops rendering and surfaces the nearest not-found.tsx boundary — or the framework’s default 404 page if none is defined.
if (!product) notFound();
generateMetadata also short-circuits gracefully: it returns only { title: 'Producto no encontrado — Florale' } so search engines never index a broken metadata frame.
notFound() throws internally — it does not return. Any code written after the if (!product) notFound() line is treated by TypeScript as unreachable, which is why product is narrowed to Product (non-undefined) for the rest of the component without an extra type assertion.
Related products are products that share the same categoryId as the current product but have a different id. The filtering is done in JavaScript after getProducts() returns, so no extra database query is needed.
const relatedProducts = allProducts.filter(
  (p) => p.categoryId === product.categoryId && p.id !== product.id
);
ProductDetailClient receives this array and renders up to six items in a 2-column (mobile) / 3-column (desktop) grid beneath the main product detail — labeled “También te puede gustar” (“You might also like”). Each related card is a plain <Link> to /producto/{rp.id} with the same square image crop used in the catalog.

ProductDetailClient component

ProductDetailClient is the 'use client' rendering boundary. It handles all interactive state that cannot live in a server component:
1

Add to cart

The Añadir al carrito button calls addItem(product.id) from useCartStore and fires a toast notification. The button switches to a green Agregado confirmation state after the first click (tracked by local useState).
2

Go to cart shortcut

If itemCount > 0, a secondary Ir al carrito button appears below the primary CTA, linking directly to /carrito.
3

Share product

The share button calls navigator.share() on supported devices. On desktop it falls back to copying the URL to the clipboard and shows a brief ✓ Copied state.
4

Image lightbox

Clicking the product image opens a full-screen lightbox overlay (Framer Motion AnimatePresence). Press Escape or click the backdrop to close it.

Complete data flow

// Types used across the page
interface Product {
  id: string;
  name: string;
  description: string;
  price: number;
  image: string;       // Supabase Storage public URL
  categoryId: string;
}

interface Category {
  id: string;
  name: string;
  description: string;
}
1

Next.js resolves the dynamic segment

The URL /producto/abc-123 makes params resolve to { id: 'abc-123' }.
2

generateMetadata runs first

Fetches getProductById('abc-123') and returns the <head> metadata before rendering.
3

Page component fetches in parallel

getProductById, getProducts, and getCategories all fire concurrently via Promise.all.
4

notFound() or render

If the product is missing, notFound() halts rendering. Otherwise, related products are filtered and passed to ProductDetailClient.
5

Client hydration

ProductDetailClient hydrates in the browser, connects to the Zustand cart store, and activates the lightbox, share, and add-to-cart interactions.
For the Supabase query details and environment variable configuration, see Data Fetching.

Build docs developers (and LLMs) love