/vape/:slug or /420/:slug after SectionSlugResolver identifies the slug as a product (not a category). It is implemented in src/pages/ProductDetail.tsx and loaded via useProductBySlug().
Fetching a Product
getProductBySlug(slug, section) queries the products table with a full join on product_variants → product_variant_options → product_attribute_values → product_attributes.
The Product Interface
The complete TypeScript interface from src/types/product.ts:
Image Gallery
TheProductImages component (src/components/products/ProductImages.tsx) renders a gallery from product.images[].
cover_image
The primary image displayed on product cards, search results, and Open Graph meta tags. Falls back to
images[0] if null.images[]
Ordered array of gallery image URLs stored in Supabase Storage. The detail page renders them as a scrollable or paginated gallery.
Status Display
Thestatus field controls visual indicators on the detail page:
| Status | Badge | Add to Cart |
|---|---|---|
active | None (default) | Enabled |
legacy | ”Legacy” warning | Enabled (store allows it) |
discontinued | ”Discontinuado” | Blocked in cart.store.ts |
coming_soon | ”Próximamente” | Disabled |
Compare-at Price
Whencompare_at_price is set and greater than price, the page displays a strikethrough original price alongside the discounted current price:
Add-to-Cart Flow
Stock validation
Before rendering the “Add to Cart” button, the page checks
product.stock > 0. If stock === 0, a “Sin stock” state is shown.Status guard
cart.store.ts → addItem() rejects items with !product.is_active or product.status === 'discontinued' and returns early without mutation.Quantity clamping
updateQuantity(productId, qty) clamps the quantity to Math.min(quantity, item.product.stock) to prevent over-ordering.Cart sidebar opens
On successful
addItem(), openCart() is called from useCartStore to display <CartSidebar />.CartItem interface used in the cart:
Urgency Indicators
TheUrgencyIndicators component (src/components/products/UrgencyIndicators.tsx) renders urgency signals based on product data:
Low stock warning
Low stock warning
When
product.stock is between 1 and a threshold (typically 5), a “¡Solo X disponibles!” badge is shown to create purchase urgency.Promotional badges
Promotional badges
Badges from
product.badges[] are rendered as colored labels. Examples: "Nuevo", "Top Seller", "Oferta". These are populated by the admin panel or the product-intelligence AI edge function via ai_sales_note.Featured / Bestseller / New flags
Featured / Bestseller / New flags
is_featured, is_new, and is_bestseller each render dedicated ribbon or badge overlays. Expiry timestamps (is_featured_until, etc.) are displayed to admins but not to customers.SEO: Dynamic Meta Tags
The product detail page usesreact-helmet-async (via the <SEO /> component at src/components/seo/SEO.tsx) to inject per-product meta tags:
cover_image is used for social sharing previews. If null, the component falls back to images[0]. Ensure products have a cover_image set for optimal OG rendering.Smart Recommendations
Below the main product content,getSmartRecommendations(product, limit) from products.service.ts fetches related products based on category compatibility rules defined in src/lib/upsell-logic.ts. If no compatibility rules exist for the product’s category, it falls back to same-category products.