docs
    commerce-kit: Commerce
    v1.0.1.md

    Commerce: @sonordev/commerce-kit/commerce

    Products, services, classes, events and checkout flows, all managed from the Sonor dashboard.

    Requires site-kit 8 or newer. Coming from site-kit 7, where this was @sonordev/site-kit/commerce? The import path changed and little else: see Moving from site-kit 7.

    Checkout runs on the payment processor you connect to the project in Sonor, Square or Stripe. EventCheckout, the inline ticket checkout, needs Square.

    Components

    ComponentPurpose
    OfferingCardCard for any offering type: image, title, price and a call to action. Variants: card, horizontal, minimal and featured
    OfferingListGrid or list of offerings, filtered by type, category or status, with optional category filters
    ProductPageA whole product page for a [slug] route: breadcrumbs, ProductDetail, related products and a back link
    ProductDetailThe product itself: gallery, variants, quantity, size chart, features, specifications, trust badges and inline checkout
    ProductGrid / ProductEmbedProduct showcase widgets: a filterable, sortable grid, or one product for a homepage (a specific slug, or the latest)
    SizeChartClothing size chart display
    EventTile / UpcomingEventsAn event tile with date, title and a registration button, and a list of upcoming events built from it
    EventCalendar / CalendarView / EventModalA month calendar with a built-in registration modal, the calendar alone, and the modal alone
    EventEmbed / EventsWidgetOne event for a homepage (a specific slug, or the next one), and a full widget with list, grid and calendar views and booking built in
    EventsAgendaA month-grouped event list. It has no hooks or browser APIs, so it renders in full on the server
    EventCheckoutInline checkout for an event with ticket tiers, paid by card in the page with Square
    CheckoutFormCheckout or registration form for a product or event, with shipping rates when the project ships
    RegistrationFormRegistration for free events and classes
    CapacityBadge / EventJsonLdA seat-count badge, and schema.org Event markup from an offering you already fetched

    useEventModal() manages the state for opening EventModal: it returns { event, schedule, isOpen, openModal, closeModal }.

    Usage

    The commerce entry is a client entry. A page that imports these components stays a server component; the components load their data in the browser, using the credential SiteKitLayout published.

    // app/shop/page.tsx
    import { OfferingList } from '@sonordev/commerce-kit/commerce'
    
    export default function ShopPage() {
      return <OfferingList type="product" />
    }
    // app/shop/[slug]/page.tsx, a single product page
    import { ProductPage } from '@sonordev/commerce-kit/commerce'
    
    export default async function Product({ params }: { params: Promise<{ slug: string }> }) {
      const { slug } = await params
      return <ProductPage slug={slug} />
    }
    // app/events/page.tsx, a calendar with booking built in
    import { EventsWidget } from '@sonordev/commerce-kit/commerce'
    
    export default function EventsPage() {
      return <EventsWidget title="Upcoming events" defaultView="calendar" />
    }

    Event tickets

    EventCheckout takes an event offering that has its schedules, such as one from fetchOffering(slug):

    import { EventCheckout } from '@sonordev/commerce-kit/commerce'
    
    <EventCheckout event={event} />

    It shows one row for each ticket tier (a single ticket row when the event has no tiers, priced from the offering), collects the buyer's details and takes the card in the page with Square, in one charge for the total.

    PropWhat it does
    eventThe event offering, with variants for tiers or a base price for a single ticket. Required
    scheduleThe date to book. Defaults to the event's first upcoming schedule
    collectPhoneAsk for a phone number. Default true
    maxPerVariantThe most tickets a buyer can pick in one tier. Default 10
    initialQuantitiesStart with tiers already picked, as { [variantId]: quantity }, so a tier card or a deep link can open checkout with that tier selected. Values are held between 0 and maxPerVariant, unknown ids are ignored, and it only sets the starting state: remount the checkout (for example with key={variantId}) to change it
    cardLabelThe card section's label. Default Card Details
    submitTextThe button text. Default Buy and the total
    onSuccess / onErrorCalled with the CheckoutResult after a charge, or with an error message
    classNameClass for the wrapper

    Render on the server

    @sonordev/commerce-kit/commerce/server loads commerce data in server components, route handlers, generateStaticParams and generateMetadata. It's server only. Every fetcher takes a config as its first argument, and {} is a complete one: the helpers read SONOR_API_KEY, and SONOR_API_URL when you set it, like the rest of site-kit's server code.

    // app/shop/[slug]/page.tsx
    import { notFound } from 'next/navigation'
    import { ProductPage } from '@sonordev/commerce-kit/commerce'
    import { getOfferingBySlugResult, getProductPaths } from '@sonordev/commerce-kit/commerce/server'
    
    export async function generateStaticParams() {
      return (await getProductPaths()).map((path) => path.params)
    }
    
    export default async function Product({ params }: { params: Promise<{ slug: string }> }) {
      const { slug } = await params
      const { ok, data: product } = await getOfferingBySlugResult({}, slug)
      if (!product) {
        if (ok) notFound() // Sonor answered, and there's no such product
        throw new Error('Sonor didn’t answer, so this page can’t tell whether the product exists.')
      }
      // The product is already here, so it's in the page's HTML from the first byte.
      return <ProductPage product={product} />
    }
    HelperWhat it returns
    getOfferings(config, { type, category, limit, status })Offerings, active by default. type can be one type or an array, and category is a category id
    getOfferingBySlug(config, slug)One offering, or null
    getUpcomingEvents(config, { limit, category })Upcoming events with their schedules, one item per upcoming date
    getNextEvent(config, category?)The next event, or null
    getProductPaths(config), getEventPaths(config), getOfferingPaths(config, type?){ params: { slug } }[] for static generation. In the App Router, map each to its params as above
    generateOfferingMetadata(offering, siteUrl){ title, description, openGraph } for an offering, using its SEO title and description when it has them, or null. openGraph.url is siteUrl/<type>s/<slug>, so set your own when your pages live elsewhere
    createServerConfig(apiUrl?, apiKey?)A config with an address or key of your own. Leave both out and the helpers read your environment, the same as passing {}

    Each fetcher that returns a list or a single offering has a *Result twin (getOfferingsResult, getOfferingBySlugResult, getUpcomingEventsResult, getNextEventResult) that returns { ok, data }. ok: true with empty data means Sonor answered and there's genuinely nothing; ok: false means the request failed. Branch on ok before a page says "nothing available" or returns a 404, because the plain helpers give the same empty answer for both.

    Offerings come back exactly as the browser components load them, images, variants, size chart, notes and options included, so you can hand ProductPage or ProductDetail the product you fetched. A product marked sold out in Sonor keeps its page: it comes back with status: 'sold_out', the components show it as unavailable with buying turned off, and checkout refuses it. If you draw a product with your own markup, check status the same way. Drafts and archived offerings have no page.

    Caching

    Each response is cached for 60 seconds under the commerce cache tag, and Sonor expires that tag when you change an offering, an image, a schedule or stock. On a site that serves the revalidation route, an edit shows within seconds; on any other, within the minute. Change the window with revalidate in the config, in seconds, or pass 0 to fetch on every request (which makes the page render on every request too):

    const products = await getOfferings({ revalidate: 300 }, { type: 'product' })

    Every request has a time limit (10 seconds an attempt, 30 in all) and retries a busy or briefly unavailable API, so a slow day can't hang your build. It goes through site-kit's serverApiFetch, the same path as the rest of site-kit's server data.

    Helpers that run anywhere

    @sonordev/commerce-kit/commerce/utils has the formatting helpers (formatPrice, formatDate, formatDateTime, formatTime, formatDateRange, getRelativeTimeUntil, getSpotsRemaining, isEventSoldOut and getOfferingUrl) and the calendar helpers (icsForEvent, googleCalendarUrl and icsResponse). It has no React, so a server component, generateMetadata or a route handler can call them, which they can't do through @sonordev/commerce-kit/commerce: that entry is a client module, and a function imported from one can't be called on the server.

    // app/events/[slug]/calendar.ics/route.ts
    import { getUpcomingEvents } from '@sonordev/commerce-kit/commerce/server'
    import { icsResponse } from '@sonordev/commerce-kit/commerce/utils'
    
    export async function GET(_request: Request, { params }: { params: Promise<{ slug: string }> }) {
      const { slug } = await params
      const event = (await getUpcomingEvents()).find((item) => item.slug === slug)
      const schedule = event?.next_schedule
      if (!event || !schedule) return new Response('Not found', { status: 404 })
      return icsResponse({ offering: event, schedule, url: `https://example.com/events/${slug}` })
    }

    API functions

    Browser calls to Sonor. They go through site-kit's transport, and the ones that create a sale, a registration or a payment are never retried automatically.

    FunctionWhat it does
    fetchOfferings(options)Offerings by type, category and status (default active), with limit, offset, orderBy and order
    fetchOffering(slug)One offering with its variants and upcoming schedules, or null when it can't be loaded
    fetchLatestOffering(type?, category?)The newest offering of a type
    fetchProducts(options)Products. fetchProductsPublic(options) returns { products, total } and also takes search
    fetchProductBySlug(slug)One full product, or null
    fetchCategories()Categories as { id, name, slug }
    fetchServices(options)Services
    fetchUpcomingEvents(options)Events with their schedules. category and limit narrow it, and past: true includes events that already happened
    fetchNextEvent(category?)The next event, or null
    fetchShippingRates(address, offerings)Rates for a shipping address, when the project ships
    shippingSelection(rate)Turns a chosen rate into the checkout fields, including its signed quote
    validateAddress(address)Checks a shipping address
    validateDiscountCode(code, subtotal, offeringIds)Whether a code applies to an order, and the discount it gives
    registerForEvent(eventId, scheduleId, customer)Registers someone for a free event. Resolves { success, sale_id, confirmation_number }, or { success: false, error }
    createCheckoutSession(options)Starts checkout for a product, service or event. Resolves { success, checkout_url, sale_id, confirmation_number }, or { success: false, error }. Pass sourceId, a card token from Square's Web Payments SDK, to charge in the page instead of redirecting
    createPaymentIntent(options)Creates a Stripe PaymentIntent, for paying by card in the page
    fetchActiveProcessor()'stripe', 'square' or null
    fetchProcessorConfig()The public SDK settings for the active processor

    Formatting helpers come with them: formatPrice, formatDate, formatDateTime, formatTime, formatDateRange, getRelativeTimeUntil, getSpotsRemaining, isEventSoldOut and getOfferingUrl. In server code, import them from @sonordev/commerce-kit/commerce/utils (see Helpers that run anywhere).

    Analytics

    With site-kit's analytics on, the components report shopping events to Sonor themselves. There's nothing to wire up.

    EventSent whenProperties
    product_viewProductDetail shows a productoffering_id, name, price, type, category
    add_to_cartSomeone presses Add to Cart in ProductDetail, which shows that button when your page passes onAddToCartoffering_id, variant_id, quantity, price, name
    begin_checkoutSomeone presses the main button in ProductDetail (Buy Now, or Get It Free) and checkout opens in the page. A button that links out to an external store doesn't report itoffering_id, variant_id, quantity, price, name
    purchaseA payment succeeds in ProductDetailthe same, plus sale_id
    checkout_startCheckoutForm is submittedoffering_id, total, quantity

    An event that happens before analytics has loaded waits for it, so the first product view on a page still counts. Checkout and event registration also send the visitor's analytics session, so Sonor can attribute a sale to the visit that led to it.

    Offering types

    type OfferingType = 'product' | 'service' | 'class' | 'event' | 'subscription'
    
    // Abbreviated: import the type for every field.
    interface CommerceOffering {
      name: string; slug: string; type: OfferingType;
      description?: string; featured_image_url?: string;
      price_type: 'fixed' | 'variable' | 'quote' | 'free';
      price?: number; compare_at_price?: number; currency: string;
      track_inventory?: boolean; inventory_count?: number;
      is_clothing?: boolean; size_chart?: SizeChart;
      duration_minutes?: number; capacity?: number;
      location?: string; is_virtual?: boolean;
      schedules?: CommerceSchedule[]; variants?: CommerceVariant[];
    }

    This entry exports all the commerce types. Nine of them (CommerceOffering, CommerceCategory, CommerceVariant, CommerceSchedule, OfferingType, Cart, CartItem, EventCheckoutProps and CheckoutLineItem) are also exported from the root, @sonordev/commerce-kit, which has nothing that runs.

    Per-category styling (data-category / data-offering-type)

    Event and offering surfaces expose the offering's category slug and type as data attributes, so sites can theme categories with plain CSS and no custom components. data-category is only present when the offering has a category; data-offering-type is always present.

    Elements carrying the attributes:

    • CalendarView / EventCalendar: the .site-kit-calendar-event chips (both title and image display modes) and the image-mode wrapper
    • EventTile (both variants, which also covers UpcomingEvents and EventEmbed)
    • EventsWidget: .site-kit-event-card in list and grid views
    • OfferingCard (all variants, which also covers OfferingList / ProductGrid)
    /* e.g. two color schemes on one calendar: workshops vs community nights */
    .site-kit-calendar-event[data-category="workshops"] {
      background: rgba(16, 185, 129, 0.15);
      color: #059669;
    }
    .site-kit-calendar-event[data-category="community"] {
      background: rgba(59, 130, 246, 0.15);
      color: #2563eb;
    }

    The components keep their site-kit- class prefix because sites style those names. Colors come from the same --sk-* brand tokens as the rest of site-kit.

    Size Charts (clothing products)

    interface SizeChart {
      unit: 'inches' | 'cm'
      fit_note?: string                // e.g., "Runs small. Order one size up."
      measurements: string[]           // ['Chest', 'Length', 'Sleeve']
      rows: Array<{
        size: string                   // 'S', 'M', 'L', 'XL'
        values: number[]               // Primary unit values
        values_alt?: number[]          // Auto-converted alternate unit
      }>
    }