cimplify
TypeScript SDK

Rich product pages

Render rich product description pages from sanitized HTML, compose multi-column layouts with the cimplify-* helpers, and generate bespoke pages per slug, including with AI.

<ProductPage productId={slug} /> fetches a product and renders a complete, responsive detail page. It picks the right layout automatically and wires up variants, scheduling, and a live-priced cart. It also renders the product's description as sanitized rich HTML (not plain text): headings, lists, tables, figures, quotes, code, collapsible sections and host-locked video embeds all display correctly. The same renderer (<RichText>) is exported for use anywhere.

Built-in layouts & resolution

You don't choose a layout; <ProductPage> resolves one from the product's shape. Resolution runs in priority order:

  1. Per-slug pages: a custom component mapped to a product's slug. Highest priority.
  2. Per-type templates: your override for a template key (see below).
  3. Built-in layout: one of seven shipped layouts, inferred from the product.
  4. DefaultProductLayout: the fallback.

resolveTemplateKey() picks the built-in layout:

LayoutChosen when
Bundleproduct.type === "bundle"
Compositeproduct.type === "composite"
Wholesalequantity_pricing.length > 1
Serviceproduct.type === "service"
Digitalproduct.type === "digital"
Foodrender_hint === "food"
Defaulteverything else (physical / fallback)

To force a layout from data, set metadata.page_template on the product (e.g. "wholesale"); it wins over the inferred type.

Override a whole product type

Swap the built-in layout for every product of a kind with templates:

app/products/[slug]/page.tsx
import { ProductPage, ProductTemplate } from "@cimplify/sdk/react";
import { MyServicePage } from "@/product-pages/service";

<ProductPage
  productId={slug}
  templates={{ [ProductTemplate.Service]: MyServicePage }}
/>;

Both pages and templates components receive ProductLayoutProps (product, onAddToCart, renderImage, relatedProducts, …).

The shared customizer

Every built-in layout renders <ProductCustomizer>, the purchase engine. From the product's shape it renders the variant selector (text options, or color/image swatches when the axis values carry color_hex/image_url), add-ons, billing plans, volume-pricing tiers, service scheduling (date + time slots grouped by time of day with capacity-aware availability, a staff picker from the slot's available staff, and a live <BookingSummary> that splits deposit-due from the balance), customer input fields, quantity, and a live-priced Add to Cart that quotes through the API. Wrap your store in <CartDrawerProvider openOnAdd> so adds open a slide-in <CartDrawer>, or use <ProductSheet> for an in-place quick view.

Responsive

Layouts are responsive out of the box: a single stacked column on phones and tablets, two columns (media / details) at lg and up. Swatches, slot grids, and the image gallery reflow with the viewport. For a mobile quick-buy, render <ProductSheet> inside your own bottom sheet; <CartDrawer> handles the add-to-cart confirmation.

A PDP is SEO-critical, so render it on the server and pre-build every slug. Don't fetch it in a client component. <ProductPage> is built for this: pass a server-fetched product to the product prop and it skips the client fetch entirely.

The canonical Server Component (this is what cimplify init scaffolds):

app/products/[slug]/page.tsx
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { ProductPage } from "@cimplify/sdk/react";
import { getServerClient, tags } from "@cimplify/sdk/server";
import { WirelessEarbudsPage } from "@/product-pages/wireless-earbuds";

// Pre-enumerate every slug at build time so the route emits cacheable
// (s-maxage) responses instead of being treated as fully dynamic.
export async function generateStaticParams() {
  const r = await getServerClient().catalogue.getProducts({ limit: 10_000 });
  return r.ok ? r.value.items.map((p) => ({ slug: p.slug ?? p.id })) : [];
}

export const revalidate = 3600; // ISR: re-validate hourly (and on webhook tag)

async function load(slug: string) {
  return getServerClient().catalogue.getProductBySlug(slug, {
    cacheOptions: { revalidate: 3600, tags: [tags.product(slug)] },
  });
}

export async function generateMetadata(
  { params }: { params: Promise<{ slug: string }> },
): Promise<Metadata> {
  const { slug } = await params;
  const r = await load(slug);
  if (!r.ok) return {};
  return { title: r.value.name, description: r.value.description ?? undefined };
}

export default async function Page(
  { params }: { params: Promise<{ slug: string }> },
) {
  const { slug } = await params;
  const r = await load(slug);
  if (!r.ok) {
    if (r.error.code === "NOT_FOUND") notFound();
    throw new Error(r.error.message); // transient; ISR retries next request
  }

  return (
    <ProductPage
      product={r.value}
      pages={{ "wireless-earbuds": WirelessEarbudsPage }}
    />
  );
}

