CoreWave Storefront Theme Guide

Build modern, server-rendered storefront themes using the CoreWave Template Engine — a WordPress-like directive system for .cw template files. Themes are compiled and rendered server-side by C#, delivering fast, SEO-friendly HTML to the browser.

Template Format: CoreWave storefront themes use the v2 .cw directive format only. Route templates, reusable sections, and partials are authored as backend-rendered .cw files.

Architecture Overview

The CoreWave storefront uses a backend-rendered template pipeline:

  1. Server-side (.cw Template Engine) — The C# backend compiles .cw template files into HTML by executing directives (@query, @foreach, @if, etc.) against a data context.
  2. React HTML Injection — The customer frontend injects the backend-rendered HTML into the storefront route. Full first-response SEO SSR is available through the backend /v1/public/storefront/ssr endpoint and nginx routing.

Key Features

  • Directive-based templates — Familiar WordPress/PHP-like syntax (@query, @foreach, @if, @include) compiled by C#
  • Server-side rendering — Full HTML output for fast page loads and excellent SEO
  • Template inheritance — Extend base layouts with @extends, @section, @yield
  • Data access functions — WordPress-like cw_get_products(), cw_get_categories(), cw_get_cart(), etc.
  • Hook and filter system — Extend templates with @hook and @filter directives
  • Plugin integration — Bundle companion plugins via bundled-plugins/
  • v2-only visual editing — Theme editing focuses on runtime .cw templates, sections, partials, settings, and route assignments

Rendering Pipeline

<code>
┌─────────────────────────────────────────────────┐
│ Theme ZIP                                        │
│  templates/home.cw   templates/product.detail.cw │
│  sections/header.cw  sections/footer.cw          │
└──────────â”Ŧ──────────────────────────────────────┘
           │ Extract & Upload
           â–ŧ
