@aimee.shop/blocks
v0.1.1
Published
React component library for rendering Aimee CMS block content on storefronts
Maintainers
Readme
@aimee.shop/blocks
React component library for rendering Aimee CMS block content on storefronts.
This package ships 24 read-only block renderers (including composition types). It is a convenience layer over raw SDK data -- storefronts can also render blocks manually.
Install
npm install @aimee.shop/blocksPeer dependencies: react >= 18, react-dom >= 18. @aimee.shop/sdk is an optional
peer dependency -- this package never imports it; all data arrives via props.
Catalog
A Storybook catalog (dev-only, not published) renders every BLOCK_DEFS type through
BlockRenderer with fixture content:
npm run storybook
npm run build-storybookTheme tokens (--aimee-heading-color, --aimee-cta-bg, --aimee-font-family) are toolbar
globals. Viewports: mobile 375 and desktop 1280. The catalog does not call a live
assistant.
Usage
Import the stylesheet once (for example in a Next.js root layout):
import '@aimee.shop/blocks/styles.css';Fetch content with the SDK and pass blocks to the renderer:
import { BlockRenderer } from '@aimee.shop/blocks';
// In an async Server Component
const entry = await sdk.cms.public.getContent('blog', slug);
return <BlockRenderer blocks={entry.blocks} />;Block data shape (mirrors the CMS API):
interface Block {
id: string;
type: string; // one of 24 known types; unknown types render as JSON via CustomBlock
sortOrder: number;
content: Record<string, unknown>;
settings?: Record<string, unknown>;
}Block types
text, heading, image, gallery, video, embed, cta, product_card, product_grid, quote, code, divider, spacer, accordion, tabs, custom, form_embed, hero, promo_banner, card_grid, comparison_table, video_lightbox, columns, container, chat_widget.
Content for each type is validated at render time by exported parsers
(parseHeadingContent, parseGalleryContent, ...). Invalid or empty content renders
nothing; recoverable fields get defaults (heading level 2, grid columns 3, etc.).
Unknown block types fall back to CustomBlock, which renders the raw content as JSON.
Composition (columns / container)
columns and container hold nested Block[] inside their content JSON. BlockRenderer
recurses into children (same parse/dispatch path, products and overrides flow down).
Limits:
- max nesting depth 3 (
MAX_BLOCK_DEPTH); deeper composition blocks degrade toCustomBlock - only
columns/containerhold children columns.lengthcapped at 6- column
widthis'auto'or a positive relative weight (CSSfr)
Notes on specific types
hero: headline (h1 by default, demotable to h2 viaheadlineLevel), optional subheadline, background image and/or video (video wins, image becomes the poster and theprefers-reduced-motionfallback), contrast overlay (overlayOpacity, 0-1), up to 2 CTAs,heightsmall|medium|large|full.promo_banner: text with optional link. First block type with typed settings:dismissible,startsAt/endsAt(ISO 8601 schedule window),storageKey(localStorage key for dismissal; falls back to the block id). Pages cached by ISR/static rendering re-evaluate the window client-side at hydration, but keep revalidation intervals shorter than your window edges.card_grid: cards with title, plain-text body, image/icon, optional link (single anchor, CSS-expanded hit area). OptionallinkTextmakes that anchor a visible underlined CTA under the body; optionalshowTitle: falsehides the heading (title stays required and is the image alt fallback).comparison_table: semantic table with column/row headers, optional caption andhighlightColumn(0-based), horizontal scroll wrapper that is keyboard focusable.video: YouTube and Vimeo page URLs are rewritten to their embed form. Allowlisted hosted players (videos.sproutvideo.com) andprovider: 'hosted'pass thehttp(s)URL through as the iframe src. Unrecognized URLs render a visible error andconsole.warn-- never a silent link. Raw media files are not hosted players; useherobackground video orembedfor those.video_lightbox: thumbnail button opening a modal embed (same URL rules asvideo) with autoplay. Unrecognized URLs keep the thumbnail and show the same visible error.columns: multi-column layout; each column haswidthand nestedblocks.container: grouped section with nestedblocksand optionalstyledefault|emphasis|accent. Presentation fields (all optional):backgroundColor/textColor(#RGBor#RRGGBB),backgroundImage/mobileSrc(http(s),mobileSrcvia<picture>),paddingYnone|sm|md|lg,minHeightnone|md|lg. Typed settings:alignstart|center|end,widthcontained|full,cssClass,htmlId.width=fullbleeds the outer surface and constrains.aimee-container__inner.
Block settings
settings mirrors content: raw Record<string, unknown> narrowed by runtime
parsers, never cast. Block types with typed settings appear in BlockSettingsMap
(promo_banner -> PromoBannerBlockSettings, container -> ContainerBlockSettings);
for every other type the raw settings record passes through unchanged. Settings may
carry extra keys (for example admin variant/personalization metadata); parsers read
what they know and ignore the rest. Components rendered through BlockRenderer also
receive blockId -- the CMS block id -- for per-block client state such as promo
banner dismissal.
Server and client components
Components are React Server Component compatible by default. Exactly six blocks are
client components ('use client'): AccordionBlock, TabsBlock, GalleryBlock,
PromoBannerBlock, VideoLightboxBlock, ChatWidgetBlock. Composition blocks (columns, container) are
server components. No renderer is async and none fetches data -- everything renders
from props.
Product blocks and the products map
product_card and product_grid content contains only product IDs. The CMS API does
not resolve products; the storefront fetches them and passes a map:
const products = {
[productId]: { id, name, slug, price, compareAtPrice, currency, images },
};
<BlockRenderer blocks={entry.blocks} products={products} />;When a product is missing from the map (or no map is passed), the card renders a visible "Product unavailable" placeholder instead of disappearing silently.
Chat widget and the assistant session
chat_widget renders the shopper assistant (ai-toolkit POST /v1/ai/shopper/chat)
as a floating drawer (mode: "drawer", launcher button bottom-right or settings.position:
"left") or an inline panel (mode: "inline"). Content: title, greeting, placeholder,
launcherLabel, suggestions (max 4 quick prompts). Settings: position, pageType.
The session is storefront data, not CMS content, so it is passed to BlockRenderer as
assistant (the same way products is):
<BlockRenderer
blocks={entry.blocks}
assistant={{
endpoint: 'https://ai-dev.aimee.shop',
publishableKey: process.env.NEXT_PUBLIC_AIMEE_PUBLISHABLE_KEY,
customerToken: session?.accessToken, // sent as X-Customer-Token
cartId: cart?.id,
page: { pageType: 'product', productId: product.id },
onCartUpdate: (cart) => adoptCart(cart.cartId),
onCheckout: (url) => router.push(url), // default: window.location.assign(url)
}}
/>The widget makes no SDK calls. Every commerce action goes through the assistant; the
storefront only bootstraps the session (key, cart id, customer token) and adopts the
cart id announced on onCartUpdate (a guest cart is created server-side on the first
write). Without assistant, an inline block shows "Chat is unavailable" and a drawer
renders nothing.
Stream payloads render as cards: product search results and details, the cart
(cart_update also replaces the running cart), orders, and the checkout_handoff
link to the hosted checkout URL. Gate refusals (status: "held"), tool errors, and
warning events show as a muted notice line. The assistant only ever streams text and these events; the
widget round-trips the text history plus conversationId (X-Session-Id).
For a custom UI, streamShopperChat, parseSseChunk, readShopperEvent,
shopperChatReducer, and the card components are exported.
Overriding block components
import type { BlockComponentProps, HeadingBlockContent } from '@aimee.shop/blocks';
function MyHeading({ content }: BlockComponentProps<HeadingBlockContent>) {
return <h2 className="my-heading">{content.text}</h2>;
}
<BlockRenderer blocks={blocks} overrides={{ heading: MyHeading }} />;Overrides are typed per block type: the heading override receives parsed
HeadingBlockContent, not raw JSON.
Theming
All styling uses CSS custom properties with the --aimee- prefix. Package defaults follow Aimee (Blue CTA #0066F7,
Ink headings #050A1C). Load Quicksand yourself if you want the heading stack to render;
this package only sets the family name. Merchant themeSettings overrides stay
the same. Defaults ship inside @layer aimee-blocks, so unlayered rules in
your stylesheet always win -- no :root:root or !important needed:
:root {
--aimee-font-family: 'Inter', sans-serif;
--aimee-font-heading: 'Playfair Display', serif;
--aimee-heading-color: #111;
--aimee-cta-bg: #0066f7;
--aimee-cta-color: #fff;
--aimee-spacing-md: 2rem;
--aimee-border-radius: 0.25rem;
/* Gradient text: set BOTH tokens or text becomes invisible */
--aimee-headline-gradient: linear-gradient(90deg, #0066f7, #fb74c6);
--aimee-headline-text-fill: transparent;
}Full token list:
- Typography:
--aimee-font-family,--aimee-font-heading,--aimee-font-size-base,--aimee-line-height-base - Headline gradient:
--aimee-headline-gradient,--aimee-headline-text-fill(pair; default no-op) - Colors:
--aimee-heading-color,--aimee-text-color,--aimee-link-color,--aimee-border-color,--aimee-bg-muted - CTA:
--aimee-cta-bg,--aimee-cta-color,--aimee-cta-hover-bg,--aimee-cta-hover-color,--aimee-cta-border-color,--aimee-cta-radius,--aimee-cta-padding,--aimee-cta-font-weight,--aimee-cta-text-transform - Hero CTA (defaults to matching
--aimee-cta-*):--aimee-hero-cta-bg,--aimee-hero-cta-color,--aimee-hero-cta-hover-bg,--aimee-hero-cta-hover-color,--aimee-hero-cta-border-color,--aimee-hero-cta-radius,--aimee-hero-cta-padding,--aimee-hero-cta-font-weight,--aimee-hero-cta-text-transform - Spacing:
--aimee-spacing-xs,--aimee-spacing-sm,--aimee-spacing-md,--aimee-spacing-lg,--aimee-spacing-xl - Layout:
--aimee-max-width,--aimee-border-radius - Code:
--aimee-code-bg,--aimee-code-border,--aimee-code-font - Quote:
--aimee-quote-border-color,--aimee-quote-bg - Overlay:
--aimee-overlay-bg - Product card:
--aimee-product-card-shadow,--aimee-product-card-hover-shadow - Hero:
--aimee-hero-min-height-small|medium|large,--aimee-hero-overlay-color,--aimee-hero-text-color,--aimee-hero-bg - Promo banner:
--aimee-promo-banner-bg,--aimee-promo-banner-color - Card grid:
--aimee-card-grid-bg,--aimee-card-grid-shadow,--aimee-card-grid-hover-shadow,--aimee-card-grid-title-size(1.125rem),--aimee-card-grid-body-size(0.9375rem) - Comparison table:
--aimee-table-header-bg,--aimee-table-highlight-bg - Video lightbox:
--aimee-play-button-bg,--aimee-play-button-color
Hero content also supports optional eyebrow (plain text above the headline).
All classes are BEM with the aimee- namespace (.aimee-heading, .aimee-product-card__price).
Security
HTML content (TextBlock, EmbedBlock) is sanitized with sanitize-html before rendering (CJS-safe on Vercel Node; no jsdom). EmbedBlock allows iframes through sanitization; VideoBlock, FormEmbedBlock, VideoLightboxBlock, and hero background media only render http(s) URLs. Card grid, comparison table, hero, and promo banner content is plain text -- no new HTML sanitization surface.