generateStaticParams plus export const revalidate give you static-at-build, revalidated-on-demand pages. generateMetadata and a <script type="application/ld+json"> Product block (see the scaffolded template) cover SEO. Pair with tag-based revalidation so a deploy or price change invalidates the exact slug. On Cloudflare Workers, stay on ISR: don't enable cacheComponents / 'use cache', and use the SDK's cacheOptions.

Note: The client form, <ProductPage productId={slug} /> inside a "use client" component with useParams, is a quick start only. It renders nothing on the server and ships no metadata, so reach for it only in app-shell contexts that can't server-render.

Rich descriptions

Beyond the layout, the product's description is rendered as rich HTML. There are two ways to produce that content, depending on whether it's data (changes without a deploy) or code (a bespoke layout):

  1. description HTML: stored on the product, rendered by <RichText>. Edit it and it's live instantly. Covers the large majority of rich product content.
  2. Per-slug component (pages): a custom React layout for a specific product. Maximum control, but it ships with your storefront build.

What description HTML supports

Descriptions are sanitized on every render (on the Cloudflare Workers edge during SSR, and again in the browser), so untrusted markup is safe. The allowlist covers:

  • Text: p, h1h6, strong/b, em/i, u, s/del, mark, sub, sup, code
  • Lists: ul, ol, li, and definition lists dl/dt/dd
  • Tables: table, thead, tbody, tr, th, td, caption (ideal for spec sheets and comparison charts)
  • Media: img, figure/figcaption, and host-locked iframe (YouTube / Vimeo only)
  • Structure: blockquote, pre, hr, details/summary, div, section
  • Inline CSS is filtered to a safe property allowlist (text-align, color, sizing). position, z-index, url(), event handlers, <script> and javascript: URLs are stripped.

<th>/<td> get bordered, theme-aware table styling; iframes render responsively (aspect-video); figures get captions, all wired to your storefront's design tokens.

Layout helpers for multi-column modules

Tailwind classes you put inside a description string won't be styled: the storefront's Tailwind build only scans its own source, never your runtime content. To build image-and-text modules, grids and banners from description HTML, use the namespaced cimplify-* classes shipped in @cimplify/sdk/styles.css (always present, no purge):

ClassEffect
cimplify-cols-2 / -3 / -4Responsive grid (1 col → 2 → N)
cimplify-mediaTwo-column image + text module
cimplify-media cimplify-media--reverseSame, image on the right
cimplify-cardBordered, padded card for grid items
cimplify-banner + cimplify-banner__contentImage with overlaid text

Make sure your globals.css imports the SDK stylesheet:

app/globals.css
@import "@cimplify/sdk/styles.css";

An image-and-text module plus a feature grid, authored as plain HTML in the description:

<div class="cimplify-media">
  <img src="https://cdn.example.com/hero.jpg" alt="In use outdoors" />
  <div>
    <h3>Built for the trail</h3>
    <p>IP68 dust and water resistance, 40-hour battery, and a titanium frame.</p>
  </div>
</div>

<div class="cimplify-cols-3">
  <div class="cimplify-card"><h4>40h battery</h4><p>All-week endurance.</p></div>
  <div class="cimplify-card"><h4>IP68</h4><p>Dust and water sealed.</p></div>
  <div class="cimplify-card"><h4>Titanium</h4><p>Lighter, stronger.</p></div>
</div>

A comparison chart is just a table:

<table>
  <thead><tr><th>Feature</th><th>Pro</th><th>Standard</th></tr></thead>
  <tbody>
    <tr><td>Battery</td><td>40h</td><td>24h</td></tr>
    <tr><td>Water resistance</td><td>IP68</td><td>IP54</td></tr>
  </tbody>
</table>

Using <RichText> directly

components/article.tsx
import { RichText } from "@cimplify/sdk/react";

export function Article({ html }: { html: string }) {
  return <RichText html={html} className="text-base" />;
}

sanitizeRichTextHtml(html) is also exported if you need the cleaned string without rendering.

Bespoke pages per slug (and AI generation)

For flagship products that need a pixel-perfect, layout-heavy page, pass a per-slug component via pages. It takes top priority over every built-in layout, and it's just another prop on the server-rendered <ProductPage> shown above:

<ProductPage
  product={product}
  pages={{ "wireless-earbuds": WirelessEarbudsPage }}
/>

Each page component receives ProductLayoutProps (product, onAddToCart, renderImage, relatedProducts, and more), so it has the full product and cart wiring. Because the product is fetched on the server, the bespoke page server-renders too.

Which path for AI?

  • Generate description HTML when content should update without a redeploy and at scale across the catalogue. The AI emits the HTML and cimplify-* classes above; it's stored on the product and rendered safely. This is the default target.
  • Generate a per-slug component only for a handful of hero products that need a custom layout. The output is .tsx that ships with the storefront build.

Both are first-class. Most catalogues are best served almost entirely by generated description HTML, reserving bespoke components for the few pages that truly need them.

On this page