┌─────────────────────────────────────────────────┐
│ Backend (C#)                                     │
│  1. Lexer — Tokenizes .cw into TaggedTokens      │
│  2. Parser — Builds AST from tokens              │
│  3. Compiler — Walks AST, executes directives    │
│  4. Sandbox — Restricts to registered cw_*()     │
│  5. Output — Server-rendered HTML                │
└──────────â”Ŧ──────────────────────────────────────┘
           │ fetchRenderTemplate() (API)
           â–ŧ
┌─────────────────────────────────────────────────┐
│ Frontend (Browser)                               │
│  Inject HTML into DOM                            │
│  Hydrate widget areas                            │
│  Missing template → Show v2 template error       │
└─────────────────────────────────────────────────┘
    

Prerequisites #

Before developing a CoreWave storefront theme, ensure you have:

  • A CoreWave institution account with storefront feature enabled
  • Access to the theme upload area in your storefront dashboard to install themes
  • A text editor or IDE for writing .cw template files and .json configuration
  • Basic knowledge of HTML, CSS, and JavaScript for theme assets
  • Familiarity with WordPress/PHP template syntax (helpful but not required — directives are similar)

Tools & Environment

  • Theme packaging — ZIP format containing manifest.json, templates/, partials/, assets/, bundled-plugins/
  • Testing — Upload and install the theme from your storefront dashboard, then preview on the public storefront
  • Asset storage — CSS, JS, images, and fonts are uploaded to object storage (Google Cloud Storage) during theme installation

Theme Format

Current storefront themes use formatVersion: 3 and .cw templates only. Theme packages must include at least one renderable .cw file in templates/, sections/, or partials/.

Theme Package Structure #

A CoreWave theme is packaged as a ZIP archive with the following directory layout:

Text
my-theme-v3.0.0.zip
├── manifest.json              // Theme metadata, templates, header/footer presets, starter content, chrome presets
├── templates/
│   ├── home.cw                // Home page template (directive-based)
│   ├── catalogue.default.cw   // Category/collection listing
│   ├── product.detail.cw      // Product detail page
│   ├── cart.cw                // Cart page
│   ├── checkout.cw            // Checkout page
│   ├── login.cw               // Customer login page
│   ├── register.cw            // Customer registration page
│   ├── maintenance.cw         // Maintenance/offline page
│   ├── blogs.default.cw       // Blog index
│   ├── post.default.cw        // Blog post
│   ├── page.default.cw        // Generic page
│   ├── page.not-found.cw      // 404 page
│   └── account/
│       ├── dashboard.cw       // Account dashboard
│       ├── orders.cw          // Order history
│       ├── order-detail.cw    // Single order view
│       ├── wishlist.cw        // Customer wishlist
│       ├── returns.cw         // Customer returns
│       ├── reviews.cw         // Customer reviews
│       ├── profile.cw         // Profile settings
│       ├── addresses.cw       // Saved addresses
│       ├── invoices.cw        // Customer invoices
│       └── documents.cw       // Customer documents
├── partials/
│   ├── header.cw              // Default header partial (fallback)
│   ├── footer.cw              // Default footer partial (fallback)
│   ├── header-light.cw        // Named header preset (@include via key)
│   ├── header-dark.cw         // Named header preset (@include via key)
│   ├── footer-dark.cw         // Named footer preset (@include via key)
│   └── footer-minimal.cw      // Named footer preset (@include via key)
├── sections/
│   ├── hero-banner.cw         // Reusable section
│   └── featured-products.cw   // Reusable section
├── assets/
│   ├── css/
│   │   ├── style.css
│   │   ├── responsive.css
│   │   └── bootstrap.min.css
│   ├── js/
│   │   └── main.js
│   └── images/
│       ├── logo.png
│       ├── thumbnail.png      // Theme thumbnail (for marketplace listing)
│       └── hero-bg.jpg
└── bundled-plugins/
    ├── whatsapp-chat-v2.0.0.zip  // Optional companion plugin
    └── reviews-summary-v2.0.0.zip

Key Files

File/DirectoryRequiredDescription
manifest.jsonYesTheme metadata, template keys, header/footer presets, starter content, chrome presets, pseudo-code hooks
templates/Yesv2 .cw template files. At minimum include a home/index template
partials/NoHeader/footer partial .cw files (default and named presets), included via @include('filename')
sections/NoReusable template partials included via @include('section-name')
assets/NoCSS, JS, images, fonts referenced by theme templates
bundled-plugins/NoCompanion plugin ZIPs installed alongside the theme

Manifest.json #

The manifest.json is the entry point for your theme. It declares metadata, template keys, starter content, chrome presets, and optional pseudo-code hooks.

JSON
{
  "name":        "My Storefront Theme",
  "version":     "2.0.0",
  "description": "A modern, responsive storefront theme",
  "author":      "CoreWave",
  "formatVersion": 3,

  "thumbnail": "assets/images/thumbnail.png",

  "templates": {
    "catalogue.home":    { "label": "Home",      "icon": "home",    "format": "cw" },
    "catalogue.default": { "label": "Catalog",   "icon": "grid",   "format": "cw" },
    "product.detail":    { "label": "Product Detail", "icon": "box",    "format": "cw" },
    "cart.default":      { "label": "Cart",       "icon": "cart",   "format": "cw" },
    "checkout.default":  { "label": "Checkout",  "icon": "credit-card", "format": "cw" },
    "page.default":      { "label": "Page",       "icon": "file",   "format": "cw" },
    "page.not-found":   { "label": "404",        "icon": "alert-circle", "format": "cw" }
  },

  "headerPresets": [
    {
      "key":     "header-light",
      "label":   "Light Header",
      "partial": "partials/header-light.cw"
    },
    {
      "key":     "header-dark",
      "label":   "Dark Header",
      "partial": "partials/header-dark.cw"
    }
  ],

  "footerPresets": [
    {
      "key":     "footer-dark",
      "label":   "Dark Footer",
      "partial": "partials/footer-dark.cw"
    },
    {
      "key":     "footer-minimal",
      "label":   "Minimal Footer",
      "partial": "partials/footer-minimal.cw"
    }
  ],

  "defaultHeaderPresetKey": "header-light",
  "defaultFooterPresetKey": "footer-dark",

  "starterContent": {
    "catalogue.home": {
      "name":   "Home",
      "status": "Published"
    },

    "pages": [
      {
        "title":           "About Us",
        "handle":          "about-us",
        "templateKey":     "page.default",
        "headerPresetKey": "header-dark",
        "footerPresetKey": "footer-minimal",
        "showInNavigation": true,
        "sortOrder":       2,
        "publish":         true
      },
      {
        "title":           "Contact",
        "handle":          "contact",
        "templateKey":     "page.default",
        "headerPresetKey": "header-light",
        "showInNavigation": true,
        "sortOrder":       3,
        "publish":         true
      }
    ],

    "menus": {
      "main": [
        { "label": "Home", "url": "/", "sortOrder": 1 },
        { "label": "Shop", "url": "/shop", "sortOrder": 2 },
        { "label": "About", "pageHandle": "about-us", "sortOrder": 3 },
        { "label": "Blog", "url": "/blogs", "sortOrder": 4 },
        { "label": "Categories", "sortOrder": 5,
          "children": [
            { "label": "Clothing",  "pageHandle": "category-clothing",  "sortOrder": 1 },
            { "label": "Electronics", "pageHandle": "category-electronics", "sortOrder": 2 }
          ]
        }
      ],
      "footer": [
        { "label": "Contact", "pageHandle": "contact", "sortOrder": 1 }
      ],
      "secondary-nav": [
        { "label": "Support", "url": "/support", "sortOrder": 1 },
        { "label": "FAQ", "pageHandle": "faq", "sortOrder": 2 }
      ]
    }
  },

  "chromePresets": [
    {
      "name":      "Light",
      "thumbnail": "assets/images/light-preset.png",
      "header": { "partial": "partials/header-light.cw" },
      "footer": { "partial": "partials/footer-dark.cw" },
      "settings": {
        "backgroundColor": "#FFFFFF",
        "textColor":        "#1a202c",
        "primaryColor":    "#0833AB",
        "fontFamily":      "Nunito, sans-serif"
      }
    }
  ]
}

Manifest Properties

nameStringRequired
Human-readable theme display name
versionStringRequired
Semantic version string (e.g. "2.0.0")
descriptionStringOptional
Theme description shown in marketplace listings
authorStringOptional
Theme author/developer name
formatVersionNumberRequired
Must be 3. CoreWave storefront themes are v2 .cw-only.
thumbnailStringOptional
Path to theme thumbnail image (for marketplace display)
headerPresetsArrayOptional
Named header presets. Each preset has key, label, and partial (path to the .cw file). Styling belongs in the partial and theme CSS, not in preset settings.
footerPresetsArrayOptional
Named footer presets. Each preset has key, label, and partial (path to the .cw file). Styling belongs in the partial and theme CSS, not in preset settings.
defaultHeaderPresetKeyStringOptional
Key of the default header preset used when a page does not specify one. Falls back to first entry in headerPresets
defaultFooterPresetKeyStringOptional
Key of the default footer preset used when a page does not specify one. Falls back to first entry in footerPresets

templateKeys (manifest.json)

Each template key in templates maps to a route kind and must point to a .cw directive template.

KeyRoute KindDescription
catalogue.homeHome / LandingStorefront index/home page
catalogue.defaultCatalogProduct category/collection listing
product.detailProductIndividual product detail page
cart.defaultCartShopping cart page
checkout.defaultCheckoutCheckout/payment page
login.defaultLoginCustomer login/sign-in page
register.defaultRegisterCustomer registration/sign-up page
maintenance.*MaintenanceMaintenance mode / offline page (any key with maintenance prefix)
page.defaultPageGeneric CMS content page
page.not-found404Page not found
blogs.defaultBlogsBlog index/listing
post.defaultBlog PostIndividual blog post
account.defaultAccountCustomer account dashboard
account.ordersAccount OrdersCustomer order history
account.order-detailAccount Order DetailSingle order view
account.wishlistAccount WishlistCustomer wishlist
account.returnsAccount ReturnsCustomer returns
account.reviewsAccount ReviewsCustomer reviews
account.profileAccount ProfileProfile settings
account.addressesAccount AddressesSaved addresses
account.invoicesAccount InvoicesCustomer invoices
account.documentsAccount DocumentsCustomer documents
Flexible Template Keys: Template keys are not restricted to the conventional prefixes listed above. You can use any dot-separated key format — for example, theme.home, my.page, custom.catalog, or anything.else. The only requirement is that each key is unique within a storefront and follows the pattern [a-z0-9._-]+. The home template is resolved by scanning for a key containing home as a dot-segment (e.g., catalogue.home, theme.home), falling back to the first available key.

Design.json #

The optional design.json defines the default visual appearance of the storefront — colors, fonts, header/footer presets, and global styles. When present, it is embedded in the runtime index and used as the fallback design.

JSON
{
  "globalStyles": {
    "fontFamily":    "Nunito, sans-serif",
    "backgroundColor": "#FFFFFF",
    "textColor":      "#1e293b",
    "primaryColor":   "#0833AB",
    "accentColor":    "#FD1D1D",
    "borderRadius":   8,
    "presets": [
      {
        "key":   "header-light",
        "name":  "Light Header",
        "block": {
          "id": "sf-theme-header",
          "systemRole": "header"
        }
      },
      {
        "key":   "footer-dark",
        "name":  "Dark Footer",
        "block": {
          "id": "sf-theme-footer",
          "systemRole": "footer"
        }
      }
    ]
  },

  "customCss": "/* custom CSS injected at runtime */",

  "cssVars": {
    "--cw-font-family":    "Nunito, sans-serif",
    "--cw-primary":       "#0833AB",
    "--cw-accent":        "#FD1D1D",
    "--cw-background":    "#FFFFFF",
    "--cw-text-color":    "#1e293b",
    "--cw-border-radius":  "8px"
  }
}

Design Properties

PropertyTypeDescription
globalStylesObjectGlobal visual settings (fonts, colors, spacing). The presets sub-array stores both chrome presets (header/footer designs, each with a systemRole marker) and CSS variable theme presets. The default preset is determined by array order (first entry = default)
templateSettingsObjectPer-template visual settings keyed by .cw template key or page handle
customCssStringCustom CSS injected at runtime
cssVarsObjectCSS custom property overrides

Template System V2 #

CoreWave v2 templates use .cw files with a WordPress/PHP-inspired directive language. These files are processed server-side by the C# backend, which compiles directives into rendered HTML.

How it works: The template engine uses a 4-stage pipeline: Lexer (tokenizes @directives), Parser (builds AST), Compiler (walks AST, executes C# code), Sandbox (restricts to safe cw_*() functions). All directives are C#-executed — no PHP involved.

Basic Template Structure

HTML
{{--
  Template Name: Home Page
  Template Key: catalogue.home
--}}

@code
  __featured = cw_get_featured_products(8);
  __store = cw_get_store_info();
@endcode

@include('header')

{{-- Hero Section --}}
<section class="hero">
  <div class="container">
    <h1>@code echo __store.name; @endcode</h1>
    <p>@code echo __store.tagline; @endcode</p>
    <a href="@link('/shop')" class="btn btn--primary">Shop Now</a>
  </div>
</section>

{{-- Featured Products --}}
@if(__featured)
<section class="products">
  <div class="container">
    <h2>Featured Products</h2>
    <div class="product-grid">
      @each('product-card', __featured, 'product')
    </div>
  </div>
</section>
@endif

@include('footer')

Template Comments

Use {{-- comment --}} for template comments that are stripped from the rendered output:

HTML
{{-- This comment will not appear in the rendered HTML --}}

Template Inheritance

Use @extends, @section, and @yield for layout inheritance:

Layout file: templates/layouts/main.cw

HTML
<!DOCTYPE html>
<html>
<head>
  @yield('head')
</head>
<body>
  @include('header')
  <main>
    @yield('content')
  </main>
  @include('footer')
  @stack('footer-scripts')
</body>
</html>

Child template: templates/page.default.cw

HTML
@extends('layouts/main')

@section('head')
  <title>@code echo __page.title; @endcode</title>
  @css('assets/css/page.css')
@endsection

@section('content')
  <article>
    <h1>@code echo __page.title; @endcode</h1>
    @code echo __page.content; @endcode
  </article>
@endsection

@push('footer-scripts')
  <script src="@asset('assets/js/page.js')"></script>
@endpush

Data Setup with @code

Use @code ... @endcode blocks to set up variables and fetch data before rendering:

HTML
@code
  __products = cw_get_products(['category' => 'clothing', 'limit' => 12]);
  __store = cw_get_store_info();
  __cart_count = cw_get_cart_count();
  __is_logged_in = cw_user_logged_in();
@endcode

Sections & Partials V2 #

Sections are reusable .cw template fragments stored in sections/. They are included from route templates with @include() and can also be repeated over data with @each(). Partials live in partials/ and are best for smaller shared fragments such as product cards, badges, breadcrumbs, and menu rows.

Text
theme.zip
├── templates/
│   ├── catalogue.home.cw
│   └── product.detail.cw
├── sections/
│   ├── hero-banner.cw
│   └── featured-products.cw
└── partials/
    └── product-card.cw

Create a Section

A section is a normal .cw file. It can read global context variables, variables set with @var, and data returned by @query or cw_*() functions.

HTML
{{-- sections/featured-products.cw --}}
<section class="featured-products">
  <header>
    <h2>{{ cw_default(__section_title, 'Featured Products') }}</h2>
  </header>

  @query(__products, ['type' => 'products', 'tag' => 'featured', 'limit' => 8, 'orderby' => 'price', 'order' => 'asc'])
    <div class="product-grid">
      @each('product-card', __products, 'product')
    </div>
  @else
    <p>No featured products are available yet.</p>
  @endquery
</section>

Use a Section

Include sections by basename, dotted key, folder path, or runtime key. The engine searches published database templates, templates/, sections/, and partials/.

HTML
@extends('layouts/main')

@section('content')
  @include('hero-banner')

  @var(__section_title, 'Best sellers')
  @include('featured-products')
@endsection
Editor isolation: When a customer edits a storefront with the visual editor, the edit is saved as that institution's theme/template override. It does not mutate the shared marketplace theme package used by other customers. Other customers using the same base theme keep their own independent runtime and overrides.

Template Directive Reference #

Data & Loops

DirectivePurposeExample
@query(__var, [...]) ... @endqueryQuery database records with loop@query(__products, ['type'=>'product', 'category'=>'clothing']) ... @endquery
@foreach(__items as __item)Loop over a collection@foreach(__products as __product) ... @endforeach
@for(__i=0; __i<__n; __i++)Numeric loop@for(__i=0; __i<3; __i++) ... @endfor
@while(__condition)Conditional loop@while(cw_have_products()) ... @endwhile
@elseFallback inside @query / @if@else <p>No items</p> @endquery
@breakExit loop early@if(__index > 10) @break @endif
@continueSkip to next iteration@if(__product.sold_out) @continue @endif

Conditionals

DirectivePurposeExample
@if(__condition) ... @endifConditional rendering@if(__product.on_sale) <span>Sale!</span> @endif
@elseif(__condition)Else-if branch@elseif(__product.featured) ... @endif
@elseElse branch@else ... @endif
@unless(__condition)Inverted condition (if not)@unless(__product.sold_out) ... @endunless
@isset(__var) ... @endissetCheck if variable is set@isset(__product.rating) ... @endisset
@empty(__var) ... @endemptyCheck if variable is empty@empty(__products) ... @endempty
@switch(__var) ... @endswitchSwitch-case@switch(__product.type) @case('simple') ... @endswitch

Code Execution

DirectivePurposeExample
@code ... @endcodeInline C# code block@code __title = cw_get_store_name(); @endcode
@echo(__value)Output a value@echo(__product.title)
@var(__key, __value)Set template variable@var(__title, 'My Page')

Template Parts & Inheritance

DirectivePurposeExample
@include('partial')Include a template part@include('header')
@each('partial', __items, 'item')Include for each item in collection@each('product-card', __products, 'product')
@extends('layout')Extend a parent layout@extends('layouts/main')
@section('name') ... @endsectionDefine a content section@section('content') ... @endsection
@yield('name')Render a section from parent layout@yield('content')

Widgets & Hooks

DirectivePurposeExample
@widget('area')Render a widget area@widget('sidebar')
@hook('name', __arg)Execute an action hook@hook('product.card.after', __product)
@filter('name', __value)Apply a filter hook@filter('product.price_html', __html)

Assets & URLs

DirectivePurposeExample
@asset('path')Theme asset URL@asset('assets/js/main.js')
@link('path')Storefront page URL@link('/about')
__entity.urlEntity-specific URL{{ __product.url }}
__entity.imageEntity image URL (property)__product.image
@css('file.css')Enqueue a CSS file@css('assets/css/hero.css')
@js('file.js')Enqueue a JS file@js('assets/js/carousel.js')

Conditional Tags

DirectivePurpose
@is_home() ... @endisIs the home page?
@is_page('slug') ... @endisIs a specific page?
@is_product() ... @endisIs a product detail page?
@is_category('slug') ... @endisIs a category page?
@is_blog() ... @endisIs the blog index?
@is_single() ... @endisIs a single blog post?
@is_search() ... @endisIs a search results page?
@is_account() ... @endisIs the customer account page?
@is_cart() ... @endisIs the cart page?
@is_checkout() ... @endisIs the checkout page?
@has_products() ... @endisDoes the query have products?
@has_image(__entity) ... @endisDoes the entity have an image?
@user_logged_in() ... @endisIs a customer logged in?
@is_logged_in ... @endis_logged_inAlias for @user_logged_in() — conditional block based on login status

Stacks & Comments

DirectivePurposeExample
{{-- comment --}}Template comment (not rendered){{-- This won't appear in HTML --}}
@stack('name')Render a push stack position@stack('footer-scripts')
@push('name') ... @endpushPush content onto a stack@push('footer-scripts') <script>...</script> @endpush

Implemented v2 Runtime Compatibility Notes

The production .cw engine accepts the same argument shapes used throughout this guide. Positional arguments, named arguments, colon arguments, equals arguments, and PHP-style array arguments are normalized before a directive or function executes.

@include('header')
@query(__products, ['type' => 'products', 'limit' => 8, 'orderby' => 'price'])
@hook('product.card.after', __product)
@filter(name: 'product.price_html', input: __html)
@var(__title, 'Featured Products')
@echo(__title)
SyntaxSupported?Details
'value' / "value"YesString literals are unquoted before use.
key=valueYesClassic named argument syntax.
key: valueYesRecommended for readable single-line calls.
['key' => 'value']YesAccepted for WordPress/PHP-like theme portability.
__object.propertyYesWorks for C# objects, dictionaries, JSON objects, arrays with numeric indexes, and function return objects.
cw_get_store().nameYesFunction results can be accessed with dot-property syntax after JSON parsing.

Directive Details

@extends, @section, @yield

Layout inheritance is resolved before normal rendering. The child template is compiled first so all sections are captured, then the parent layout is loaded and rendered with those sections available. Layout lookup accepts layouts/main, layouts.main, and matching templates/layouts/main.cw runtime keys.

@extends('layouts/main')

@section('title')Home@endsection

@section('content')
  <h1>{{ cw_get_store().name }}</h1>
@endsection

@include and @each

Includes resolve against published database templates and the active marketplace runtime index. The lookup supports templates/, sections/, partials/, dotted keys, basename keys, and direct .cw file paths. Use @each when the same partial should be rendered for every item in a collection.

@query(__products, ['type' => 'products', 'limit' => 12])
  @each('product-card', __products, 'product')
@endquery

@code, @var, and @echo

@code is a safe theme-script block, not arbitrary PHP or arbitrary C#. It supports variable assignment and echo statements. Assigned values are stored in the template context and can be arrays/objects returned by cw_*() functions.

@code
  __store = cw_get_store();
  __featured = cw_get_featured_products(8);
  echo __store.name;
@endcode

@var(__cta, 'Shop now')
<a href="/products">@echo(__cta)</a>

Control Flow

The engine supports @if, @elseif, @else, @unless, @isset, @empty, @switch, @case, @default, @break, and @continue. Loop flow directives work inside @foreach, @query, @each, @for, and @while. @break also exits a @switch branch.

@foreach(__products as __product)
  @if(__product.stockQuantity <= 0)
    @continue
  @endif

  @switch(__product.productType)
    @case('Digital')
      <span>Instant delivery</span>
      @break
    @default
      <span>Ships after checkout</span>
  @endswitch
@endforeach

Stacks

@push appends rendered content to a named stack. @stack outputs the concatenated stack content, usually in a layout before </head> or </body>.

@push('footer-scripts')
  <script src="@asset('assets/js/gallery.js')" defer></script>
@endpush

{{-- in layout --}}
@stack('footer-scripts')

Loop Variables

Inside @foreach and @query loops, the __loop variable provides metadata:

PropertyDescription
__loop.firstIs this the first iteration?
__loop.lastIs this the last iteration?
__loop.indexZero-based index
__loop.iterationOne-based index
__loop.countTotal items in the loop
__loop.remainingRemaining items

@query — The Core Data Directive

The @query directive queries the database and loops through results:

HTML
@query(__products, [
  'type' => 'product',
  'category' => 'clothing',
  'limit' => 12,
  'order' => 'desc',
  'orderby' => 'price'
])
  @foreach(__products as __product)
    @include('product-card', ['product' => __product])
  @endforeach
@else
  <p>No products found.</p>
@endquery

Supported Query Parameters

ParameterValuesDescription
typeproduct, page, blog, post, category, customer, order, discountEntity type to query
categorystring, slugFilter by category slug
category_idintFilter by category ID
collectionstring, slugFilter by collection slug
tagsstring[]Filter by tags
idsint[]Specific IDs to fetch
limitint (default: 20)Max results
offsetintPagination offset
pageintPage number
orderasc, descSort direction
orderbyprice, title, date, popularity, rating, salesSort field
featuredboolFeatured products only
on_saleboolOn-sale products only
in_stockboolIn-stock products only
searchstringSearch keyword

@is_logged_in / @user_logged_in — Login-Aware Block

The @is_logged_in directive (or its alias @user_logged_in()) conditionally renders its block content only when a storefront customer is authenticated. An @else branch can be used to show alternative content for unauthenticated visitors.

HTML
@code
  __customer = cw_get_customer_profile().customer;
@endcode

@is_logged_in
  <div class="welcome-banner">
    <h3>Welcome back, { __customer.first_name } { __customer.last_name }!</h3>
    <a href="/account">My Account</a>
  </div>
@else
  <div class="login-prompt">
    <p>Sign in for personalised shopping.</p>
    <a href="/login" class="btn btn-primary">Sign In</a>
  </div>
@endis_logged_in

You can also use the function form in @if blocks for more complex conditions:

HTML
@if(cw_user_logged_in())
  <p>You are signed in.</p>
@else
  <p>Guest browsing.</p>
@endif

Data Access Functions #

Data access in v2 templates uses two complementary approaches: the @query directive for fetching collections, and cw_*() utility functions for lookups, formatting, and store metadata. All functions execute server-side via the template engine's sandbox.

Note: Use @query for product, category, collection, blog, post, discount, and tag listing. Use cw_*() functions for single-item lookups, store metadata, formatting, and tag operations. The current product and category objects are injected automatically as template variables (__product, __category) on their respective pages.

Product & Category Functions

CODE
@query(__products, ['type' => 'products', 'limit' => 8, 'orderby' => 'price', 'order' => 'asc'])
@query(__categories, ['type' => 'categories', 'limit' => 20])
@query(__products_by_category, ['type' => 'products', 'category' => 'clothing', 'limit' => 12])
@query(__products_by_tag, ['type' => 'products', 'tag' => 'featured', 'limit' => 6])
@query(__products_by_ids, ['type' => 'products', 'ids' => '1,2,3,4,5'])

{{-- Single lookups (returns Tag collection) --}}
__product_tags = cw_get_product_tags(productId: __product.id);
__post_tags = cw_get_post_tags(postId: __post.id);
__tags = cw_get_tags(scope: 'products');
__tag = cw_get_tag(slug: 'featured');
__by_tag_products = cw_get_products_by_tag(tag: 'featured', limit: 10);
__by_tag_posts = cw_get_posts_by_tag(tag: 'news', limit: 10);

Blog & Post Functions

CODE
@query(__posts, ['type' => 'posts', 'limit' => 5, 'orderby' => 'published_at'])
@query(__posts_in_category, ['type' => 'posts', 'category' => 'news', 'limit' => 10])
@query(__blogs, ['type' => 'blogs'])
@query(__all_posts, ['type' => 'posts', 'limit' => 50])

Discount Functions

CODE
@query(__discounts, ['type' => 'discounts', 'limit' => 10])

{{-- Single discount by code --}}
__discount = cw_get_discount(code: 'SAVE20');

{{-- Validate a discount code against a subtotal --}}
__validation = cw_validate_discount(code: 'SAVE20', subtotal: '150.00');
{{-- Returns: {"valid": true/false, "error": "...", "type": "...", "value": 20} --}}

Tag Functions

CODE
@query(__tags, ['type' => 'tags', 'scope' => 'products', 'limit' => 20])
@query(__all_tags, ['type' => 'tags', 'scope' => 'both'])

{{-- Named tag functions --}}
__product_tags = cw_get_product_tags(productId: 42);
__post_tags = cw_get_post_tags(postId: 15);
__tags = cw_get_tags(scope: 'products');
__tag = cw_get_tag(slug: 'featured');
__tagged_products = cw_get_products_by_tag(tag: 'sale', limit: 10);
__tagged_posts = cw_get_posts_by_tag(tag: 'announcement', limit: 5);

Store & Formatting Functions

CODE
__store = cw_get_store();
{{-- Returns store object with name, slug, logo, currency, contact info, brand colors --}}

cw_format_money(amount: '1500.00', currency: 'NGN');
{{-- Returns: "NGN 1,500.00" --}}

cw_format_date(date: '2024-01-15', format: 'M d, Y');
{{-- Returns: "Jan 15, 2024" --}}
{{-- Supported formats: M d, Y | Y-m-d | d/m/Y | F j, Y --}}

cw_slugify(input: 'Hello World');
{{-- Returns: "hello-world" --}}

cw_truncate(text: 'Long text here...', length: 100, ellipsis: '...');
{{-- Returns truncated string with ellipsis --}}

cw_default(value: '', default: 'No content');
{{-- Returns default if value is empty --}}

cw_echo('Any string');
{{-- Outputs the string directly --}}

cw_count(var: __products);
{{-- Returns the count of items in a collection --}}

{{-- Utility & conditional lookups --}}
cw_has_image(entity: __product);
{{-- Returns: "true" or "false" --}}

cw_get_store_name();
{{-- Returns: "My Store" (string) --}}

cw_get_store_currency_code();
{{-- Returns: "NGN" (string, from institution settings) --}}

cw_get_cart_count();
{{-- Returns: "3" (int as string, from session) --}}

cw_get_cart_applied_discounts();
{{-- Returns: JSON array of applied discount codes --}}

cw_user_logged_in();
{{-- Returns: "true" or "false" (checks session for customer_email) --}}

cw_get_related_products(productId: __product.id, limit: 4);
{{-- Returns: JSON array of related Product objects (shared categories) --}}

{{-- ── Shopper Currency Switcher Functions ── --}}
cw_set_currency(code: 'USD');
{{-- Sets the shopper's preferred currency in the session. Returns: {"success": true, "code": "USD"} --}}
{{-- Call this when a shopper selects a currency from your switcher UI --}}

cw_get_current_currency();
{{-- Returns: {"code": "USD", "symbol": "$", "name": "US Dollar", "is_base": false} --}}
{{-- Returns the shopper's preferred currency, or the store's base currency if none is set --}}

cw_get_available_currencies();
{{-- Returns: JSON array of all active currencies available to the shopper --}}
{{-- Each entry: {"code": "USD", "symbol": "$", "name": "US Dollar", "is_base": false} --}}

cw_convert_price(amount: 1500.00, to: 'USD');
{{-- Converts an amount from the store's base currency to the target currency --}}
{{-- Uses exchange rates configured in the CurrencyRate system (BuyRate/SellRate) --}}
{{-- Returns: {"original_amount": 1500.00, "converted_amount": 1.04, "from_currency": "NGN", "to_currency": "USD", "rate": 1440.50} --}}
{{-- If 'to' is omitted, converts to the shopper's preferred currency (or base currency) --}}

cw_get_checkout_breakdown(subtotal: 5000, countryCode: 'NG', regionCode: 'LA', cityName: 'Lagos', deliveryMethodName: 'Standard', weight: 2.5, couponCode: 'WELCOME10', customerEmail: 'customer@example.com');
{{-- Returns full checkout pricing breakdown: {"subtotal":5000,"discountTotal":500,"deliveryTotal":1000,"taxTotal":350,"processingFee":250,"processingFeeTax":18.75,"shippingFeeTax":75,"grandTotal":6193.75,"currencyCode":"NGN","deliveryMethodName":"Standard","couponError":null} --}}
{{-- Parameters: subtotal, countryCode, regionCode, cityName (optional), deliveryMethodName (optional), weight (optional), couponCode (optional), customerEmail (optional) --}}
{{-- V2: Delivery total is resolved through the active API shipping provider, not zone-based rules --}}
{{-- V3 (Fee Overhaul): Pricing breakdown now includes processingFee, processingFeeTax (7.5% regulatory charge on processing fee), and shippingFeeTax (7.5% regulatory charge on shipping fee). The grandTotal formula is: subtotal - discount + deliveryTotal + taxTotal + processingFee + processingFeeTax + shippingFeeTax. --}}

cw_get_shipping_price(subtotal: 5000, countryCode: 'NG', cityName: 'Lagos', weight: 2.5);
{{-- Returns API-calculated shipping price: {"shippingCost":1000,"currencyCode":"NGN","error":null} --}}
{{-- Parameters: subtotal, countryCode, cityName (optional), weight (optional, default: 0.5) --}}
{{-- Uses the active shipping provider configured in Storefront > Shipping Settings. --}}
{{-- Returns shippingCost = 0 and an error message if no provider is active or rate cannot be calculated. --}}

cw_get_order_tracking(orderNumber: 'ORD-001');
{{-- V2 — Returns tracking data for a given order: {"trackingNumber":"1Z999AA10123456784","status":"in_transit","estimatedDeliveryDate":"2026-05-10","deliveredAt":null,"events":[...],"error":null} --}}
{{-- Parameters: orderNumber (required) --}}
{{-- Each event: {"status":"picked_up","location":"Lagos, NG","description":"Package picked up","occurredAt":"2026-05-01T10:00:00Z"} --}}
{{-- Returns error if order not found or no tracking available. --}}

Pagination & Conditional Checks

Pagination and conditional routing checks are built into the template context. Pages using @query automatically receive pagination state. Conditional template sections can use @if with template context checks:

CODE
@if(__products && cw_count(var: __products) > 0)
  {{-- Has products --}}
@endif

@if(cw_get_store().name)
  {{-- Store exists --}}
@endif

@isset(__product)
  {{-- Product page context --}}
@endisset

Function Reference Table

FunctionReturnsDescription
cw_get_product_tags(productId)Collection of TagsGet tags for a product
cw_get_post_tags(postId)Collection of TagsGet tags for a blog post
cw_get_tags(scope)Collection of TagsGet all tags (scope: products/posts/both)
cw_get_tag(slug)?TagGet a single tag by slug
cw_get_products_by_tag(tag, limit, offset)Collection of ProductsGet products by tag slug with pagination (offset defaults to 0)
cw_get_posts_by_tag(tag, limit, offset)Collection of PostsGet posts by tag slug with pagination (offset defaults to 0)
cw_get_discounts(limit)Collection of DiscountsGet active discount codes
cw_get_discount(code)?DiscountGet a discount by code
cw_validate_discount(code, subtotal){"valid","error","type","value"}Validate a discount against subtotal
cw_get_store()StoreGet store metadata and settings
cw_get_store_info()StoreAlias for cw_get_store(). Useful for themes ported from the v2 starter examples.
cw_get_products(limit?, category?, tag?, ids?, orderby?, order?)Collection of ProductsFetch products directly as a JSON collection. Accepts named parameters or a PHP-style options array. Prefer @query for large rendered loops; use this function when assigning reusable collections in @code.
cw_get_featured_products(limit?)Collection of ProductsFetch a product collection for featured-product sections. Current runtime returns active products using the supplied limit and ordering arguments.
cw_get_categories(limit?, orderby?)Collection of CategoriesFetch storefront product categories. Each item includes ID, name, slug/code fallback, description, and product count where available.
cw_get_collections(limit?, orderby?)Collection of CollectionsFetch product collections for collection grids, collection pickers, and landing page sections.
cw_get_blogs()Collection of BlogsFetch active storefront blogs with handles and post counts.
cw_get_posts(limit?, category?/blog?, tag?, orderby?, order?)Collection of PostsFetch published blog posts. Use category or blog to filter by blog handle, and tag to filter by tag slug.
cw_json_decode(value)object/array/stringReturns the supplied JSON text for template evaluation. When assigned with @code, JSON-looking function results are parsed by the engine and support dot/index access.
cw_get_store_name()stringGet store name from institution settings
cw_get_store_currency_code()stringGet store currency code (e.g., NGN)
cw_has_image(entity)boolCheck if entity has an image/thumbnail
cw_get_related_products(productId, limit)Collection of ProductsGet related products by shared categories
cw_get_cart_count()intGet cart item count from session
cw_get_cart_applied_discounts()Collection of DiscountsGet applied discounts from session
cw_user_logged_in()boolCheck if customer is logged in (session check)
cw_submit_product_review(productId, rating, comment)objectSubmit a product review (rating 1-5)
cw_product_reviews(productId, page, limit)Collection of ReviewsGet paginated product reviews
cw_get_customer_product_review(productId)?Review or nullGet the logged-in customer's review for a specific product (or null if not reviewed)
cw_submit_post_comments(postId, name, email, comment)objectSubmit a blog post comment
cw_post_comments(postId, page, limit)Collection of CommentsGet paginated blog post comments
cw_format_money(amount, currency)stringFormat a decimal as currency (default: NGN)
cw_format_date(date, format)stringFormat a date string
cw_slugify(input)stringConvert text to URL-safe slug
cw_truncate(text, length, ellipsis)stringTruncate text with ellipsis (default 100 chars)
cw_default(value, default)stringReturn default if value is empty
cw_echo(...)stringOutput raw string value
cw_count(var)intCount items in a collection variable
cw_base_paths()objectGet all storefront base paths. See Base Paths Reference below for the full list of accessor properties. Usage: cw_base_paths().account
cw_get_select_options(source, limit?, orderBy?, order?, parentId?, category?, tag?, scope?, ids?)Collection of option objects★ NEW (V2) Get dynamic select options for a data source. Each option: {"value","label"}. Sources: products, categories, collections, blogs, posts, tags, discounts, countries, regions, cities. Used in widget field "type": "select" with source property. Iterate with @foreach.
Navigation Functions
cw_get_navigations()Collection of Navigation objectsGet all active menus with items arranged as a multilevel children tree. Each menu: {"id","name","slug","location","active","items":[...]}. Each item may have a nested children array
cw_get_navigation(id)?NavigationGet a single navigation (menu) by its numeric ID with the full items tree. Returns null if not found
cw_get_navigation_by_location(location)?NavigationGet a single navigation by location key ("main" or "footer"). Returns null if no menu exists at that location
cw_get_navigation_by_key(key)?NavigationGet a single navigation by its slug/key (e.g. "main-menu"). Returns null if no menu matches
Checkout & Shipping Functions
cw_get_checkout_breakdown(subtotal, countryCode, regionCode, cityName?, deliveryMethodName?, weight?, couponCode?, customerEmail?)objectGet full checkout pricing breakdown including subtotal, discount, delivery, tax, processing fee, regulatory taxes, and grand total. V2: Delivery total is resolved through the active API shipping provider. V3 (Fee Overhaul): Returns additional fields — processingFee (gateway fee), processingFeeTax (7.5% regulatory charge on processing fee), shippingFeeTax (7.5% regulatory charge on shipping fee). taxTotal is the product-level tax set by the storefront owner. Returns {"subtotal","discountTotal","deliveryTotal","taxTotal","processingFee","processingFeeTax","shippingFeeTax","grandTotal","currencyCode","deliveryMethodName","couponError"}
cw_get_shipping_price(subtotal, countryCode, cityName?, weight?)object★ NEW (V2) Get API-calculated shipping price. Delegates to the active shipping provider configured in Storefront > Shipping Settings. Returns {"shippingCost","currencyCode","error"}. Returns shippingCost = 0 and an error message if no provider is active or rate cannot be calculated.
cw_get_order_tracking(orderNumber)object★ NEW (V2) Get tracking data for a given order number. Returns {"trackingNumber","status","estimatedDeliveryDate","deliveredAt","events":[...],"error"}. Each event: {"status","location","description","occurredAt"}. Returns error if order not found or no tracking available.
cw_get_shipping_options(countryCode, regionCode, cityName?, subtotal?, weight?)Collection⛔ DEPRECATED Use cw_get_shipping_price instead. Returns available shipping options based on the older zone hierarchy.
cw_get_shipping_zones()Collection⛔ DEPRECATED Use cw_get_shipping_price instead. Shipping zones are no longer configurable.
Shopper Currency Switcher Functions
cw_set_currency(code)objectSet the shopper's preferred currency in the session. Returns {"success": true, "code": "USD"}
cw_get_current_currency()objectGet the shopper's preferred currency. Returns {"code","symbol","name","is_base"}. Falls back to store base currency
cw_get_available_currencies()CollectionGet all active currencies available to the shopper. Each entry: {"code","symbol","name","is_base"}
cw_convert_price(amount, to?)objectConvert amount from base currency to target currency using configured exchange rates. Returns {"original_amount","converted_amount","from_currency","to_currency","rate"}. Omitting to uses shopper's preferred currency
Customer Account Functions
cw_login_user(email, password, redirect_url?)objectAuthenticate with email and password. Optionally pass redirect_url for post-auth redirect. Returns session token, customer profile, and summary
cw_verify_login(challenge_token, code, redirect_url?)objectVerify an OTP code and complete authentication. Returns redirectUrl in response
cw_logout_user()objectLog out the current customer
cw_register_user(first_name, last_name, email, password, phone?, country_id?, region_id?, city_id?, address?, zip_code?, redirect_url?)objectRegister a new customer account with email and password. Optionally pass location fields (country_id, region_id, city_id), address, zip_code, phone, and redirect_url
cw_auth_redirect(url?)stringGenerate a meta-refresh redirect snippet. Defaults to redirect_url from session, then /account
cw_get_customer_profile()objectGet authenticated customer profile and summary
cw_update_customer_profile(first_name?, last_name?, other_names?, phone?, zip_code?, billing_address?, shipping_address?, extra_info?, profile_picture_asset_id?, country_id?, region_id?, city_id?)objectUpdate customer profile information. extra_info is a JSON array of {"title","value"} objects for custom fields. profile_picture_asset_id is a Media Library asset ID for the profile picture. country_id, region_id, city_id are optional location IDs
cw_update_customer_address(type, address)objectUpdate billing or shipping address. type is "billing", "shipping", or "both"
cw_get_customer_orders(page?, limit?)objectGet paginated order history
cw_get_customer_order(order_id)?OrderGet a single order by ID
cw_get_customer_invoices(page?, limit?)objectGet paginated invoice list
cw_get_customer_receipts(page?, limit?)objectGet paginated receipt list
cw_get_customer_documents(order_id?)Collection of DocumentsGet documents for an order or all orders
cw_get_customer_wishlist()Collection of Wishlist ItemsGet customer wishlist items
cw_add_to_customer_wishlist(product_id)objectAdd a product to wishlist
cw_remove_from_customer_wishlist(product_id)objectRemove a product from wishlist
cw_get_customer_returns(page?, limit?)objectGet paginated return requests
cw_create_customer_return(order_id, reason, product_id?)objectCreate a return request
cw_submit_return_request(order_id, reason, product_id?)objectAlias for cw_create_customer_return — create a return request
cw_cancel_return_request(return_id)objectCancel a pending return request
cw_get_customer_reviews(page?, limit?)objectGet paginated customer product reviews
cw_get_customer_dashboard()objectGet customer dashboard with summary, wishlist, returns, reviews, spend chart

Base Paths Reference

The cw_base_paths() function returns an object with the following accessor properties. Each property returns a URL string relative to the storefront's base URL ({baseUrl}):

AccessorResolved PathDescription
.home{baseUrl}/Homepage URL
.catalogue{baseUrl}/Catalogue/listing page (same as home)
.shop{baseUrl}/shopShop listing page
.pages{baseUrl}/pagesPages directory
.blogs{baseUrl}/{blogBase}Blog listing using the configured blog base (same as .blog)
.blog{baseUrl}/{blogBase}Blog listing using the configured blog base
.products{baseUrl}/{productBase}Product detail base using the configured product base (same as .product)
.product{baseUrl}/{productBase}Product detail base using the configured product base
.category_base{baseUrl}/{categoryBase}Category taxonomy base using the configured category base
.tag / .tag_base{baseUrl}/{tagBase}Tag taxonomy base using the configured tag base
.category{baseUrl}/{categoryArchiveBase}Category listing using the configured category archive base (same as .categories)
.categories{baseUrl}/{categoryArchiveBase}Category listing using the configured category archive base
.account{baseUrl}/{accountBase}Customer account dashboard overview using the configured account base
.account_dashboard{baseUrl}/{accountBase}/dashboardCustomer account dashboard
.account_orders{baseUrl}/{accountBase}/ordersOrder history page
.account_wishlist{baseUrl}/{accountBase}/wishlistWishlist page
.account_returns{baseUrl}/{accountBase}/returnsReturns page
.account_reviews{baseUrl}/{accountBase}/reviewsReviews page
.account_profile{baseUrl}/{accountBase}/profileProfile page
.account_addresses{baseUrl}/{accountBase}/addressesAddresses page
.account_order_detail{baseUrl}/{accountBase}/order-detailSingle order detail page
.account_invoices{baseUrl}/{accountBase}/invoicesInvoices page
.account_documents{baseUrl}/{accountBase}/documentsDocuments page
.cart{baseUrl}/cartShopping cart page
.checkout{baseUrl}/checkoutCheckout page
.login{baseUrl}/loginLogin page
.register{baseUrl}/registerRegistration page
.not_found{baseUrl}/not-found404 not-found page

@query Supported Types & Parameters

TypeParametersReturns
productslimit, orderby, order, category, category_id, tag/tags, ids, q/search, brand, product_type, min_price, max_price, in_stock, on_sale, featuredCollection of Product objects
postslimit, orderby, order, blog/blogHandle/blog_id, tag/tags, ids, q/searchCollection of Blog Post objects
categorieslimit, orderby, order, q/search, code, parent_id, activeCollection of Category objects
collectionslimit, orderby (title)Collection of Collection objects
blogslimit, orderby, order, q/search, statusCollection of Blog objects
discountslimitCollection of Discount objects
tagsscope (products/posts/both), limitCollection of Tag objects
countrieslimit, orderby (name)Collection of Country objects
regionslimit, orderby (name), country_id (int)Collection of Region objects
citieslimit, orderby (name), region_id (int)Collection of City objects

Returned Object Properties

Each @query type and cw_*() function returns objects with the following accessible properties. Properties are accessed via arrow syntax: __product.title.

Product Object

Returned by @query(['type' => 'products', ...]) and cw_get_products_by_tag(). Automatically available as __product on product detail pages.

PropertyTypeDescription
__product.idintProduct ID
__product.titlestringProduct name / title
__product.slugstringURL-safe product segment. Derived from SKU, then name, then ID.
__product.urlstringProduct detail URL using the configured product base, e.g. /{productBase}/{slug}.
__product.skustringStock keeping unit
__product.descriptionstringFull description (HTML)
__product.short_descriptionstringTruncated description (200 chars)
__product.barcodestringBarcode / UPC
__product.pricedecimalCurrent unit price
__product.compare_pricedecimal?Compare-at / original price (set via admin product form)
__product.on_saleboolTrue when product is marked as on sale (set via admin product form)
__product.discount_percentdecimal?Discount percent (0-100) applied when on_sale is true
__product.cost_pricedecimalCost price (internal use)
__product.stock_quantityintCurrent stock count
__product.in_stockboolTrue when stock_quantity > 0
__product.ratingdecimalAverage rating (0.0-5.0, computed from customer reviews)
__product.review_countintNumber of customer reviews
__product.brandstringBrand name
__product.manufacturerstringManufacturer name
__product.product_typestringProduct type enum text.
__product.template_keystring?Optional product-specific template key.
__product.imagestring?Primary image URL (may be null)
__product.thumbnailstring?Primary thumbnail URL
__product.imagesarrayArray of Image objects (see Image Object below)
__product.categoriesarrayArray of {id, name, slug} objects
__product.tagsarrayArray of {id, name, slug} objects
__product.created_atdatetimeCreation timestamp
__product.updated_atdatetimeLast update timestamp

Image Object

Returned inside __product.images array and __post.images array.

PropertyTypeDescription
__image.urlstringFull image URL
__image.altstringAlt text
__image.widthintImage width in pixels
__image.heightintImage height in pixels
__image.thumbnailstring?Thumbnail-size image URL
__image.is_coverboolTrue if this is the primary/cover image

Category Object

Returned by @query(['type' => 'categories', ...]). Automatically available as __category on category pages.

PropertyTypeDescription
__category.idintCategory ID
__category.namestringDisplay name
__category.slugstringURL slug derived from category code, then name.
__category.codestringCategory code used by URL/category filtering when available.
__category.descriptionstringDescription
__category.parent_idint?Parent category ID.
__category.display_orderintManual display order.
__category.template_keystring?Optional category-specific template key.
__category.product_countintActive products in category

Collection Object

Returned by @query(['type' => 'collections', ...]).

PropertyTypeDescription
__collection.idintCollection ID
__collection.titlestringCollection title
__collection.descriptionstringCollection description
__collection.durationstringOptional duration (e.g. "3 months")
__collection.thumbnailstring?Thumbnail image URL
__collection.display_orderintDisplay order
__collection.is_activeboolIs the collection active?
__collection.handlestringURL handle (same as title)
__collection.product_countintActive products in collection

Blog Object

Returned by @query(['type' => 'blogs']).

PropertyTypeDescription
__blog.idintBlog ID
__blog.namestringBlog name
__blog.slugstringURL handle
__blog.descriptionstringDescription (HTML)
__blog.post_countintNumber of posts

Blog Post Object

Returned by @query(['type' => 'posts', ...]). Automatically available as __post on post detail pages.

PropertyTypeDescription
__post.idintPost ID
__post.titlestringPost title
__post.slugstringURL slug (handle)
__post.excerptstringSummary / excerpt
__post.contentstringFull content (JSON/HTML)
__post.content_jsonstringRaw content JSON
__post.cover_imagestring?Cover image URL (may be null)
__post.published_atdatetimePublish timestamp
__post.statusstringPublished / Draft
__post.blog_idintParent blog ID
__post.categoriesarrayArray of {id, name, slug} objects
__post.tagsarrayArray of {id, name, slug} objects
__post.created_atdatetimeCreation timestamp
__post.updated_atdatetimeLast update timestamp

Discount Code Object

Returned by @query(['type' => 'discounts', ...]) and cw_get_discount().

PropertyTypeDescription
__discount.idintDiscount ID
__discount.codestringDiscount code
__discount.display_namestringDisplay name
__discount.typestringDiscount type
__discount.valuedecimalAmount or percentage
__discount.descriptionstringDescription
__discount.minimum_subtotaldecimalMinimum order subtotal
__discount.maximum_discount_amountdecimalMaximum discount cap
__discount.is_free_shippingboolGrants free shipping
__discount.starts_atdatetimeStart date
__discount.ends_atdatetimeExpiry date
__discount.applies_to_allboolApplies to all products

Tag Object

Returned by @query(['type' => 'tags', ...]) and tag lookup functions.

PropertyTypeDescription
__tag.idintTag ID
__tag.namestringDisplay name
__tag.slugstringURL-safe slug
__tag.scopestringScope (products / posts / both)
__tag.product_countintTagged products count
__tag.post_countintTagged posts count

Country Object

Returned by @query(['type' => 'countries', ...]).

PropertyTypeDescription
__country.idintCountry ID
__country.namestringCountry name
__country.country_code_2stringTwo-letter country code (e.g., "US", "NG")

Region / State Object

Returned by @query(['type' => 'regions', 'country_id' => 1, ...]). Represents a state or province within a country.

PropertyTypeDescription
__region.idintRegion ID
__region.namestringRegion / state name
__region.country_idintParent country ID
__region.short_codestring?Short code (e.g., "CA", "NY")

City / District Object

Returned by @query(['type' => 'cities', 'region_id' => 1, ...]). Represents a city or district within a region/state.

PropertyTypeDescription
__city.idintCity ID
__city.namestringCity / district name
__city.region_idintParent region ID

Store Object

Returned by cw_get_store().

PropertyTypeDescription
__store.idintStorefront ID
__store.namestringStore / business name
__store.slugstringStore URL handle
__store.taglinestring?Tagline (currently null)
__store.logostring?Logo URL from theme design
__store.logo_urlstring?Logo URL alias
__store.faviconstring?Favicon URL from theme design
__store.favicon_urlstring?Favicon URL alias
__store.primary_colorstring?Primary brand color (hex)
__store.secondary_colorstring?Secondary brand color (hex)
__store.currencystringCurrency code (NGN)
__store.currency_codestringCurrency code alias
__store.current_currency_codestringShopper's preferred currency code (from session/localStorage cache), falls back to base currency
__store.current_currency_symbolstringShopper's preferred currency symbol (e.g., $)
__store.current_currency_namestringShopper's preferred currency name (e.g., US Dollar)
__store.current_currency_is_baseboolWhether the shopper's preferred currency is the store's base currency
__store.languagestringLanguage code (en)
__store.language_codestringLanguage code alias
__store.timezonestringTimezone (Africa/Lagos)
__store.emailstring?Contact email from CMS
__store.phonestring?Contact phone from CMS
__store.addressstring?Contact address from CMS
__store.social_linksstring?Social links (JSON)

Product Review Functions

Review functions allow customers to submit and browse product reviews directly from templates.

CODE
{{-- Submit a product review --}}
@code
  __result = cw_submit_product_review(
    productId: __product.id,
    rating: 5,
    comment: 'Great product!'
  );
  {{-- __result.success == true --}}
@endcode

{{-- Get paginated product reviews --}}
@code
  __reviews = cw_product_reviews(productId: __product.id, page: 1, limit: 5);
@endcode

@foreach(__reviews as __review)
  <div class="review">
    <div class="review__rating">
      @for(__i=0; __i<__review.rating; __i++)★ @endfor
      @for(__i=__review.rating; __i<5; __i++)☆ @endfor
    </div>
    <p class="review__comment">@code echo __review.comment; @endcode</p>
    <span class="review__author">— @code echo __review.author; @endcode</span>
    <span class="review__date">@code echo __review.date; @endcode</span>
  </div>
@endforeach

Each review object returned by cw_product_reviews() has the following accessors:

AccessorTypeDescription
__review.authorstringReviewer name (from Customer record)
__review.avatarstring?Profile picture URL for the reviewer (if they have one)
__review.ratingintRating (1-5)
__review.commentstringReview text
__review.datestringSubmission date/time

Logged-in Customer Product Review

Use cw_get_customer_product_review(productId) on a product detail page to show the current customer's existing review. Combine with @if(cw_user_logged_in()) to only show the review form to authenticated users.

CODE
{{-- On a product detail page — show the current customer's review if they've written one --}}
@if(cw_user_logged_in())
  @code
    __my_review = cw_get_customer_product_review(productId: __product.id);
  @endcode

  @if(__my_review)
    <div class="your-review">
      <h4>Your Review</h4>
      <div class="review__rating">
        @for(__i=0; __i<__my_review.rating; __i++★ @endfor
        @for(__i=__my_review.rating; __i<5; __i++☆ @endfor
      </div>
      <p>@code echo __my_review.reviewText; @endcode</p>
      <small>Reviewed on @code echo __my_review.createdAt; @endcode</small>

      {{-- Update your review --}}
      @code
        __updated = cw_submit_product_review(
          productId: __product.id,
          rating: 4,
          comment: 'Updated review text'
        );
      @endcode
    </div>
  @else
    <div class="write-review">
      <h4>Write a Review</h4>
      @code
        __result = cw_submit_product_review(
          productId: __product.id,
          rating: 5,
          comment: 'Great product!'
        );
      @endcode
    </div>
  @endif
@else
  <p><a href="{{ cw_route('login') }}">Sign in</a> to review this product.</p>
@endif

When the customer has already reviewed the product, cw_get_customer_product_review() returns an object with id, productId, rating, reviewText, status, createdAt, and updatedAt. If they haven't reviewed it, the function returns null.

Blog Post Comment Functions

Comment functions enable blog post discussions with customer name, email, and content.

CODE
{{-- Submit a blog post comment --}}
@code
  __result = cw_submit_post_comments(
    postId: __post.id,
    name: 'John',
    email: 'john@example.com',
    comment: 'Great article! Thanks for sharing.'
  );
  {{-- __result.success == true --}}
@endcode

{{-- Get paginated blog post comments --}}
@code
  __comments = cw_post_comments(postId: __post.id, page: 1, limit: 20);
@endcode

@if(cw_count(var: __comments) > 0)
  <h3>Comments</h3>
  @foreach(__comments as __comment)
    <div class="comment">
      <img src="@code echo __comment.avatar; @endcode"
           alt="@code echo __comment.author; @endcode"
           class="comment__avatar">
      <div class="comment__body">
        <strong>@code echo __comment.author; @endcode</strong>
        <p>@code echo __comment.comment; @endcode</p>
        <span>@code echo __comment.date; @endcode</span>
      </div>
    </div>
  @endforeach
@endif

Each comment object returned by cw_post_comments() has the following accessors:

AccessorTypeDescription
__comment.authorstringCommenter name
__comment.avatarstringGravatar / avatar URL
__comment.commentstringComment content
__comment.datestringSubmission date/time

Newsletter Functions

Newsletter functions let you collect email subscriptions directly from your storefront templates. Only the email address is required; the subscriber's name is optional.

CODE
{{-- Subscribe to the newsletter (email required, name optional) --}}
@code
  __result = cw_subscribe_newsletter(email: 'customer@example.com');
  {{-- __result.success == true, __result.message == "Subscribed successfully" --}}
@endcode

{{-- Subscribe with optional name --}}
@code
  __result = cw_subscribe_newsletter(
    email: 'john@example.com',
    name: 'John Doe'
  );
  {{-- __result.success == true --}}
@endcode

{{-- Display a simple newsletter subscription form --}}
<form method="post" action="#">
  <input type="hidden" name="csrf_token" value="{ cw_csrf_token() }">
  <div class="newsletter-form">
    <input type="email" name="email" placeholder="Your email address" required>
    <input type="text" name="name" placeholder="Your name (optional)">
    <button type="submit">Subscribe</button>
  </div>
</form>

{{-- On form submission, call cw_subscribe_newsletter --}}
@if(__form_submitted)
  @code
    __newsletter_result = cw_subscribe_newsletter(
      email: __form_email,
      name: __form_name
    );
  @endcode
  @if(__newsletter_result.success)
    <p class="success">Thank you for subscribing!</p>
  @else
    <p class="error">{ __newsletter_result.message }</p>
  @endif
@endif

The cw_subscribe_newsletter() function accepts the following parameters:

ParameterTypeRequiredDescription
emailstringYesSubscriber email address
namestringNoSubscriber name (optional)

The function returns an object with the following properties:

PropertyTypeDescription
__result.successboolWhether the subscription was successful
__result.messagestringStatus message

Customer Account Functions

Customer account functions provide a complete authentication and account management system for your storefront templates. All account functions require the customer to be authenticated via password-based login or OTP verification. The customer session token is automatically passed when rendering account templates.

Authentication Flow

The system supports password-based authentication. The flow is:

  1. Call cw_login_user(email, password, redirect_url?) — authenticates with email and password. Optionally pass redirect_url to specify where the customer should be redirected after successful login
  2. Call cw_register_user(first_name, last_name, email, password, phone?, country_id?, region_id?, city_id?, address?, zip_code?, redirect_url?) — creates a new customer account with email and password
  3. Once authenticated, call any customer account function (orders, wishlist, profile, etc.). The session token is automatically stored in the template context
  4. Call cw_logout_user() to end the session
CODE
{{-- Step 1: Login with email and password (optional OTP) --}}
@code
  __result = cw_login_user(
    email: 'customer@example.com',
    password: 'secure-password',
    redirect_url: '/account/orders',
    send_login_otp: true,      {{-- optional: send OTP code via email --}}
    otp_required: false         {{-- optional: require OTP verification --}}
  );
  {{-- Password mode returns: { "token": "jwt...", "customer": {...}, "summary": {...}, "redirectUrl": "/account/orders" } --}}
  {{-- OTP mode returns: { "otpSent": true, "otpToken": "challenge...", "token": null } --}}
  __session_token = __result.token;
@endcode

{{-- Step 2: If OTP was sent, verify it --}}
@code
  __session = cw_verify_login(challenge_token: __result.otpToken, code: '123456');
  {{-- Returns: { "token": "jwt...", "customer": {...}, "summary": {...}, "redirectUrl": "/account/orders" } --}}
  {{-- The session token is now stored and available for subsequent calls --}}
@endcode

{{-- Step 3: Access customer data --}}
@code
  __profile = cw_get_customer_profile();
  __orders = cw_get_customer_orders(page: 1, limit: 10);
  __wishlist = cw_get_customer_wishlist();
@endcode

{{-- Step 4: Logout --}}
@code
  __result = cw_logout_user();
  {{-- Clears the customer session --}}
@endcode
Session Persistence: Once authenticated, the session token is automatically included when rendering account templates (account.dashboard, account.orders, etc.). All customer account cw_* functions use this session context to identify the customer. You do not need to pass the token manually.

Post-Auth Redirect

When using custom login/register templates, you can control where the customer is redirected after successful authentication. The system supports several approaches:

  1. Client-side redirect (AJAX mode): Call cw_login_user(email, password, redirect_url) or cw_register_user(first_name, last_name, email, password, ..., redirect_url) with a redirect_url parameter. The response includes a redirectUrl field that your frontend JS can use to navigate the customer (e.g., window.location.href = result.redirectUrl).
  2. Server-side redirect (POST form mode): Submit the login/register/verify forms to the POST endpoints (/templates/auth/login, /templates/auth/register, /templates/auth/verify). The response includes a redirectUrl field, and the server sets a session cookie so the next page load recognizes the authenticated customer.
  3. Meta-refresh redirect: Use the cw_auth_redirect(url?) function to generate an HTML snippet that automatically redirects the browser. If no URL is provided, it uses the redirect_url previously set during login/register, falling back to /account. This is useful for non-JS fallback scenarios.
CODE
{{-- Example: Meta-refresh redirect after successful verification --}}
@code
  __session = cw_verify_login(
    challenge_token: __challenge_token,
    code: '123456',
    redirect_url: '/account/orders'
  );
@endcode

{{-- If verification was successful, output a redirect --}}
@if(__session.token)
  {!! cw_auth_redirect() !!}
  {{-- Generates: <meta http-equiv="refresh" content="0;url=/account/orders"> --}}
  {{-- Plus a JavaScript fallback: <script>location.href='/account/orders'</script> --}}
@else
  <p>Invalid code. Please try again.</p>
@endif
Redirect priority: If redirect_url is passed to cw_verify_login(), it takes highest priority. Otherwise, it falls back to the redirect_url passed to cw_login_user() or cw_register_user(). If neither is provided, cw_auth_redirect() defaults to /account.

Registration Flow

CODE
{{-- Register a new customer with password and optional email verification --}}
@code
  __result = cw_register_user(
    first_name: 'John',
    last_name: 'Doe',
    email: 'john@example.com',
    password: 'secure-password',
    phone: '+2348012345678',            {{-- optional --}}
    country_id: 1,                      {{-- optional --}}
    region_id: 1,                       {{-- optional --}}
    city_id: 1,                         {{-- optional --}}
    address: '123 Main Street',         {{-- optional --}}
    zip_code: '10001',                  {{-- optional --}}
    redirect_url: '/account',           {{-- optional: redirect after login --}}
    send_welcome_mail: true,            {{-- optional: send welcome email --}}
    send_mail_verification_mail: true,  {{-- optional: send verification email --}}
    mail_verification_required: true    {{-- optional: require email verification --}}
  );
  {{-- Returns: { "token": "jwt...", "customer": {...}, "summary": {...}, "redirectUrl": "/account" } --}}
  {{-- If mail_verification_required is true, token is null until verified --}}
  __session_token = __result.token;
@endcode

{{-- Verify email to complete registration --}}
@code
  __session = cw_verify_register(
    email: 'john@example.com',
    verification_token: 'token-from-email',
    redirect_url: '/account'
  );
  {{-- Customer is now registered, email verified, and logged in --}}
  {{-- Response includes redirectUrl if redirect_url was provided --}}
@endcode

Customer Profile & Dashboard

CODE
{{-- Get customer profile with summary --}}
@code
  __profile = cw_get_customer_profile();
  {{-- Returns: { "customer": {...}, "summary": {...} } --}}
  __customer = __profile.customer;
  __summary = __profile.summary;
@endcode

<h1>Welcome, { __customer.first_name } { __customer.last_name }</h1>
<p>Email: { __customer.email }</p>
<p>Phone: { __customer.phone }</p>
<p>Total Orders: { __summary.total_orders }</p>
<p>Total Spend: { cw_format_money(__summary.total_spend, __summary.currency) }</p>

{{-- Get customer dashboard with aggregated data --}}
@code
  __dashboard = cw_get_customer_dashboard();
  {{-- Returns: { "summary": {...}, "wishlist": [...], "returns": [...], "reviews": [...], "spendChart": [...] } --}}
  __dash_summary = __dashboard.summary;
@endcode

{{-- Update customer profile --}}
@code
  __updated = cw_update_customer_profile(
    first_name: 'John',
    last_name: 'Updated',
    phone: '+2348098765432',
    zip_code: '10001',
    billing_address: '456 New Street, Lagos',
    shipping_address: '456 New Street, Lagos'
  );
@endcode

Customer Object

Returned as __profile.customer from cw_get_customer_profile() and __session.customer from cw_verify_login().

PropertyTypeDescription
__customer.idintCustomer ID
__customer.first_namestringCustomer first name
__customer.last_namestringCustomer last name
__customer.other_namesstring?Customer other/middle names (optional)
__customer.emailstringCustomer email address
__customer.phonestring?Customer phone number
__customer.zip_codestring?Customer zip/postal code
__customer.billing_addressstring?Billing address
__customer.shipping_addressstring?Shipping address
__customer.extra_infoCollectionAdditional custom fields (title, value pairs)
__customer.profile_picture_asset_idint?Media Library asset ID for the customer's profile picture
__customer.profile_picture_urlstring?Public URL of the customer's profile picture (resolved from Media Library)
__customer.country_idint?Country ID for geographic location
__customer.region_idint?Region/State ID for geographic location
__customer.city_idint?City ID for geographic location
__customer.country_idint?Country ID for geographic location
__customer.region_idint?Region/State ID for geographic location
__customer.city_idint?City ID for geographic location
__customer.country_idint?Country ID for geographic location
__customer.region_idint?Region/State ID for geographic location
__customer.city_idint?City ID for geographic location

Customer Summary Object

Returned as __profile.summary from cw_get_customer_profile() and __session.summary from cw_verify_login().

PropertyTypeDescription
__summary.total_ordersintTotal number of orders placed
__summary.paid_ordersintNumber of paid/completed orders
__summary.total_spenddecimalTotal amount spent across all orders
__summary.currencystringCurrency code (e.g., NGN)
__summary.latest_order_atstring?Date/time of most recent order

Profile Editing

Customers can edit their profile details (first name, last name, phone, billing address, shipping address) and manage custom extra info fields from their account dashboard. All profile editing functions require an active customer session.

CODE
{{-- Display current profile in an editable form --}}
@code
  __profile = cw_get_customer_profile();
  __customer = __profile.customer;
@endcode

@is_logged_in
  <form class="profile-form" method="post" action="@link('/account/profile/update')">
    
<div class="form-group"> <label>Phone Number</label> <input type="tel" name="phone" value="{ __customer.phone }"> </div> <div class="form-group"> <label>Billing Address</label> <textarea name="billing_address" rows="3">{ __customer.billing_address }</textarea> </div> <div class="form-group"> <label>Shipping Address</label> <textarea name="shipping_address" rows="3">{ __customer.shipping_address }</textarea> </div> <button type="submit" class="btn btn--primary">Save Profile</button> </form> {{-- Or use the cw function directly in @code blocks --}} @code __updated = cw_update_customer_profile( first_name: 'John', last_name: 'Updated', phone: '+2348098765432', zip_code: '10001', billing_address: '456 New Street, Lagos', shipping_address: '456 New Street, Lagos' ); {{-- __updated.success == true --}} @endcode {{-- Update profile with custom extra info fields --}} @code __updated = cw_update_customer_profile( first_name: 'John', last_name: 'Doe', phone: '+2348012345678', zip_code: '10001', extra_info: '[{"title": "Company", "value": "Acme Ltd"}, {"title": "Tax ID", "value": "TX12345"}]' ); {{-- extra_info is a JSON array of {title, value} objects --}} @endcode {{-- Update profile picture using a Media Library asset ID --}} @code __updated = cw_update_customer_profile( profile_picture_asset_id: 42 ); {{-- __updated.customer.profile_picture_url will contain the resolved URL --}} {{-- Pass 0 or omit to leave unchanged, set to a valid Media Library asset ID to update --}} @endcode @else <p>Please <a href="@link('/account/login')">log in</a> to edit your profile.</p> @endis_logged_in

Address Management

Customers can manage their billing and shipping addresses independently or together. Use cw_update_customer_address(type, address) for convenient address-only updates.

CODE
{{-- Update billing address only --}}
@code
  __result = cw_update_customer_address(
    type: 'billing',
    address: '123 Main Street, Ikeja, Lagos'
  );
  {{-- __result.success == true --}}
@endcode

{{-- Update shipping address only --}}
@code
  __result = cw_update_customer_address(
    type: 'shipping',
    address: '456 Warehouse Road, Apapa, Lagos'
  );
  {{-- __result.success == true --}}
@endcode

{{-- Update both addresses to the same value --}}
@code
  __result = cw_update_customer_address(
    type: 'both',
    address: '789 New Home Ave, Victoria Island, Lagos'
  );
  {{-- __result.success == true --}}
@endcode

{{-- Or use the full profile function for combined updates --}}
@code
  __result = cw_update_customer_profile(
    zip_code: '10001',
    billing_address: '123 Main Street, Ikeja, Lagos',
    shipping_address: '456 Warehouse Road, Apapa, Lagos'
  );
  {{-- __result.success == true --}}
@endcode

Addresses are stored as free-text fields on the customer record. When an order is placed, the customer's current billing and shipping addresses are captured at checkout. The cw_get_customer_profile() function returns the current addresses via __customer.billing_address and __customer.shipping_address.

Orders

CODE
{{-- Get paginated orders --}}
@code
  __result = cw_get_customer_orders(page: 1, limit: 10);
  __orders = __result.orders;       {{-- PagedResponse with items, totalCount, page, pageSize --}}
  __summary = __result.summary;     {{-- Customer summary object --}}
@endcode

@if(cw_count(var: __orders.items) > 0)
  <table class="orders-table">
    <thead>
      <tr><th>Order #</th><th>Date</th><th>Status</th><th>Total</th><th></th></tr>
    </thead>
    <tbody>
      @foreach(__orders.items as __order)
      <tr>
        <td>{ __order.reference }</td>
        <td>{ cw_format_date(__order.created_at) }</td>
        <td><span class="badge badge-{ __order.status }">{ __order.status }</span></td>
        <td>{ cw_format_money(__order.amount, __order.currency_code) }</td>
        <td><a href="?order={ __order.id }">View</a></td>
      </tr>
      @endforeach
    </tbody>
  </table>

  {{-- Pagination --}}
  <div class="pagination">
    @if(__orders.page > 1)
      <a href="?page={ __orders.page - 1 }">Previous</a>
    @endif
    <span>Page { __orders.page } of { ceil(__orders.totalCount / 10) }</span>
    @if(__orders.page * 10 < __orders.totalCount)
      <a href="?page={ __orders.page + 1 }">Next</a>
    @endif
  </div>
@else
  <p>No orders found.</p>
@endif

{{-- Get a single order by ID --}}
@code
  __order = cw_get_customer_order(order_id: 123);
  {{-- Returns a single Order object or null --}}
@endcode

Order Object

Returned by cw_get_customer_orders() (as items) and cw_get_customer_order().

PropertyTypeDescription
__order.idintOrder/checkout ID
__order.referencestringOrder reference number
__order.statusstringOrder status (Pending, Paid, Failed, etc.)
__order.currency_codestringCurrency code (e.g., NGN)
__order.amountdecimalTotal order amount paid by customer (includes subtotal, discount, delivery, tax, processing fee, and regulatory taxes)
__order.fee_amountdecimal?Transaction/payment fee charged to customer (or absorbed by seller if PassProcessingFeeToCustomer is false)
__order.net_amountdecimal?Net amount after fees (merchant receives this after all fees and regulatory charges)
__order.grand_totaldecimal?Full grand total: subtotal - discount + delivery + tax + processing fee + processing fee tax + shipping fee tax
__order.processing_feedecimal?Processing/gateway fee amount (before regulatory charge)
__order.processing_fee_taxdecimal?Regulatory charge (7.5%) on the processing fee. Hidden from customer breakdown, deducted from merchant revenue
__order.shipping_fee_taxdecimal?Regulatory charge (7.5%) on the shipping/delivery fee. Hidden from customer breakdown, deducted from merchant revenue
__order.customer_namestringCustomer name on order
__order.customer_emailstringCustomer email on order
__order.customer_phonestring?Customer phone on order
__order.customer_addressstring?Customer address on order
__order.created_atstringOrder creation date/time
__order.paid_atstring?Payment completion date
__order.delivery_statusstring?Delivery status (Pending, Shipped, Delivered)
__order.delivered_atstring?Delivery completion date
__order.itemsCollectionOrder line items (products with quantity, price)
__order.item_countintNumber of items in the order
__order.lifecycleobjectOrder lifecycle phases (pending, confirmed, paid, fulfilled, delivered)
__order.has_invoiceboolWhether an invoice exists for this order
__order.invoice_idint?Invoice ID if generated
__order.invoice_referencestring?Invoice reference number
__order.invoice_htmlstring?Rendered invoice HTML
__order.document_countintNumber of documents attached to this order
__order.documentsCollectionOrder documents (receipts, invoices, contracts)
__order.return_request_countintNumber of return requests for this order
__order.returnsCollectionReturn requests for this order
__order.payment_referencestring?Payment gateway reference

Invoices

CODE
{{-- Get paginated invoices (orders with generated invoices) --}}
@code
  __result = cw_get_customer_invoices(page: 1, limit: 10);
  __invoices = __result.orders;     {{-- or __result.items --}}
@endcode

@foreach(__invoices.items as __invoice)
  <div class="invoice-row">
    <span>{ __invoice.invoice_reference }</span>
    <span>{ cw_format_money(__invoice.amount, __invoice.currency_code) }</span>
    <span>{ cw_format_date(__invoice.created_at) }</span>
    @if(__invoice.invoice_html)
      <a href="?download_invoice={ __invoice.id }">Download</a>
    @endif
  </div>
@endforeach

Each invoice item in the collection has the same properties as an Order object, plus invoice-specific fields (invoice_id, invoice_reference, invoice_html, has_invoice).

Receipts

CODE
{{-- Get paginated receipts (paid orders) --}}
@code
  __result = cw_get_customer_receipts(page: 1, limit: 10);
  __receipts = __result.orders;     {{-- or __result.items --}}
@endcode

@foreach(__receipts.items as __receipt)
  <div class="receipt-row">
    <span>{ __receipt.reference }</span>
    <span>{ cw_format_money(__receipt.amount, __receipt.currency_code) }</span>
    <span>{ cw_format_date(__receipt.paid_at) }</span>
  </div>
@endforeach

Each receipt item in the collection has the same properties as an Order object (filtered to paid orders only).

Documents

CODE
{{-- Get documents for a specific order --}}
@code
  __documents = cw_get_customer_documents(order_id: 123);
@endcode

{{-- Get documents across all orders --}}
@code
  __all_docs = cw_get_customer_documents();
@endcode

@foreach(__all_docs as __doc)
  <div class="document-row">
    <span>Order #{ __doc.order_reference }</span>
    <span>{ __doc.document_type }</span>
    <span>{ cw_format_date(__doc.created_at) }</span>
    @if(__doc.rendered_html)
      <a href="?view_doc={ __doc.document_id }">View</a>
    @endif
  </div>
@endforeach

Each document object has the following accessors:

PropertyTypeDescription
__doc.order_idintOrder/checkout ID
__doc.order_referencestringOrder reference number
__doc.document_idintDocument ID
__doc.document_typestringDocument type (receipt / invoice / contract)
__doc.rendered_htmlstring?Rendered document HTML
__doc.file_urlstring?Document file URL (PDF or other format)
__doc.created_atstringDocument creation date

Wishlist

CODE
{{-- Get wishlist items --}}
@code
  __wishlist = cw_get_customer_wishlist();
@endcode

@if(cw_count(var: __wishlist) > 0)
  <div class="wishlist-grid">
    @foreach(__wishlist as __item)
      <div class="wishlist-item">
        @if(__item.product_image)
          <img src="{ __item.product_image }" alt="{ __item.product_name }" />
        @endif
        <h3>{ __item.product_name }</h3>
        <p>{ cw_format_money(__item.product_price) }</p>
        <p>Added: { cw_format_date(__item.added_at) }</p>
        <button onclick="cw_add_to_cart({ __item.product_id })">Add to Cart</button>
        <button onclick="cw_remove_from_wishlist({ __item.product_id })">Remove</button>
      </div>
    @endforeach
  </div>
@else
  <p>Your wishlist is empty.</p>
@endif

{{-- Add a product to wishlist --}}
@code
  __result = cw_add_to_customer_wishlist(product_id: 42);
  {{-- __result.success == true --}}
@endcode

{{-- Remove a product from wishlist --}}
@code
  __result = cw_remove_from_customer_wishlist(product_id: 42);
  {{-- __result.success == true --}}
@endcode

Each wishlist item has the following accessors:

PropertyTypeDescription
__item.idintWishlist item ID
__item.product_idintProduct ID
__item.product_namestringProduct name
__item.product_imagestring?Product image URL
__item.product_pricedecimalProduct price
__item.added_atstringDate added to wishlist

Returns

CODE
{{-- Get paginated return requests --}}
@code
  __result = cw_get_customer_returns(page: 1, limit: 10);
  __returns = __result.items;       {{-- or __result.returns --}}
@endcode

@foreach(__returns as __return)
  <div class="return-row">
    <span>Order #{ __return.order_reference }</span>
    <span>{ __return.product_name }</span>
    <span>Reason: { __return.reason }</span>
    <span class="badge badge-{ __return.status }">{ __return.status }</span>
    <span>{ cw_format_date(__return.created_at) }</span>
  </div>
@endforeach

{{-- Create a return request --}}
@code
  __result = cw_create_customer_return(
    order_id: 123,
    reason: 'Product is damaged',
    product_id: 42      {{-- optional, for specific item returns --}}
  );
  {{-- __result.success == true --}}
@endcode

{{-- Cancel a return request --}}
@code
  __result = cw_cancel_return_request(
    return_id: 7
  );
  {{-- __result.success == true --}}
@endcode

Each return request has the following accessors:

PropertyTypeDescription
__return.idintReturn request ID
__return.checkout_idintOrder/checkout ID
__return.order_referencestringOrder reference number
__return.product_idint?Product ID (if item-specific)
__return.product_namestring?Product name
__return.reasonstringReturn reason
__return.statusstringReturn status (Pending, Approved, Declined, Refunded)
__return.created_atstringRequest creation date
__return.updated_atstring?Last update date

Reviews

CODE
{{-- Get paginated customer reviews --}}
@code
  __result = cw_get_customer_reviews(page: 1, limit: 10);
  __reviews = __result.items;       {{-- or __result.reviews --}}
@endcode

@foreach(__reviews as __review)
  <div class="review-item">
    <h4>{ __review.product_name }</h4>
    <div class="rating">
      @for(__i=0; __i<__review.rating; __i++)★ @endfor
      @for(__i=__review.rating; __i<5; __i++)☆ @endfor
    </div>
    <p>{ __review.review_text }</p>
    <span>{ cw_format_date(__review.created_at) }</span>
  </div>
@endforeach

Each review has the following accessors:

PropertyTypeDescription
__review.idintReview ID
__review.product_idintProduct ID
__review.product_namestringProduct name
__review.ratingintRating (1-5)
__review.review_textstring?Review content text
__review.created_atstringReview submission date

Dashboard Object

Returned by cw_get_customer_dashboard().

PropertyTypeDescription
__dashboard.summaryobjectCustomer summary (total_orders, paid_orders, total_spend, currency, latest_order_at)
__dashboard.wishlistCollectionWishlist items
__dashboard.returnsCollectionReturn requests
__dashboard.reviewsCollectionCustomer reviews
__dashboard.spend_chartCollectionSpending chart data (period, amount pairs)

Filter Reference

Filters transform content inline using pipe syntax:

CODE
{{-- String filters --}}
@code echo '1500.00' | money; @endcode       {{-- "NGN 1,500.00" --}}
@code echo '2024-01-15' | date; @endcode      {{-- "Jan 15, 2024" --}}
@code echo 'Hello World' | slugify; @endcode  {{-- "hello-world" --}}
@code echo __post.content | truncate; @endcode
@code echo __product.title | default('Untitled'); @endcode

{{-- Filters with arguments --}}
@code echo __store.price | money(currency: 'USD'); @endcode
@code echo '2024-01-15' | date(format: 'Y-m-d'); @endcode
@code echo __post.excerpt | truncate(length: 50, ellipsis: '[...]'); @endcode
FilterArgumentsDescription
| moneycurrency (default: NGN)Format amount as currency string
| dateformat (default: M d, Y)Format date string
| slugify—Convert to URL-safe slug
| truncatelength (default: 100), ellipsis (default: ...)Truncate text
| defaultdefaultFallback if empty

Email Templates #

The system includes configurable email templates for automated customer communications. These templates can be customized per institution from StoreFront > Mail Templates in the admin panel. Each template uses bracket-style variables (e.g., [storeName]) that are replaced with actual values when the email is sent.

If a template is not customized at the institution level, the system falls back to the default template defined globally. Leave a field empty in the admin editor to restore the system default.

Available Email Templates

Template KeyPurposeAvailable Variables
storefront.welcome Welcome email sent to new customers after registration when send_welcome_mail: true [storeName], [customerName]
storefront.email_verification Verification email sent when send_mail_verification_mail: true. Contains a token the customer must use with cw_verify_register(token) [storeName], [customerName], [verificationToken], [verificationUrl], [expiresInMinutes]
storefront.login_otp One-time passcode email sent when send_login_otp: true. Used with cw_verify_login(otp_token, code) [storeName], [customerName], [otpCode], [expiresInMinutes]
storefront.order_confirmation Sent to customers when an order is confirmed [storeName], [customerName], [orderReference], [orderTotal], [orderItems]
storefront.order_delivery Sent when an order is marked as delivered [storeName], [customerName], [orderReference]
storefront.order_return Sent when a return request is received [storeName], [customerName], [returnReference]
storefront.shipment_update Sent when there is a tracking/shipment update on an order [storeName], [customerName], [orderReference], [trackingUrl]

Template Variables

All email templates use bracket-style variable placeholders. The following variables are available:

VariableDescription
[storeName]The store/institution display name
[customerName]The customer's full name
[verificationToken]Email verification token (used with cw_verify_register)
[verificationUrl]Full verification URL including the token
[expiresInMinutes]Token/code expiry duration in minutes
[otpCode]One-time passcode for login
[orderReference]Order reference number
[orderTotal]Order total amount
[orderItems]HTML list of order line items
[returnReference]Return request reference number
[trackingUrl]Shipment tracking URL
CODE
{{-- Example: sending a welcome email during registration --}}
@code
  __result = cw_register_user(
    first_name: 'Jane',
    last_name: 'Doe',
    email: 'jane@example.com',
    password: 'secure-password',
    send_welcome_mail: true,
    send_mail_verification_mail: true,
    mail_verification_required: true
  );
  {{-- If mail_verification_required is true, token is null until verified --}}
  {{-- __result.emailVerificationRequired == true --}}
@endcode
CODE
{{-- Example: verifying email with token --}}
@code
  __result = cw_verify_register(token: 'abc123def456...');
  {{-- On success: { "verified": true, "message": "Email verified successfully.", "email": "jane@example.com" } --}}
  {{-- On failure: { "verified": false, "error": "Invalid or expired verification token." } --}}
@endcode
CODE
{{-- Example: login with OTP via email --}}
@code
  __result = cw_login_user(
    email: 'jane@example.com',
    password: 'secure-password',
    send_login_otp: true,
    otp_required: true
  );
  {{-- OTP mode returns: { "otpSent": true, "otpToken": "challenge...", "token": null } --}}
  {{-- Pass the otpToken to cw_verify_login with the code from email --}}
@endcode

{{-- Then in a separate step, verify the OTP code --}}
@code
  __session = cw_verify_login(
    challenge_token: __otpToken,
    code: '123456'
  );
  {{-- __session.token contains the JWT session --}}
@endcode

Admin Configuration

Navigate to StoreFront > Mail Templates in the admin dashboard to customize any email template for your institution. Changes are saved per institution and override the global defaults.

Each template editor provides:

  • Subject — A text field for the email subject line
  • Body (HTML) — An HTML editor for the email body content
  • Available variables — Displayed below each editor for reference

Route Context V2 #

The storefront runtime injects a route object and current-entity helpers before rendering a .cw template. Use this when a template needs to know which product, category, blog, post, page, or account section is being rendered.

RouteKindInjected VariablesDefault Template
/catalogue or page__route.kindcatalogue.home / configured homepage
/pages/{pageHandle}page__route.pageHandle, __pagepage.{handle} then page.default
/{productBase}/{id|sku|slug}product__route.productSlug, __productItem TemplateKey, Storefront Overview default, then product.detail
/{categoryArchiveBase}/{category}catalogue__route.categorySlug, __categorycatalogue.home
/{blogBase}blogs__route.kindblogs.default
/{blogBase}/{blogHandle}blog__route.blogHandle, __blogBlog TemplateKey, Storefront Overview default, then blog.default
/{blogBase}/{blogHandle}/{postHandle}post__route.blogHandle, __route.postHandle, __blog, __postPost TemplateKey, Storefront Overview default, then post.default

Current Route Functions

HTML
@code
  __route = cw_current_route();
  __product_id = cw_current_product_id();
  __product_slug = cw_current_product_slug();
  __category_slug = cw_current_category_slug();
  __blog_id = cw_current_blog_id();
  __blog_slug = cw_current_blog_slug();
  __post_id = cw_current_post_id();
  __post_slug = cw_current_post_slug();
@endcode

@if(__route.kind == 'product')
  <p>Rendering product {{ __product_slug }}</p>
@endif
FunctionReturnsDescription
cw_current_route()objectReturns kind, productSlug, categorySlug, blogSlug, postSlug, and related route fields.
cw_current_product_id()stringCurrent product ID when the product was resolved.
cw_current_product_slug()stringThe product URL segment. Product URLs accept numeric ID, SKU, or slugified product name.
cw_current_category_slug()stringCurrent category route segment.
cw_current_blog_id()stringCurrent blog ID when available.
cw_current_blog_slug() / cw_current_blog_handle()stringCurrent blog handle from the URL.
cw_current_post_id()stringCurrent blog post ID when available.
cw_current_post_slug() / cw_current_post_handle()stringCurrent post handle from the URL.

Single Product, Blog & Post Templates V2 #

Single entity routes are backend-rendered by the .cw engine and also work through the current React storefront compatibility renderer. A product, blog, or post can use its own TemplateKey; otherwise the runtime falls back to the merchant-selected default templates in Storefront → Overview → Default Theme Templates, then the theme manifest's optional recommended assignments, then the conventional default template key.

Multiple detail templates are supported. A theme can ship product.detail.cw, product.minimal.cw, post.editorial.cw, blog.magazine.cw, and any other template keys listed by the active theme runtime. Merchants choose the storefront-wide product, blog archive, and blog post defaults in Storefront → Overview; individual products, blogs, and posts can still override that with their own TemplateKey.

Single Product Page

Create one or more product detail templates such as templates/product.detail.cw and templates/product.compact.cw, then set the storefront default in Storefront → Overview. The route is /{productBase}/{id|sku|slugified-name}, where productBase comes from Storefront Appearance → Permalinks. Because products do not currently store a dedicated slug column, the runtime resolves by numeric ID, exact SKU, slugified SKU, or slugified product name.

HTML
{{-- templates/product.detail.cw --}}
@extends('layouts/main')

@section('content')
  @code
    __product = cw_get_product(slug: cw_current_product_slug());
  @endcode

  @isset(__product)
    <article class="product-detail">
      <img src="{{ __product.image }}" alt="{{ __product.title }}">
      <h1>{{ __product.title }}</h1>
      <p>{{ cw_format_money(amount: __product.price) }}</p>
      <div>{{ __product.description }}</div>

      @query(__related, ['type' => 'products', 'category' => __product.categories.0.slug, 'limit' => 4])
        @each('product-card', __related, 'product')
      @endquery
    </article>
  @else
    @include('not-found')
  @endisset
@endsection

Single Blog Archive

Create one or more blog archive templates such as templates/blog.default.cw and templates/blog.magazine.cw, then set the storefront default in Storefront → Overview. The route is /{blogBase}/{blogHandle}, where blogBase comes from Storefront Appearance → Permalinks. The current blog is injected as __blog; you can also fetch it with cw_get_blog(slug: cw_current_blog_slug()).

HTML
{{-- templates/blog.default.cw --}}
@extends('layouts/main')

@section('content')
  <h1>{{ __blog.name }}</h1>

  @query(__posts, ['type' => 'posts', 'blog' => cw_current_blog_slug(), 'limit' => 12, 'orderby' => 'published_at', 'order' => 'desc'])
    @each('post-card', __posts, 'post')
  @else
    <p>No posts have been published in this blog yet.</p>
  @endquery
@endsection

Single Blog Post

Create one or more post templates such as templates/post.default.cw and templates/post.editorial.cw, then set the storefront default in Storefront → Overview. The route is /{blogBase}/{blogHandle}/{postHandle}. The current post is injected as __post and the parent blog as __blog.

HTML
{{-- templates/post.default.cw --}}
@extends('layouts/main')

@section('content')
  @code
    __post = cw_get_post(slug: cw_current_post_slug(), blog: cw_current_blog_slug());
  @endcode

  <article class="blog-post">
    @code
      __paths = cw_base_paths();
    @endcode

    <p><a href="{{ __paths.blog }}/{{ cw_current_blog_slug() }}">{{ __blog.name }}</a></p>
    <h1>{{ __post.title }}</h1>
    <time>{{ cw_format_date(date: __post.published_at, format: 'M d, Y') }}</time>
    <div class="post-content">{{ __post.content }}</div>
  </article>
@endsection

Single Lookup Functions

FunctionLookup ArgumentsReturns
cw_get_product()id, productId, slug, handle, skuOne product object or null.
cw_get_category()id, categoryId, slug, codeOne category object or null.
cw_get_blog()id, blogId, slug, handleOne published blog object or null.
cw_get_post()id, postId, slug, handle, optional blog/blog_idOne published post object or null.

Filtering & Sorting Products, Blogs, Posts & Categories V2 #

Collection filters work in both @query and the matching cw_get_*() functions. Parameters can be passed with PHP-style arrays, named arguments, or colon arguments.

Products

HTML
@query(__products, [
  'type' => 'products',
  'category' => 'clothing',
  'tag' => 'sale,featured',
  'brand' => 'CoreWave',
  'min_price' => 1000,
  'max_price' => 50000,
  'in_stock' => true,
  'on_sale' => true,
  'orderby' => 'price',
  'order' => 'asc',
  'limit' => 24
])
  @each('product-card', __products, 'product')
@endquery
Product FilterDescription
category, category_idMatches category ID, code, exact name, slugified code, or slugified name.
tag, tagsMatches one or more product tag slugs. Comma-separated values are accepted.
ids, idComma-separated product IDs.
q, searchSearches name, description, SKU, brand, and manufacturer.
brandExact brand match.
product_typeMatches product type enum text such as Goods or Service.
min_price, max_priceUnit price range.
in_stock, on_sale, featuredBoolean filters. featured maps to on-sale/discounted products in the current runtime.
orderbyname, title, sku, brand, price, stock, stock_quantity, created_at, updated_at.

Categories

HTML
@query(__categories, ['type' => 'categories', 'active' => true, 'parent_id' => 0, 'orderby' => 'display_order', 'order' => 'asc'])
  @foreach(__categories as __category)
    <a href="/category/{{ __category.slug }}">{{ __category.name }}</a>
  @endforeach
@endquery
Category FilterDescription
q, searchSearches name, description, and code.
codeExact category code match.
parent_idLimits results to children of a parent category.
activeBoolean active/inactive filter.
orderbyname, display_order, or created_at.

Blogs & Posts

HTML
@query(__blogs, ['type' => 'blogs', 'q' => 'news', 'orderby' => 'published_at', 'order' => 'desc'])
  @each('blog-card', __blogs, 'blog')
@endquery

@query(__posts, ['type' => 'posts', 'blog' => 'news', 'tag' => 'announcement', 'orderby' => 'published_at', 'order' => 'desc'])
  @each('post-card', __posts, 'post')
@endquery
Blog/Post FilterDescription
blogs: q/searchSearches blog name, handle, and description.
blogs: statuspublished by default. Use any to include all statuses in trusted/admin previews.
blogs: orderbyname, published_at, or created_at.
posts: blog, blogHandle, blog_idFilters posts by parent blog handle/name/ID.
posts: tag, tagsFilters by one or more tag slugs.
posts: ids, idComma-separated post IDs.
posts: q/searchSearches title, excerpt, and content JSON.
posts: orderbytitle, published_at, created_at, or updated_at.

Template Examples #

Product Card Partial

File: sections/product-card.cw

HTML
{{--
  Template Part: Product Card
  Usage: @include('product-card', ['product' => __product])
--}}
<div class="product-card">
  {{-- Sale badge --}}
  @if(__product.on_sale)
    <span class="product-card__badge">Sale!</span>
  @endif

  <a href="{{ __product.url }}" class="product-card__image">
    @has_image(__product)
      @image(__product, 'medium')
    @else
      <div class="product-card__placeholder">No Image</div>
    @endis
  </a>

  <div class="product-card__info">
    <h3><a href="{{ __product.url }}">@code echo __product.title; @endcode</a></h3>

    {{-- Star rating --}}
    @if(__product.rating > 0)
    <div class="product-card__rating">
      @for(__i=0; __i<5; __i++)
        @if(__i < cw_count(var: __product.rating))
          <span class="star star--filled">★</span>
        @else
          <span class="star star--empty">☆</span>
        @endif
      @endfor
      <span class="rating-count">(@code echo __product.review_count; @endcode)</span>
    </div>
    @endif

    <div class="product-card__price">
      @if(__product.on_sale && __product.compare_price)
        <span class="price--compare">@code echo cw_format_money(amount: __product.compare_price); @endcode</span>
      @endif
      <span class="price--current">@code echo cw_format_money(amount: __product.price); @endcode</span>
    </div>
  </div>
</div>

Cart Page Template

File: templates/cart.cw

HTML
{{--
  Template Name: Cart Page
  Template Key: cart.default
--}}

@include('header')

<div class="cart-page">
  <div class="container">
    <h1>@code echo __('Shopping Cart'); @endcode</h1>

    {{-- Cart summary from session --}}
    <div class="cart-summary">
      <p>
        @code echo __('Items in cart:'); @endcode
        <strong>@code echo cw_get_cart_count(); @endcode</strong>
      </p>

      {{-- Applied discounts --}}
      @code
        __applied_discounts = cw_get_cart_applied_discounts();
      @endcode
      @if(cw_count(var: 'applied_discounts') > 0)
        <div class="cart-discounts">
          <h3>@code echo __('Applied Discounts'); @endcode</h3>
          @foreach(__applied_discounts as __discount)
            <div class="cart-discount">
              <span>@code echo __discount.code; @endcode</span>
              <span>-@code echo __discount.display_name; @endcode</span>
            </div>
          @endforeach
        </div>
      @endif
    </div>

    {{-- Featured products --}}
    @query(__featured_products, ['type' => 'products', 'limit' => 4, 'orderby' => 'created_at', 'order' => 'desc'])

    @if(cw_count(var: 'featured_products') > 0)
      <h2>@code echo __('Featured Products'); @endcode</h2>
      <div class="product-grid">
        @foreach(__featured_products as __product)
          @include('product-card', ['product' => __product])
        @endforeach
      </div>
    @else
      <div class="cart-empty">
        <p>@code echo __('Your cart is empty.'); @endcode</p>
        <a href="@link('/shop')" class="btn btn--primary">@code echo __('Continue Shopping'); @endcode</a>
      </div>
    @endif
  </div>
</div>

@include('footer')

Product Detail Page

File: templates/product.detail.cw

HTML
{{--
  Template Name: Product Detail
  Template Key: product.detail
--}}

@code
  __related_products = cw_get_related_products(productId: __product.id, limit: 4);
  __reviews = cw_product_reviews(productId: __product.id, page: 1, limit: 5);
@endcode

@include('header')

<div class="product-detail">
  <div class="container">
    <div class="product-detail__gallery">
      @has_image(__product)
        <div class="product-gallery__main">
          @image(__product, 'large')
        </div>
      @else
        <div class="product-gallery__placeholder">No Image</div>
      @endis

      {{-- Gallery images --}}
      @if(cw_count(var: __product.images) > 1)
        <div class="product-gallery__thumbs">
          @foreach(__product.images as __image)
            <img src="@code echo __image.thumbnail; @endcode"
                 alt="@code echo __image.alt; @endcode"
                 class="@if(__image.is_cover) active @endif">
          @endforeach
        </div>
      @endif
    </div>

    <div class="product-detail__info">
      <h1>@code echo __product.title; @endcode</h1>

      {{-- Star rating --}}
      @if(__product.rating > 0)
      <div class="product-detail__rating">
        @for(__i=0; __i<5; __i++)
          @if(__i < cw_count(var: __product.rating))
            <span class="star star--filled">★</span>
          @else
            <span class="star star--empty">☆</span>
          @endif
        @endfor
        <a href="#reviews">@code echo __product.review_count; @endcode @code echo __('reviews'); @endcode</a>
      </div>
      @endif

      <div class="product-detail__price">
        @if(__product.on_sale && __product.compare_price)
          <span class="price--compare">@code echo cw_format_money(amount: __product.compare_price); @endcode</span>
          <span class="price--current price--sale">@code echo cw_format_money(amount: __product.price); @endcode</span>
          <span class="price--badge">Sale!</span>
        @else
          <span class="price--current">@code echo cw_format_money(amount: __product.price); @endcode</span>
        @endif
      </div>

      <div class="product-detail__description">
        @code echo __product.description; @endcode
      </div>

      <div class="product-detail__actions">
        @hook('product.add_to_cart', __product)
      </div>
    </div>
  </div>
</div>

{{-- Customer Reviews --}}
<section id="reviews" class="product-reviews">
  <div class="container">
    <h2>@code echo __('Customer Reviews'); @endcode
      (@code echo __product.review_count; @endcode)
    </h2>

    @if(cw_count(var: 'reviews') > 0)
      @foreach(__reviews as __review)
        <div class="review">
          <div class="review__rating">
            @for(__i=0; __i<__review.rating; __i++)★ @endfor
            @for(__i=__review.rating; __i<5; __i++)☆ @endfor
          </div>
          <p class="review__comment">@code echo __review.comment; @endcode</p>
          <span class="review__author">— @code echo __review.author; @endcode</span>
          <span class="review__date">@code echo __review.date; @endcode</span>
        </div>
      @endforeach
    @else
      <p>@code echo __('No reviews yet. Be the first to review!'); @endcode</p>
    @endif

    {{-- Submit review form --}}
    <div class="review-form">
      <h3>@code echo __('Write a Review'); @endcode</h3>
      <form method="post" action="@link('/reviews/submit')">
        <input type="hidden" name="product_id" value="@code echo __product.id; @endcode">
        <div class="form-group">
          <label>@code echo __('Rating'); @endcode</label>
          <select name="rating" required>
            <option value="5">★★★★★</option>
            <option value="4">★★★★☆</option>
            <option value="3">★★★☆☆</option>
            <option value="2">★★☆☆☆</option>
            <option value="1">★☆☆☆☆</option>
          </select>
        </div>
        <div class="form-group">
          <label>@code echo __('Review'); @endcode</label>
          <textarea name="comment" rows="4" required></textarea>
        </div>
        <button type="submit" class="btn btn--primary">
          @code echo __('Submit Review'); @endcode
        </button>
      </form>
    </div>
  </div>
</section>

{{-- Related Products --}}
@if(cw_count(var: 'related_products') > 0)
<section class="related-products">
  <div class="container">
    <h2>@code echo __('Related Products'); @endcode</h2>
    <div class="product-grid">
      @each('product-card', __related_products, 'product')
    </div>
  </div>
</section>
@endif

@include('footer')

Assets (CSS / JS / Images) #

Theme assets are organized under the assets/ directory. CSS, JavaScript, images, and fonts are uploaded to object storage during theme installation and served through a public media proxy.

Asset Directory Structure

DirectoryPurpose
assets/css/Stylesheet files (loaded in priority order: bootstrap → font-awesome → animate → style → responsive)
assets/js/JavaScript files (loaded in priority order: jquery → bootstrap → owl-carousel → main)
assets/images/Image files (logo, thumbnails, backgrounds, icons)
assets/fonts/Custom font files (referenced via @font-face in CSS)

Referencing Assets in Templates

HTML
{{-- Using the @asset directive --}}
<img src="@asset('assets/images/logo.png')" alt="Logo">

{{-- Using @css to enqueue stylesheets --}}
@css('assets/css/hero.css')
@css('assets/css/cart.css')

{{-- Using @js to enqueue scripts --}}
@js('assets/js/carousel.js')
@js('assets/js/main.js')

{{-- Using product image property --}}
<img src="@code echo __product.image; @endcode" alt="@code echo __product.title; @endcode">

CSS Loading Priority

CSS files are loaded in priority order to ensure dependencies are satisfied:

PriorityFiles
1 (First)bootstrap.min.css
2font-awesome.css, icofont.css, flaticon.css, themify.css
3animate.css, swiper.css, owl.carousel.css
4magnific-popup.css, jquery-ui.css
5preloader.css
6global.css, header.css, footer.css, style.css
7 (Last)responsive.css

Asset URL Resolution

The @asset() directive resolves relative paths against the theme's asset storage URLs. Relative paths in CSS (e.g., url('../fonts/custom.woff')) are also automatically rewritten to absolute proxy URLs at runtime.

Note: CSS files are fetched and injected as inline <style> tags (not <link>), because the object storage serves CSS with Content-Type: application/octet-stream, which browsers block for <link> elements.

Theme Surfaces #

Theme surfaces represent the different page contexts in which a storefront renders content. Each surface maps to a specific route kind and determines which template key is resolved at runtime.

KeyLabelDescription
homeHomeStorefront landing/index page
pageGeneric PageStandard CMS content pages
blog.archiveBlog ArchiveBlog listing/index page
blog.singleBlog SingleIndividual blog post page
product.archiveProduct ArchiveProduct catalog listing page
product.singleProduct SingleIndividual product detail page
category.archiveCategory ArchiveProduct category listing page
searchSearch ResultsSearch results page
cartCartShopping cart page
checkoutCheckoutCheckout/payment page
accountAccountCustomer account dashboard
headerHeaderHeader surface slot
footerFooterFooter surface slot
404404Page not found

Pseudo-Code Hooks System #

The Pseudo-Code system allows theme and plugin developers to declare safe, declarative extension hooks without writing custom server-side code. Hooks fire at specific lifecycle events and execute pre-configured actions.

Note: The pseudo-code system is separate from the template @hook directive. Pseudo-code hooks are declared in manifest.json and fire on server-side events (order created, customer signs up, etc.). Template @hook directives are for frontend rendering hooks.

Declaring Hooks in Manifest

JSON
{
  "corewavePseudoCode": {
    "engine": "corewave-pseudo/1.0",
    "blocks": [
      {
        "id":     "welcome-email",
        "hook":    "corewave.customer.after_signup",
        "action": "send_email",
        "args": {
          "template": "welcome",
          "subject": "Welcome to our store!"
        }
      }
    ]
  }
}

Use Cases

  • Checkout Automation — Apply discounts, validate inputs, set shipping rates, add order notes (corewave.checkout.*, corewave.order.*)
  • Customer Engagement — Send welcome emails on signup, tag customers, assign segments (corewave.customer.*, corewave.notifications.*)
  • Inventory & Catalogue — React to product creation/updates, monitor low stock (corewave.inventory.*, corewave.catalogue.*)
  • Blog & Content — Customize blog rendering, validate comments, send notifications (corewave.blog.*)
  • Template Display — Show real-time counts in templates using sf-pseudo-command block

Available Hooks

HookDescription
corewave.customer.after_signupFires after a customer registers
corewave.customer.after_loginFires after customer login
corewave.customer.after_logoutFires after customer logout
corewave.customer.profile.updatedFires when customer updates profile
corewave.order.createdFires when a new order is placed
corewave.order.paidFires when payment is confirmed
corewave.order.fulfilledFires when order is fulfilled
corewave.order.cancelledFires when order is cancelled
corewave.checkout.before_submitFires before checkout submission
corewave.checkout.after_submitFires after checkout submission
corewave.catalogue.product.createdFires when a product is created
corewave.catalogue.product.updatedFires when a product is updated
corewave.inventory.low_stockFires when stock falls below threshold
corewave.inventory.out_of_stockFires when product goes out of stock
corewave.blog.post.createdFires when a blog post is published
corewave.blog.comment.createdFires when a new comment is added
corewave.notifications.email.sendFires before sending an email notification
corewave.notifications.sms.sendFires before sending an SMS notification
corewave.theme.page.renderFires before a storefront page is rendered
corewave.theme.page.renderedFires after a storefront page is rendered

Available Actions

ActionDescription
send_emailSend a transactional email using a template
send_smsSend an SMS notification
redirectRedirect customer to a specific URL
apply_discountAuto-apply a discount code to the cart
add_order_noteAdd a note to the order
add_order_tagTag an order with a label
tag_customerAssign a tag to the customer
assign_segmentAssign customer to a segment
inject_htmlInject HTML at page head_end or body_end
log_eventLog an event for debugging/audit

Block Fields Reference

FieldRequiredDescription
idYesUnique identifier (a-z, A-Z, 0-9, _, -, max 80 chars)
hookYesThe lifecycle event to bind to
actionYesThe action to execute when the hook fires
whenNoOptional condition expression (max 500 chars)
argsNoConfiguration payload passed to the action

Plugin Integration #

Plugins extend storefront themes with additional features, blocks, and behaviors. They integrate with both the template system (via @hook, @filter, and @widget directives) and the pseudo-code system (for server-side event handling).

Plugin Manifest

JSON
{
  "name":        "WhatsApp Chat",
  "pluginCode": "whatsapp-chat",
  "version":     "2.0.0",

  "corewavePseudoCode": {
    "engine": "corewave-pseudo/1.0",
    "blocks": [
      {
        "id":     "whatsapp-button",
        "hook":    "corewave.theme.page.render",
        "action": "inject_html",
        "args": {
          "position": "body_end",
          "html":    "
Chat with us
" } } ] } }

Bundled Plugins in Theme Packages

Theme packages can include companion plugins inside bundled-plugins/. Each ZIP must be a valid plugin package with its own manifest.json:

Text
my-theme-v3.0.0.zip
├── manifest.json
├── design.json
├── templates/
├── assets/
└── bundled-plugins/
    ├── whatsapp-chat-v2.0.0.zip
    └── reviews-summary-v2.0.0.zip

Using Template Hooks

The @hook and @filter directives in .cw templates allow plugins to inject content at specific points:

HTML
{{-- Action hook — plugins can execute code here --}}
@hook('product.card.after', __product)

{{-- Filter hook — plugins can modify a value --}}
@code
  __html = '<span class="price">' . cw_format_price(__product.price) . '</span>';
  __html = @filter('product.price_html', __html);
  echo __html;
@endcode

{{-- Widget area — plugins can render UI components --}}
@widget('sidebar')

Custom Widgets V2 #

Custom widgets allow theme developers to encapsulate reusable UI components with their own templates, data queries, and configuration. Widgets are .cw template fragments that can be rendered server-side using the @widget('name') directive.

Widget Registration

Widgets are registered in the theme's manifest.json under the widgets section. Each widget declaration specifies the template file, default data query, and configuration schema:

JSON
"widgets": {
  "featured-products": {
    "label":       "Featured Products",
    "description": "Displays a grid of featured products",
    "template":  "widgets/featured-products.cw",
    "fields": {
      "title":    { "label": "Section Title", "type": "text", "default": "Featured Products" },
      "limit":   { "label": "Product Count", "type": "number", "default": 4 },
      "category": { "label": "Category Filter", "type": "select", "default": "", "source": "categories" }
    }
  }
}

Widget Template Files

Widget templates are placed in a widgets/ directory at the theme root. They use the same .cw directive syntax as regular templates, with access to widget-specific configuration via __widget:

CODE
{-- widgets/featured-products.cw --}
{-- Load category options dynamically using cw_get_select_options --}
@code
  __category_opts = cw_get_select_options(['source' => 'categories', 'limit' => 100]);
  __selected_cat  = __widget.settings.category;
@endcode

{-- If admin selected a specific category, only show those products --}
@if(__selected_cat)
  @query(__products, [
    'category' => __selected_cat,
    'limit'    => __widget.settings.limit ?? 4
  ])
@else
  @query(__products, [
    'featured' => 1,
    'limit'    => __widget.settings.limit ?? 4
  ])
@endif

<section class="featured-products-widget">
  @if(__widget.settings.title)
    <h2>{ __widget.settings.title }</h2>
  @endif

  {-- Render a category filter dropdown using @foreach over dynamic options --}
  <form class="category-filter">
    <select name="category" onchange="this.form.submit()">
      <option value="">All Categories</option>
      @foreach(__category_opts as __opt)
        <option value="{ __opt.value }" @if(__opt.value == __selected_cat)selected</option>
      @endforeach
    </select>
  </form>

  <div class="product-grid">
    @foreach(__products as __product)
      <div class="product-card">
        <a href="{ __product.url }">
          <img src="{ __product.thumbnail }" alt="{ __product.title }" loading="lazy" />
          <h3>{ __product.title }</h3>
        </a>
        <span class="price">{ cw_format_price(__product.price) }</span>
      </div>
    @endforeach
  </div>
</section>

Widget Directory Structure

Text
my-theme-v3.0.0.zip
├── manifest.json
├── design.json
├── templates/
├── widgets/                    // Custom widget .cw templates
│   ├── featured-products.cw
│   ├── newsletter-signup.cw
│   └── social-links.cw
├── assets/
└── bundled-plugins/

Using Widgets in Templates

Once registered, widgets are rendered in any .cw template using the @widget directive. You can pass configuration overrides directly:

HTML
{-- Render widget with default settings --}
@widget('featured-products')

{-- Render widget with overridden settings --}
@widget('featured-products', [
  'title'    => 'Best Sellers',
  'limit'    => 8,
  'category' => 'best-sellers'
])

Widget Areas

Widget areas are named slots in your templates where administrators can drop widgets via the platform UI. They are defined using the @widget('area-name') directive without a specific widget name:

HTML
{-- Widget area — admins can place any widget here --}
<aside class="sidebar">
  @widget('blog-sidebar')
</aside>
Widget Areas vs Named Widgets: A named widget call (@widget('featured-products')) renders a specific registered widget. A widget area call (@widget('blog-sidebar')) creates a slot where administrators can place widgets via the platform admin UI.

Widget Configuration Schema Fields

FieldTypeDescription
labelstringDisplay name for the widget shown in admin UI
descriptionstringShort description of what the widget does
templatestringRelative path to the .cw template file (e.g. widgets/featured-products.cw)
fieldsobjectMap of field names to their schemas (label, type, default, options, enum, source)

Field Type Reference

TypeDescriptionDefault Value
textSingle-line text input""
textareaMulti-line text input""
numberNumeric input0
booleanYes/No togglefalse
selectDropdown selector. Use options (array of {value, label} objects) for static choices or source (string) for dynamic options from products, categories, collections, blogs, posts, tags, discounts, countries, regions, citiesFirst option or ""
multi-selectCheckbox group for multiple selections. Uses the same options (array of {value, label} objects) for static choices or source (string) for dynamic options — identical to select. The stored value is a JSON array string (e.g. ["red","blue"]). Use cw_json_decode() in templates to retrieve the array.[]
colorColor picker#000000
imageMedia library image picker""
linkURL/link picker""

Best Practices

  • Keep widget templates focused — each widget should do one thing well
  • Provide sensible defaults for all configuration fields
  • Use @query inside widgets to fetch their own data rather than relying on parent context
  • Prefix widget template files with a descriptive name to avoid conflicts
  • Include a widgets/ directory in your theme package even if initially empty — it signals widget support
  • Test widgets with both named and area usage to ensure they render correctly in both modes

Select / Multi-Select Field Types V2

Both select and multi-select fields support two population modes:

ModePropertyDescription
Static options Array of {value, label} objects — choices are fixed and known at theme-authoring time. See example below.
Dynamic source String referencing a platform data source (e.g. "source": "products"). Options are fetched at render time via cw_get_select_options().

The options Property (Static / Manual Population)

Use static options when the possible values are fixed and known at theme-authoring time. The options property is an array of objects — each object must have a value (stored in settings) and a label (displayed in the admin UI):

JSON
{
  "fields": {
    "category": {
      "label":   "Category Filter",
      "type":    "select",
      "default": "",
      "options": [
        { "value": "clothing",   "label": "Clothing" },
        { "value": "electronics", "label": "Electronics" },
        { "value": "accessories", "label": "Accessories" }
      ]
    },
    "colors": {
      "label":   "Available Colors",
      "type":    "multi-select",
      "default": ["red"],
      "options": [
        { "value": "red",   "label": "Red" },
        { "value": "blue",  "label": "Blue" },
        { "value": "green", "label": "Green" }
      ]
    }
  }
}

Usage in templates (select): Access the single selected value directly.

CODE
@if(widget.settings.category == 'clothing')
  <div class="filter filter--clothing">Showing clothing items</div>
@elseif(widget.settings.category == 'electronics')
  <div class="filter filter--electronics">Showing electronics</div>
@else
  <div class="filter filter--all">Showing all categories</div>
@endif

Usage in templates (multi-select): The stored value is a JSON array string (e.g. ["red","blue"]). Use cw_json_decode() to parse it into an array:

CODE
{-- Decode multi-select value into an array --}
@code
  __selected_colors = cw_json_decode(widget.settings.colors);
@endcode

{-- Check if a specific value is selected --}
@if(in_array('red', __selected_colors))
  <div class="color-swatch color-swatch--red">Red is active</div>
@endif

{-- Iterate through all selected values --}
<ul class="selected-filters">
  @foreach(__selected_colors as __color)
    <li>{ __color }</li>
  @endforeach
</ul>

The source Property (Dynamic / Runtime Population)

When a widget field has a source property (e.g. "source": "products"), its options are populated dynamically at render time from the platform database. Use the cw_get_select_options() template function inside a @foreach block to build dropdowns or filter controls:

JSON
{
  "fields": {
    "category": {
      "label":   "Category Filter",
      "type":    "select",
      "default": "",
      "source": "categories"
    },
    "products": {
      "label":   "Featured Products",
      "type":    "multi-select",
      "default": [],
      "source": "products"
    }
  }
}
CODE
{-- Fetch select options from a data source --}
@code
  __category_opts = cw_get_select_options(['source' => 'categories', 'limit' => 100]);
  __product_opts  = cw_get_select_options(['source' => 'products', 'limit' => 50, 'order' => 'asc']);
  __blog_opts     = cw_get_select_options(['source' => 'blogs']);
  __country_opts  = cw_get_select_options(['source' => 'countries', 'orderBy' => 'name']);
  __tag_opts      = cw_get_select_options(['source' => 'tags', 'scope' => 'products']);
  __discount_opts = cw_get_select_options(['source' => 'discounts']);
@endcode

{-- Render a single-select dropdown using dynamic options --}
<select name="category">
  @foreach(__category_opts as __opt)
    <option value="{ __opt.value }">{ __opt.label }</option>
  @endforeach
</select>

{-- Render a multi-select checkbox group using dynamic options --}
@code
  __selected_cats = cw_json_decode(widget.settings.categories);
@endcode
<div class="checkbox-group">
  @foreach(__category_opts as __opt)
    <label>
      <input type="checkbox"
             value="{ __opt.value }"
             {{ in_array(__opt.value, __selected_cats) ? 'checked' : '' }}>
      { __opt.label }
    </label>
  @endforeach
</div>

Supported Sources

SourceReturnsAvailable Parameters
productsProduct options (id / title)limit, orderBy, order, ids
categoriesCategory options (slug / title)limit, orderBy, order
collectionsCollection options (slug / title)limit, orderBy
blogsBlog options (slug / title)
postsBlog post options (id / title)limit, orderBy, order, blog, tag
tagsTag options (slug / name)scope (products, posts, or both), limit
discountsDiscount options (code / title)limit
countriesCountry options (code / name)limit, orderBy
regionsRegion options (code / name)limit, orderBy, parentId (country ID)
citiesCity options (id / name)limit, orderBy, parentId (region ID)
Select / Multi-Select summary: Use options (array of {value, label} objects) when choices are fixed and known at theme-authoring time. Use source (string) when choices depend on dynamic store data (products, categories, etc.). Dynamic options are fetched via cw_get_select_options() and iterated with @foreach. For multi-select, use cw_json_decode() to retrieve the currently-selected values from the widget settings.

Theme Runtime Index #

The runtime index is a JSON file generated for your theme at platform/storefront/themes/{themeCode}/{version}/runtime/index.json. It serves as the single source of truth for the storefront frontend, containing all resolved URLs, embedded content, and theme metadata.

JSON
{
  "schemaVersion": 3,
  "themeCode":      "my-storefront-theme",
  "version":        "1.0.0",
  "generatedAt":    "2025-01-15T10:30:00.000Z",

  "manifest":      { /* sanitized manifest.json */ },
  "defaultDesign": { /* design.json (if present) */ },

  "themeCssUrl": "https://storage.googleapis.com/.../style.css",
  "cssUrls": [ /* ordered CSS URLs */ ],
  "scriptUrls": [ /* ordered JS URLs */ ],

  "files": {
    "assets/css/style.css": "https://storage.googleapis.com/.../style.css",
    "assets/images/logo.png": "https://storage.googleapis.com/.../logo.png"
  },

  "templates": {
    "catalogue.home": { /* inlined .cw content */ },
    "page.default": "https://storage.googleapis.com/.../page.default.cw"
  },
  "templateFormats": {
    "catalogue.home": "cw",
    "cart.default": "json"
  }
}

Loading Pipeline

  1. Fetch institution bootstrap data (settings, active theme info)
  2. Fetch the active theme's runtime index via the theme runtime client
  3. Load CSS assets sequentially in priority order
  4. Load JavaScript assets sequentially in priority order
  5. Resolve the appropriate template for the current page
  6. Fetch the rendered .cw template via the Template Engine API
  7. Inject the rendered HTML into the DOM

First-Response SSR Endpoint

For SEO crawlers and custom-domain deployments that need the first HTTP response to contain the rendered storefront HTML, CoreWave exposes a full-document SSR endpoint in addition to the fragment render endpoint used by the React storefront runtime.

EndpointPurposeReturns
GET /v1/public/storefront/templates/renderRender one .cw template fragment by templateKey. Used by the React storefront runtime.text/html fragment
GET /v1/public/storefront/ssrResolve a storefront route, render the selected .cw template, and wrap it in a complete HTML document.text/html document
curl -i 'https://api.books.corewave360.com/v1/public/storefront/ssr?host=corewave360.com&path=/blogs/news/spring-launch'

The SSR endpoint accepts host, handle, path, customerToken, and preferredCurrency. It uses the same institution resolution as the normal public storefront APIs. It resolves route kinds for home/catalogue, pages, blogs, blog archives, blog posts, account sections, cart, checkout, login, and registration.

In production, route the public storefront domain to this endpoint when the request expects HTML and does not target a static asset. A typical nginx deployment keeps static React assets served by the frontend host, while HTML document requests can be proxied to:

location / {
  proxy_pass https://api.books.corewave360.com/v1/public/storefront/ssr?host=$host&path=$request_uri;
  proxy_set_header Host api.books.corewave360.com;
  proxy_set_header X-Forwarded-Proto $scheme;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

The returned document includes a canonical URL, SEO-friendly body HTML, active theme CSS URLs, active theme script URLs, and a cw-template-key meta tag for debugging. If a template is marked auth-protected, the SSR endpoint enforces the same customer-token check as templates/render.

Starter Content #

The starterContent section in manifest.json defines which templates are automatically created and published when a user installs your theme. This ensures the storefront has content immediately after installation.

JSON
"starterContent": {
  "catalogue.home": {
    "name":   "Home",
    "status": "Published"
  },
  "catalogue.default": {
    "name":   "Catalog",
    "status": "Published"
  }
}
FieldTypeDescription
nameStringDisplay name for the template instance
statusString"Published" or "Draft"
homeTemplateKeyStringOptional override for the recommended home template key
recommendedAssignmentsObjectOptional route-to-template mapping overrides
menusObjectNamed navigation menus to seed on installation. Each key is the menu slug (e.g. main, footer, secondary-nav) and the value is an array of Menu Item objects (see Menu Seeding below)
pagesArrayArray of Page objects to seed as CMS pages on installation (see Page Seeding below)

Page Seeding

Themes can seed any number of CMS pages with individual header/footer preset assignments using the pages array inside starterContent. Each entry in the array becomes a StorefrontPage record. This allows you to pre-populate pages like About Us, Contact, FAQ, etc., each with its own header and footer preset.

JSON
"starterContent": {
  "catalogue.home": { "name": "Home", "status": "Published" },

  "pages": [
    {
      "title":             "About Us",
      "handle":            "about-us",
      "templateKey":       "page.default",
      "headerPresetKey":   "header-dark",
      "footerPresetKey":   "footer-minimal",
      "showInNavigation":  true,
      "sortOrder":         2,
      "publish":           true
    },
    {
      "title":             "Contact",
      "handle":            "contact",
      "templateKey":       "page.default",
      "headerPresetKey":   "header-light",
      "showInNavigation":  true,
      "sortOrder":         3,
      "publish":           true
    }
  ]
}
FieldTypeDescription
titleStringPage title displayed in the browser tab and admin UI
handleStringURL slug for the page (e.g. "about-us" → /about-us)
templateKeyString?Template key to render this page (e.g. "page.default")
headerPresetKeyString?Key of the header preset to render on this page. Omit or set null to use the storefront's defaultHeaderPresetKey
footerPresetKeyString?Key of the footer preset to render on this page. Omit or set null to use the storefront's defaultFooterPresetKey
contentHtmlString?Optional raw HTML content body for the page (seeded as ContentJson)
showInNavigationBooleanWhether the page appears in automatic navigation menus
sortOrderIntDisplay order for navigation sorting (lower = first)
publishBooleanWhether the page is published immediately (true) or created as draft (false)
Tip: When a page omits headerPresetKey or footerPresetKey, the storefront falls back to the defaultHeaderPresetKey and defaultFooterPresetKey defined at the manifest top level. This lets you set a consistent look for most pages while customising specific pages like landing pages or promotional content.

Themes can seed any number of named navigation menus with nested items using the menus object inside starterContent. Each key in the object becomes a navigation menu slug (e.g. main, footer, secondary-nav), and its value is an array of menu item objects. This allows you to define as many navigations as needed — primary nav, footer links, sidebar menus, utility links, etc.

JSON
"starterContent": {
  "catalogue.home": { "name": "Home", "status": "Published" },
  "catalogue.default": { "name": "Catalog", "status": "Published" },
  "menus": {
    "main": [
      { "label": "Home",   "url": "/",        "sortOrder": 1 },
      { "label": "Shop",   "url": "/shop",    "sortOrder": 2 },
      { "label": "About",  "pageHandle": "about-us", "sortOrder": 3 },
      { "label": "Blog",   "url": "/blogs",   "sortOrder": 4 },
      { "label": "Categories", "sortOrder": 5,
        "children": [
          { "label": "Clothing",  "pageHandle": "category-clothing",  "sortOrder": 1 },
          { "label": "Electronics", "pageHandle": "category-electronics", "sortOrder": 2 }
        ]
      }
    ],
    "footer": [
      { "label": "Contact", "pageHandle": "contact", "sortOrder": 1 }
    ],
    "secondary-nav": [
      { "label": "Support", "url": "/support", "sortOrder": 1 },
      { "label": "FAQ", "pageHandle": "faq", "sortOrder": 2 }
    ]
  }
}
FieldTypeDescription
labelStringDisplay text for the menu link
pageHandleString?Handle of a seeded page to link to (e.g. "about-us")
urlString?Custom URL override (e.g. "/shop", "https://example.com")
sortOrderIntDisplay order (lower = first)
childrenArray?Nested child items (same structure), enabling dropdown/submenu navigation. Supports arbitrary depth
Note: Menu items are only seeded if the menu does not already exist for the storefront. If a menu item with the same label already exists, its sort order and URL may be updated, but merchant edits are preserved. The children array supports multilevel nesting for dropdown menus.
Tip: Set at least one starter template to "Published" — otherwise the storefront will show empty content after theme installation.

Object Property References #

The following tables list all accessible properties for each data type returned by @query and cw_*() functions. Properties are accessed via arrow syntax: __product.title, __store.name.

Product Object

Returned by @query(['type' => 'products', ...]). The current product on a detail page is available as __product.

PropertyTypeDescription
__product.idintProduct ID
__product.titlestringProduct name / title
__product.skustringStock keeping unit
__product.descriptionstringFull description (HTML)
__product.short_descriptionstringTruncated description (200 chars)
__product.barcodestringBarcode / UPC
__product.pricedecimalCurrent unit price
__product.compare_pricedecimal?Compare-at / original price
__product.discount_percentdecimal?Discount percent (0-100) applied when on_sale is true
__product.cost_pricedecimalCost price (internal use)
__product.stock_quantityintCurrent stock count
__product.in_stockbooltrue when stock_quantity > 0
__product.on_saleboolSale status (currently false)
__product.brandstringBrand name
__product.manufacturerstringManufacturer name
__product.imagestringPrimary thumbnail URL
__product.thumbnailstringPrimary thumbnail URL (same as image)
__product.imagesarrayArray of Image objects {url, alt, width, height}
__product.categoriesarrayArray of {id, name, slug} objects
__product.tagsarrayArray of {id, name, slug} objects
__product.created_atdatetimeCreation timestamp
__product.updated_atdatetimeLast update timestamp

Image Object

Returned inside __product.images array:

PropertyTypeDescription
__image.urlstringFull image URL
__image.altstringAlt text (product name)
__image.widthintImage width (may be 0)
__image.heightintImage height (may be 0)

Category Object

Returned by @query(['type' => 'categories', ...]):

PropertyTypeDescription
__category.idintCategory ID
__category.namestringCategory display name
__category.slugstringURL slug (same as name)
__category.descriptionstringCategory description
__category.product_countintNumber of active products in category

Blog Object

Returned by @query(['type' => 'blogs']):

PropertyTypeDescription
__blog.idintBlog ID
__blog.namestringBlog name
__blog.slugstringURL slug (blog handle)
__blog.descriptionstringBlog description (HTML)
__blog.post_countintNumber of posts in this blog

Blog Post Object

Returned by @query(['type' => 'posts', ...]). The current post on a post detail page is available as __post.

PropertyTypeDescription
__post.idintPost ID
__post.titlestringPost title
__post.slugstringURL slug (post handle)
__post.excerptstringPost excerpt / summary
__post.contentstringFull post content (JSON/HTML)
__post.content_jsonstringRaw content JSON
__post.cover_imagestring?Cover image URL (may be null)
__post.published_atdatetimePublish timestamp
__post.statusstringPost status (Published/Draft/etc)
__post.blog_idintParent blog ID
__post.categoriesarrayArray of {id, name, slug} objects
__post.tagsarrayArray of {id, name, slug} objects
__post.created_atdatetimeCreation timestamp
__post.updated_atdatetimeLast update timestamp

Store Object

Returned by cw_get_store():

PropertyTypeDescription
__store.idintStorefront ID
__store.namestringStore / business name
__store.slugstringStore URL handle
__store.taglinestring?Store tagline (may be null)
__store.logostring?Logo URL from theme design
__store.logo_urlstring?Logo URL (alias of logo)
__store.faviconstring?Favicon URL from theme design
__store.favicon_urlstring?Favicon URL (alias of favicon)
__store.primary_colorstring?Brand primary color (hex)
__store.secondary_colorstring?Brand secondary color (hex)
__store.currencystringCurrency code (currently "NGN")
__store.currency_codestringCurrency code (alias of currency)
__store.languagestringLanguage code (currently "en")
__store.language_codestringLanguage code (alias of language)
__store.timezonestringTimezone (currently "Africa/Lagos")
__store.emailstring?Contact email from CMS settings
__store.phonestring?Contact phone from CMS settings
__store.addressstring?Contact address from CMS settings
__store.social_linksstring?Social media links (JSON)

Navigation Object

Returned by cw_get_navigations(), cw_get_navigation(id), cw_get_navigation_by_location(location), and cw_get_navigation_by_key(key):

PropertyTypeDescription
__nav.idintMenu ID
__nav.namestringMenu display name
__nav.slugstringURL-friendly key (e.g., "main-menu")
__nav.locationstringMenu location ("main" or "footer")
__nav.activeboolWhether the menu is active
__nav.itemsarrayArray of Menu Item objects with optional nested children

Menu Item Object

Returned inside __nav.items arrays:

PropertyTypeDescription
__item.idintMenu item ID
__item.menu_idintParent menu ID
__item.parent_item_idint?Parent item ID (null for root items)
__item.labelstringDisplay text for the link
__item.urlstring?Resolved URL (custom URL or page link)
__item.page_idint?Linked page ID (if any)
__item.sort_orderintDisplay order
__item.visibleboolWhether the item is visible
__item.childrenarrayNested child items (same structure, for dropdown/sub-menus)

Discount Code Object

Returned by @query(['type' => 'discounts']) and cw_get_discount():

PropertyTypeDescription
__discount.idintDiscount ID
__discount.codestringDiscount code
__discount.display_namestringDisplay name for admin
__discount.typestringDiscount type
__discount.valuedecimalDiscount value (amount or percentage)
__discount.descriptionstringDiscount description
__discount.minimum_subtotaldecimalMinimum order subtotal required
__discount.maximum_discount_amountdecimalMaximum discount cap
__discount.is_free_shippingboolGrants free shipping
__discount.starts_atdatetimeDiscount start date
__discount.ends_atdatetimeDiscount expiry date
__discount.applies_to_allboolApplies to all products

Tag Object

Returned by @query(['type' => 'tags', ...]) and tag functions:

PropertyTypeDescription
__tag.idintTag ID
__tag.namestringDisplay name
__tag.slugstringURL-safe slug
__tag.scopestringScope (products/posts/both)
__tag.product_countintNumber of tagged products
__tag.post_countintNumber of tagged blog posts

Template Assignments #

Template assignments map route kinds to specific template keys. When a user visits a storefront page, the runtime resolves the route kind and loads the corresponding template. These assignments can be configured in the manifest or overridden via the platform admin.

Route KindTemplate Key PatternExample
Home / Landingcatalogue.home or catalogue.{custom}catalogue.home, catalogue.landing
Catalog / Categorycatalogue.defaultcatalogue.default
Product Detailproduct.detailproduct.detail
Cartcart.defaultcart.default
Checkoutcheckout.defaultcheckout.default
Blog Indexblogs.defaultblogs.default
Blog Postpost.defaultpost.default
CMS Pagepage.defaultpage.default
404page.not-foundpage.not-found
Loginlogin.defaultlogin.default
Registerregister.defaultregister.default
Account Dashboardaccount.default or account.{section}account.dashboard, account.orders

Storefront Appearance Settings V2 #

The Storefront → Appearance panel in the admin dashboard provides a comprehensive settings interface for customizing your storefront's branding, reading preferences, discussion/comment rules, avatars, image sizes, permalink structure, and privacy settings. These settings are persisted in two JSON columns: appearanceJson (contains branding, theme, layout, typography) and cmsSettingsJson (contains reading, discussion, avatars, media, permalink, privacy).

Branding Assets

Branding assets are stored in appearanceJson.branding. Each image asset is selected from the Media Library via a media picker modal and stored as a public URL string. The Site tagline is a plain text field.

FieldTypeDefaultJSON PathDescription
Default logostring (URL)""appearanceJson.branding.logoDefaultPrimary logo displayed on the storefront. Falls back to logo if logoDefault is empty.
Logo for light backgroundstring (URL)""appearanceJson.branding.logoLightAlternative logo optimized for light-colored backgrounds.
Logo for dark backgroundstring (URL)""appearanceJson.branding.logoDarkAlternative logo optimized for dark-colored backgrounds.
Faviconstring (URL)""appearanceJson.branding.faviconBrowser tab icon and bookmark icon for the storefront.
Site taglinestring""appearanceJson.branding.siteTaglineShort descriptive phrase shown alongside the site title on the storefront.
JSON
{
  "branding": {
    "logoDefault": "https://storage.example.com/brand/primary-logo.png",
    "logoLight": "https://storage.example.com/brand/logo-light.png",
    "logoDark": "https://storage.example.com/brand/logo-dark.png",
    "favicon": "https://storage.example.com/brand/favicon.ico",
    "siteTagline": "Premium quality since 2020"
  }
}

Reading Settings

Reading settings control how blog posts and syndication feeds behave. These are stored in cmsSettingsJson.reading.

UI LabelJSON PathTypeDefaultRangeDescription
Blog pages show at mostcmsSettingsJson.reading.postsPerPageinteger101 – 200Maximum number of blog posts displayed per page on blog index views.
Syndication feeds show the most recentcmsSettingsJson.reading.feedItemsCountinteger101 – 200Number of most recent items included in RSS/Atom syndication feeds.
For each post in a feed, includecmsSettingsJson.reading.feedContentModeenum"full""full" | "excerpt"Whether feed items contain the full post body or just an excerpt.
Discourage search engines from indexingcmsSettingsJson.reading.discourageSearchIndexingbooleanfalsetrue / falseWhen enabled, adds a tag to all storefront pages.

Discussion Settings

Discussion settings control comment behavior, notifications, and moderation rules. These are stored in cmsSettingsJson.discussion.

Default Post Settings

UI LabelJSON PathTypeDefaultDescription
Attempt to notify any blogs linked to from the postcmsSettingsJson.discussion.defaultPost.notifyLinkedBlogsbooleantrueSends pingback notifications to URLs referenced in new posts.
Allow link notifications from other blogs (pingbacks and trackbacks)cmsSettingsJson.discussion.defaultPost.allowPingbacksbooleantrueAccepts incoming pingback/trackback notifications from other blogs.
Allow people to submit comments on new postscmsSettingsJson.discussion.defaultPost.allowCommentsOnNewPostsbooleantrueGlobally enables comments on new blog posts (can be overridden per-post).

Other Comment Settings

UI LabelJSON PathTypeDefaultRangeDescription
Comment author must fill out name and emailcmsSettingsJson.discussion.other.requireNameEmailbooleantruetrue / falseRequires comment authors to provide both name and email fields.
Users must be registered and logged in to commentcmsSettingsJson.discussion.other.requireLoginbooleanfalsetrue / falseOnly allows authenticated users to submit comments.
Automatically close comments on old postscmsSettingsJson.discussion.other.autoCloseCommentsbooleanfalsetrue / falseEnables automatic comment closing after a configurable number of days.
Close comments when post is this many days oldcmsSettingsJson.discussion.other.closeAfterDaysinteger141 – 3650Number of days after which comments are automatically closed.
Show comments cookies opt-in checkboxcmsSettingsJson.discussion.other.showCookiesOptInbooleantruetrue / falseDisplays a GDPR/privacy cookie consent checkbox on the comment form.
Enable threaded (nested) commentscmsSettingsJson.discussion.other.enableThreadedCommentsbooleantruetrue / falseAllows replies to comments, creating nested comment threads.
Number of levels for threaded commentscmsSettingsJson.discussion.other.threadedLevelsinteger52 – 10Maximum nesting depth for threaded comment replies.
Break comments into pagescmsSettingsJson.discussion.other.breakCommentsIntoPagesbooleantruetrue / falsePaginates comments when there are more than the per-page limit.
Top level comments per pagecmsSettingsJson.discussion.other.commentsPerPageinteger501 – 500Number of top-level comments displayed per comment page.
Comments page to display by defaultcmsSettingsJson.discussion.other.defaultCommentsPageenum"last""last" | "first"Which comment page to show by default (newest or oldest first).
Comments to display at top of each pagecmsSettingsJson.discussion.other.commentsSortenum"older""older" | "newer"Sort order of comments within each page.

Email Me Whenever

UI LabelJSON PathTypeDefaultDescription
Anyone posts a commentcmsSettingsJson.discussion.other.emailOnAnyCommentbooleanfalseSends an email notification for every new comment.
A comment is held for moderationcmsSettingsJson.discussion.other.emailOnModerationbooleanfalseSends an email when a comment is queued for manual moderation.
Anyone posts a notecmsSettingsJson.discussion.other.emailOnNotebooleanfalseSends an email when a note (internal moderation note) is posted.

Before a Comment Appears

UI LabelJSON PathTypeDefaultRangeDescription
Comment must be manually approvedcmsSettingsJson.discussion.other.mustApproveManuallybooleanfalsetrue / falseAll comments must be approved by a moderator before becoming visible.
Comment author must have a previously approved commentcmsSettingsJson.discussion.other.requirePreviouslyApprovedbooleanfalsetrue / falseAuto-approves comments from authors who have had at least one comment approved before.
Hold a comment if it contains this many links or morecmsSettingsJson.discussion.other.moderationLinksThresholdinteger20 – 50Number of links allowed before a comment is automatically held for moderation.
Comment moderation keys (one per line)cmsSettingsJson.discussion.other.moderationKeywordsstring (multi-line)""—Keywords/patterns that trigger moderation. One entry per line. Comments containing these keywords are held for review.
Disallowed comment keys (one per line)cmsSettingsJson.discussion.other.disallowedKeysstring (multi-line)""—Keywords/patterns that cause a comment to be rejected outright. One entry per line.

Avatars

Avatar settings control how user profile images are displayed on comments. Stored in cmsSettingsJson.avatars.

UI LabelJSON PathTypeDefaultDescription
Show avatarscmsSettingsJson.avatars.showAvatarsbooleantrueGlobally enables or disables avatar display on comments.
Maximum ratingcmsSettingsJson.avatars.maxRatingenum"G"Filters avatars by rating: "G", "PG", "R", or "X".
Default avatarcmsSettingsJson.avatars.defaultAvatarenum"mystery-person"Fallback avatar shown when no Gravatar is found. Options: "mystery-person", "blank", "gravatar", "identicon", "wavatar", "monsterid", "retro", "robohash", "initials", "color".

Image Sizes

Image size settings define the default dimensions for automatically generated image variants. Stored in cmsSettingsJson.media.

SizeJSON PathDefault WidthDefault HeightCropDescription
ThumbnailcmsSettingsJson.media.thumbnail160160falseSmall square thumbnail used in listings and grids.
MediumcmsSettingsJson.media.medium6400 (auto)—Medium-sized image for post content and galleries.
LargecmsSettingsJson.media.large12800 (auto)—Large image for featured content and hero sections.

Privacy

Privacy settings allow selecting a storefront page as the privacy policy page. Stored in cmsSettingsJson.privacy.

UI LabelJSON PathTypeDefaultDescription
Privacy policy pagecmsSettingsJson.privacy.policyPageIdstring (ID)""Links to a storefront CMS page that serves as the privacy policy. When set, the storefront footer/app can reference this page automatically.

Data Storage

The Appearance page saves settings to two separate JSON columns on the StorefrontSettings record:

ColumnContentsAPI Field
AppearanceJsonbranding, theme, layout, typographyappearanceJson
CmsSettingsJsonreading, discussion, avatars, media, permalink, privacycmsSettingsJson

Both columns are serialized JSON objects. The frontend loads them via GET /operations/storefront/appearance and saves them via PUT /operations/storefront/appearance. The cmsSettingsJson is deeply merged with defaults using a mergeDeep strategy, ensuring missing keys are always populated with sensible defaults.

Storefront Overview Template Defaults

Storefront → Overview stores default v2 single-entity template selections on the Storefront.DesignJson object under defaultTemplateKeys. These settings are storefront-specific, so one customer's choice of a product detail, blog archive, or blog post template does not affect another customer using the same installed theme package.

JSON PathTypeDescription
defaultTemplateKeys.productDetailstringDefault active .cw template key for product detail routes when the product has no item-level TemplateKey.
defaultTemplateKeys.blogArchivestringDefault active .cw template key for single blog archive routes when the blog has no item-level TemplateKey.
defaultTemplateKeys.blogPoststringDefault active .cw template key for blog post routes when the post has no item-level TemplateKey.
JSON
{
  "defaultTemplateKeys": {
    "productDetail": "product.detail",
    "blogArchive": "blog.magazine",
    "blogPost": "post.editorial"
  }
}
Note: When saving, the frontend strips any homepageMode key from the reading settings to prevent stale data. If you need to set a homepage mode, configure it through the dedicated Storefront Pages panel instead.

Auth-Protected Templates & Page Link V2 #

Templates can be configured as auth-protected, meaning the storefront will require a customer to be logged in before they can view pages rendered using that template. This is useful for restricted content, member-only pages, private catalogs, or any page that should not be accessible to guest shoppers.

is_auth_protected Flag

When a template has isAuthProtected set to true, the backend enforces authentication before rendering the template. If an unauthenticated user attempts to access the page:

  1. The server returns an HTTP 401 response with a redirect URL.
  2. The storefront frontend detects the 401 and automatically redirects the user to the login page.
  3. After successful login, the user is returned to the originally requested page.

page_link Property

The pageLink property is an optional URL path that can be assigned to a template. This path serves as the canonical link when referencing the template from other templates — for example, in navigation menus, cross-linking between pages, or cw_route() directives. The pageLink does not determine routing; it only provides a stable URL reference for linking purposes.

Managing Auth Protection & Page Link in the Admin

In the Storefront → Theme Builder → Template Surfaces panel, each surface has two new fields:

Runtime Data

When fetching a published template via the storefront render API, the response includes:

JSON
{
  "templateKey": "my.custom.page",
  "isAuthProtected": true,
  "pageLink": "/my-custom-page"
}
Note: Auth protection works at the template level, not the route level. If multiple templates share the same route kind, only the ones with isAuthProtected: true will enforce login. The login page itself (login.default) should never be auth-protected.

Account & Auth Templates V2 #

Customer account pages and authentication flows (login, registration, password reset) are fully customizable using .cw template files. These templates use the same directive system as other storefront pages, with access to customer session data and auth-related functions.

Login Page (login.default)

The login page template renders the customer sign-in form. It has access to __customer (null if not logged in) and can display error/success messages via session variables:

CODE
{-- templates/login.cw --}
@extends('layouts/main')

@section('head')
  <title>Sign In — { cw_get_store().name }</title>
  @css('assets/css/auth.css')
@endsection

@section('content')
<div class="auth-page">
  <div class="auth-container">
    <h1>Sign In</h1>

    @if(__session.login_error)
      <div class="alert alert-danger">{ __session.login_error }</div>
    @endif

    @if(__session.reset_sent)
      <div class="alert alert-success">Password reset link sent to your email.</div>
    @endif

    <form method="POST" action="{ cw_route('login') }">
      @hook('login.form.before')

      <div class="form-group">
        <label for="email">Email Address</label>
        <input type="email" name="email" id="email" required class="form-control" />
      </div>

      <div class="form-group">
        <label for="password">Password</label>
        <input type="password" name="password" id="password" required class="form-control" />
      </div>

      <div class="form-group form-check">
        <label>
          <input type="checkbox" name="remember" /> Remember Me
        </label>
      </div>

      @hook('login.form.before_submit')

      <button type="submit" class="btn btn-primary btn-block">Sign In</button>

      <p class="auth-links">
        <a href="{ cw_route('password.reset') }">Forgot Password?</a>
        <a href="{ cw_route('register') }">Create Account</a>
      </p>

      @hook('login.form.after')
    </form>
  </div>
</div>
@endsection

Registration Page (register.default)

The registration page template renders the customer sign-up form. It can include custom fields and validation:

CODE
{-- templates/register.cw --}
@extends('layouts/main')

@section('head')
  <title>Create Account — { cw_get_store().name }</title>
  @css('assets/css/auth.css')
@endsection

@section('content')
<div class="auth-page">
  <div class="auth-container">
    <h1>Create Account</h1>

    @if(__session.register_error)
      <div class="alert alert-danger">{ __session.register_error }</div>
    @endif

    <form method="POST" action="{ cw_route('register') }">
      @hook('register.form.before')

      <div class="form-group">
        <label for="first_name">First Name</label>
        <input type="text" name="first_name" id="first_name" required class="form-control" />
      </div>

      <div class="form-group">
        <label for="last_name">Last Name</label>
        <input type="text" name="last_name" id="last_name" required class="form-control" />
      </div>

      <div class="form-group">
        <label for="email">Email Address</label>
        <input type="email" name="email" id="email" required class="form-control" />
      </div>

      <div class="form-group">
        <label for="phone">Phone Number</label>
        <input type="tel" name="phone" id="phone" class="form-control" />
      </div>

      <div class="form-group">
        <label for="password">Password</label>
        <input type="password" name="password" id="password" required class="form-control" />
        <small class="form-text">Minimum 8 characters</small>
      </div>

      <div class="form-group">
        <label for="password_confirm">Confirm Password</label>
        <input type="password" name="password_confirm" id="password_confirm" required class="form-control" />
      </div>

      @hook('register.form.before_submit')

      <button type="submit" class="btn btn-primary btn-block">Create Account</button>

      <p class="auth-links">
        <a href="{ cw_route('login') }">Already have an account? Sign In</a>
      </p>

      @hook('register.form.after')
    </form>
  </div>
</div>
@endsection

Maintenance Page (maintenance.*)

The maintenance page is shown when the storefront is in maintenance mode. It can display a custom message, countdown, or contact information. Any template key with the maintenance prefix is treated as a maintenance page:

CODE
{-- templates/maintenance.cw --}
@code
  __store = cw_get_store();
@endcode

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>{ __store.name } — Under Maintenance</title>
  @css('assets/css/maintenance.css')
</head>
<body>
  <div class="maintenance-page">
    <div class="maintenance-content">
      @if(__store.logo)
        <img src="{ __store.logo }" alt="{ __store.name }" class="maintenance-logo" />
      @endif

      <h1>We'll Be Back Soon</h1>
      <p>We're currently performing scheduled maintenance to improve your experience.</p>

      @if(__store.support_email)
        <p>For urgent inquiries, contact us at <a href="mailto:{ __store.support_email }">{ __store.support_email }</a></p>
      @endif

      @hook('maintenance.content')
    </div>
  </div>
</body>
</html>

Account Dashboard & Sub-Templates

Account section templates are organized in the templates/account/ directory. Each sub-template handles a specific section of the customer account area (orders, wishlist, profile, etc.). They share a common layout via @extends:

CODE
{-- templates/account/dashboard.cw -- Account dashboard --}
@extends('layouts/main')

@section('head')
  <title>My Account — { cw_get_store().name }</title>
  @css('assets/css/account.css')
@endsection

@section('content')
@code
  __profile = cw_get_customer_profile();
  __customer = __profile.customer;
  __summary = __profile.summary;
  __orders_result = cw_get_customer_orders(page: 1, limit: 5);
  __recent_orders = __orders_result.orders.items;
  __wishlist = cw_get_customer_wishlist();
  __wishlist_count = cw_count(var: __wishlist);
@endcode

Account Section Template Keys

The following account sub-templates are available, each mapping to a specific route:

Template KeyFileDescription
account.defaulttemplates/account/dashboard.cwAccount dashboard with overview, recent orders, quick links
account.orderstemplates/account/orders.cwFull order history with pagination and filtering
account.order-detailtemplates/account/order-detail.cwSingle order view with items, status, tracking, and documents
account.wishlisttemplates/account/wishlist.cwWishlist items with add-to-cart and remove actions
account.returnstemplates/account/returns.cwReturn requests history and create return form
account.reviewstemplates/account/reviews.cwProduct reviews written by the customer
account.profiletemplates/account/profile.cwProfile information edit form
account.addressestemplates/account/addresses.cwSaved addresses management (add, edit, delete)
account.invoicestemplates/account/invoices.cwInvoice history and download links
account.documentstemplates/account/documents.cwPost-purchase document downloads (receipts, contracts, etc.)

Auth-Related Data Functions

These functions are available in account and auth templates:

FunctionReturnsDescription
cw_get_customer_profile()object or nullReturns the logged-in customer profile object (contains customer and summary), or null if not authenticated
cw_user_logged_in()booleanReturns true if a customer session is active
cw_get_customer_orders(page, limit)objectReturns paginated orders for the current customer. Accepts named params: page, limit, sort_by, sort_dir
cw_get_customer_order(order_id)object or nullReturns a single order by its ID, or null if not found
cw_get_customer_invoices(page, limit)objectReturns paginated invoices (orders with invoice) for the current customer
cw_get_customer_receipts(page, limit)objectReturns paginated receipts (paid orders) for the current customer
cw_get_customer_documents(order_id)arrayReturns documents for a specific order, or all documents if no order_id given
cw_get_customer_wishlist()arrayReturns wishlist items for the current customer
cw_add_to_customer_wishlist(product_id)objectAdds a product to the customer's wishlist
cw_remove_from_customer_wishlist(product_id)voidRemoves a product from the customer's wishlist
cw_get_customer_returns(page, limit)objectReturns paginated return requests for the current customer
cw_create_customer_return(order_id, reason, product_id)objectCreates a return request for an order. product_id is optional
cw_get_customer_reviews(page, limit)objectReturns paginated product reviews written by the customer
cw_get_customer_dashboard()objectReturns dashboard summary with wishlist, returns, reviews, and spend chart data
cw_update_customer_profile(first_name?, last_name?, other_names?, phone?, zip_code?, billing_address?, shipping_address?, extra_info?, profile_picture_asset_id?, country_id?, region_id?, city_id?)objectUpdates the customer's profile. All params are optional. Supports location fields (country_id, region_id, city_id)
cw_login_user(email, password, redirect_url?)objectAuthenticates with email and password. Returns session token, customer profile, and summary
cw_verify_login(challenge_token, code)objectVerifies the OTP code and establishes a customer session
cw_logout_user()voidClears the current customer session
cw_register_user(first_name, last_name, email, password, phone?, country_id?, region_id?, city_id?, address?, zip_code?)objectRegisters a new customer account with email and password. Supports location fields and optional address/phone
cw_count(var)intReturns the count of items in a collection variable
cw_format_money(amount, currency_code)stringFormats a monetary value with currency symbol (e.g. $19.99)
cw_route(name, params)stringGenerates a route URL by name (e.g. login, account.orders, account.order-detail)
Authentication Check: Use @if(cw_user_logged_in()) to conditionally render account content. If the customer is not logged in, redirect them to the login page using the cw_route('login') function for the login URL or display a login prompt.

Account Template Directory Structure

Text
templates/
├── home.cw
├── login.cw                    // Customer sign-in
├── register.cw                 // Customer registration
├── maintenance.cw              // Maintenance/offline page
├── account/
│   ├── dashboard.cw            // Account overview
│   ├── orders.cw               // Order history
│   ├── order-detail.cw         // Single order view
│   ├── wishlist.cw             // Wishlist management
│   ├── returns.cw              // Returns & exchanges
│   ├── reviews.cw              // Product reviews
│   ├── profile.cw              // Profile settings
│   ├── addresses.cw            // Address book
│   ├── invoices.cw             // Invoice history
│   └── documents.cw            // Post-purchase documents
├── header.cw
└── footer.cw
Tip: You can override any account section template by placing a .cw file with the matching template key in the templates/account/ directory. If a template is missing, the runtime falls back to the default account UI built into the storefront.

Best Practices #

Template Organization

Performance

SEO

Plugin Compatibility

v2 Package Compatibility

v2 Validation Guide V2 #

Use this checklist before publishing a theme package.

Required Checks

AreaRequirement
ManifestformatVersion is 3; route assignments point to existing .cw template keys.
TemplatesHome, product detail, blog archive, blog post, cart, checkout, auth, and account routes have matching .cw files when enabled.
SectionsReusable sections live in sections/*.cw and are included with @include, @each, or @section/@yield.
DataTemplates use @query and cw_get_*() functions for product, blog, category, cart, checkout, and account data.
SEODocument pages rendered through /v1/public/storefront/ssr include title, description, canonical URL, and semantic HTML.
Visual EditorTheme edits target the installed runtime templates, sections, partials, settings, and route assignments.