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.
.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:
- Server-side (.cw Template Engine) â The C# backend compiles
.cwtemplate files into HTML by executing directives (@query,@foreach,@if, etc.) against a data context. - 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/ssrendpoint 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
@hookand@filterdirectives - Plugin integration â Bundle companion plugins via
bundled-plugins/ - v2-only visual editing â Theme editing focuses on runtime
.cwtemplates, 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
.cwtemplate files and.jsonconfiguration - 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:
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/Directory | Required | Description |
|---|---|---|
manifest.json | Yes | Theme metadata, template keys, header/footer presets, starter content, chrome presets, pseudo-code hooks |
templates/ | Yes | v2 .cw template files. At minimum include a home/index template |
partials/ | No | Header/footer partial .cw files (default and named presets), included via @include('filename') |
sections/ | No | Reusable template partials included via @include('section-name') |
assets/ | No | CSS, JS, images, fonts referenced by theme templates |
bundled-plugins/ | No | Companion 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.
{
"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
"2.0.0")3. CoreWave storefront themes are v2 .cw-only.key, label, and partial (path to the .cw file). Styling belongs in the partial and theme CSS, not in preset settings.key, label, and partial (path to the .cw file). Styling belongs in the partial and theme CSS, not in preset settings.headerPresetsfooterPresetstemplateKeys (manifest.json)
Each template key in templates maps to a route kind and must point to a .cw directive template.
| Key | Route Kind | Description |
|---|---|---|
catalogue.home | Home / Landing | Storefront index/home page |
catalogue.default | Catalog | Product category/collection listing |
product.detail | Product | Individual product detail page |
cart.default | Cart | Shopping cart page |
checkout.default | Checkout | Checkout/payment page |
login.default | Login | Customer login/sign-in page |
register.default | Register | Customer registration/sign-up page |
maintenance.* | Maintenance | Maintenance mode / offline page (any key with maintenance prefix) |
page.default | Page | Generic CMS content page |
page.not-found | 404 | Page not found |
blogs.default | Blogs | Blog index/listing |
post.default | Blog Post | Individual blog post |
account.default | Account | Customer account dashboard |
account.orders | Account Orders | Customer order history |
account.order-detail | Account Order Detail | Single order view |
account.wishlist | Account Wishlist | Customer wishlist |
account.returns | Account Returns | Customer returns |
account.reviews | Account Reviews | Customer reviews |
account.profile | Account Profile | Profile settings |
account.addresses | Account Addresses | Saved addresses |
account.invoices | Account Invoices | Customer invoices |
account.documents | Account Documents | Customer documents |
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.
{
"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
| Property | Type | Description |
|---|---|---|
globalStyles | Object | Global 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) |
templateSettings | Object | Per-template visual settings keyed by .cw template key or page handle |
customCss | String | Custom CSS injected at runtime |
cssVars | Object | CSS 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.
@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
{{--
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:
{{-- 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
<!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
@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:
@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.
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.
{{-- 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/.
@extends('layouts/main')
@section('content')
@include('hero-banner')
@var(__section_title, 'Best sellers')
@include('featured-products')
@endsection
Template Directive Reference #
Data & Loops
| Directive | Purpose | Example |
|---|---|---|
@query(__var, [...]) ... @endquery | Query 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 |
@else | Fallback inside @query / @if | @else <p>No items</p> @endquery |
@break | Exit loop early | @if(__index > 10) @break @endif |
@continue | Skip to next iteration | @if(__product.sold_out) @continue @endif |
Conditionals
| Directive | Purpose | Example |
|---|---|---|
@if(__condition) ... @endif | Conditional rendering | @if(__product.on_sale) <span>Sale!</span> @endif |
@elseif(__condition) | Else-if branch | @elseif(__product.featured) ... @endif |
@else | Else branch | @else ... @endif |
@unless(__condition) | Inverted condition (if not) | @unless(__product.sold_out) ... @endunless |
@isset(__var) ... @endisset | Check if variable is set | @isset(__product.rating) ... @endisset |
@empty(__var) ... @endempty | Check if variable is empty | @empty(__products) ... @endempty |
@switch(__var) ... @endswitch | Switch-case | @switch(__product.type) @case('simple') ... @endswitch |
Code Execution
| Directive | Purpose | Example |
|---|---|---|
@code ... @endcode | Inline 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
| Directive | Purpose | Example |
|---|---|---|
@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') ... @endsection | Define a content section | @section('content') ... @endsection |
@yield('name') | Render a section from parent layout | @yield('content') |
Widgets & Hooks
| Directive | Purpose | Example |
|---|---|---|
@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
| Directive | Purpose | Example |
|---|---|---|
@asset('path') | Theme asset URL | @asset('assets/js/main.js') |
@link('path') | Storefront page URL | @link('/about') |
__entity.url | Entity-specific URL | {{ __product.url }} |
__entity.image | Entity 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
| Directive | Purpose |
|---|---|
@is_home() ... @endis | Is the home page? |
@is_page('slug') ... @endis | Is a specific page? |
@is_product() ... @endis | Is a product detail page? |
@is_category('slug') ... @endis | Is a category page? |
@is_blog() ... @endis | Is the blog index? |
@is_single() ... @endis | Is a single blog post? |
@is_search() ... @endis | Is a search results page? |
@is_account() ... @endis | Is the customer account page? |
@is_cart() ... @endis | Is the cart page? |
@is_checkout() ... @endis | Is the checkout page? |
@has_products() ... @endis | Does the query have products? |
@has_image(__entity) ... @endis | Does the entity have an image? |
@user_logged_in() ... @endis | Is a customer logged in? |
@is_logged_in ... @endis_logged_in | Alias for @user_logged_in() â conditional block based on login status |
Stacks & Comments
| Directive | Purpose | Example |
|---|---|---|
{{-- comment --}} | Template comment (not rendered) | {{-- This won't appear in HTML --}} |
@stack('name') | Render a push stack position | @stack('footer-scripts') |
@push('name') ... @endpush | Push 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)
| Syntax | Supported? | Details |
|---|---|---|
'value' / "value" | Yes | String literals are unquoted before use. |
key=value | Yes | Classic named argument syntax. |
key: value | Yes | Recommended for readable single-line calls. |
['key' => 'value'] | Yes | Accepted for WordPress/PHP-like theme portability. |
__object.property | Yes | Works for C# objects, dictionaries, JSON objects, arrays with numeric indexes, and function return objects. |
cw_get_store().name | Yes | Function 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:
| Property | Description |
|---|---|
__loop.first | Is this the first iteration? |
__loop.last | Is this the last iteration? |
__loop.index | Zero-based index |
__loop.iteration | One-based index |
__loop.count | Total items in the loop |
__loop.remaining | Remaining items |
@query â The Core Data Directive
The @query directive queries the database and loops through results:
@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
| Parameter | Values | Description |
|---|---|---|
type | product, page, blog, post, category, customer, order, discount | Entity type to query |
category | string, slug | Filter by category slug |
category_id | int | Filter by category ID |
collection | string, slug | Filter by collection slug |
tags | string[] | Filter by tags |
ids | int[] | Specific IDs to fetch |
limit | int (default: 20) | Max results |
offset | int | Pagination offset |
page | int | Page number |
order | asc, desc | Sort direction |
orderby | price, title, date, popularity, rating, sales | Sort field |
featured | bool | Featured products only |
on_sale | bool | On-sale products only |
in_stock | bool | In-stock products only |
search | string | Search 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.
@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:
@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.
@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
@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
@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
@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
@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
__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:
@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
| Function | Returns | Description |
|---|---|---|
cw_get_product_tags(productId) | Collection of Tags | Get tags for a product |
cw_get_post_tags(postId) | Collection of Tags | Get tags for a blog post |
cw_get_tags(scope) | Collection of Tags | Get all tags (scope: products/posts/both) |
cw_get_tag(slug) | ?Tag | Get a single tag by slug |
cw_get_products_by_tag(tag, limit, offset) | Collection of Products | Get products by tag slug with pagination (offset defaults to 0) |
cw_get_posts_by_tag(tag, limit, offset) | Collection of Posts | Get posts by tag slug with pagination (offset defaults to 0) |
cw_get_discounts(limit) | Collection of Discounts | Get active discount codes |
cw_get_discount(code) | ?Discount | Get a discount by code |
cw_validate_discount(code, subtotal) | {"valid","error","type","value"} | Validate a discount against subtotal |
cw_get_store() | Store | Get store metadata and settings |
cw_get_store_info() | Store | Alias for cw_get_store(). Useful for themes ported from the v2 starter examples. |
cw_get_products(limit?, category?, tag?, ids?, orderby?, order?) | Collection of Products | Fetch 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 Products | Fetch 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 Categories | Fetch storefront product categories. Each item includes ID, name, slug/code fallback, description, and product count where available. |
cw_get_collections(limit?, orderby?) | Collection of Collections | Fetch product collections for collection grids, collection pickers, and landing page sections. |
cw_get_blogs() | Collection of Blogs | Fetch active storefront blogs with handles and post counts. |
cw_get_posts(limit?, category?/blog?, tag?, orderby?, order?) | Collection of Posts | Fetch 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/string | Returns 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() | string | Get store name from institution settings |
cw_get_store_currency_code() | string | Get store currency code (e.g., NGN) |
cw_has_image(entity) | bool | Check if entity has an image/thumbnail |
cw_get_related_products(productId, limit) | Collection of Products | Get related products by shared categories |
cw_get_cart_count() | int | Get cart item count from session |
cw_get_cart_applied_discounts() | Collection of Discounts | Get applied discounts from session |
cw_user_logged_in() | bool | Check if customer is logged in (session check) |
cw_submit_product_review(productId, rating, comment) | object | Submit a product review (rating 1-5) |
cw_product_reviews(productId, page, limit) | Collection of Reviews | Get paginated product reviews |
cw_get_customer_product_review(productId) | ?Review or null | Get the logged-in customer's review for a specific product (or null if not reviewed) |
cw_submit_post_comments(postId, name, email, comment) | object | Submit a blog post comment |
cw_post_comments(postId, page, limit) | Collection of Comments | Get paginated blog post comments |
cw_format_money(amount, currency) | string | Format a decimal as currency (default: NGN) |
cw_format_date(date, format) | string | Format a date string |
cw_slugify(input) | string | Convert text to URL-safe slug |
cw_truncate(text, length, ellipsis) | string | Truncate text with ellipsis (default 100 chars) |
cw_default(value, default) | string | Return default if value is empty |
cw_echo(...) | string | Output raw string value |
cw_count(var) | int | Count items in a collection variable |
cw_base_paths() | object | Get 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 objects | Get 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) | ?Navigation | Get a single navigation (menu) by its numeric ID with the full items tree. Returns null if not found |
cw_get_navigation_by_location(location) | ?Navigation | Get a single navigation by location key ("main" or "footer"). Returns null if no menu exists at that location |
cw_get_navigation_by_key(key) | ?Navigation | Get 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?) | object | Get 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) | object | Set the shopper's preferred currency in the session. Returns {"success": true, "code": "USD"} |
cw_get_current_currency() | object | Get the shopper's preferred currency. Returns {"code","symbol","name","is_base"}. Falls back to store base currency |
cw_get_available_currencies() | Collection | Get all active currencies available to the shopper. Each entry: {"code","symbol","name","is_base"} |
cw_convert_price(amount, to?) | object | Convert 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?) | object | Authenticate 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?) | object | Verify an OTP code and complete authentication. Returns redirectUrl in response |
cw_logout_user() | object | Log 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?) | object | Register 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?) | string | Generate a meta-refresh redirect snippet. Defaults to redirect_url from session, then /account |
cw_get_customer_profile() | object | Get 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?) | object | Update 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) | object | Update billing or shipping address. type is "billing", "shipping", or "both" |
cw_get_customer_orders(page?, limit?) | object | Get paginated order history |
cw_get_customer_order(order_id) | ?Order | Get a single order by ID |
cw_get_customer_invoices(page?, limit?) | object | Get paginated invoice list |
cw_get_customer_receipts(page?, limit?) | object | Get paginated receipt list |
cw_get_customer_documents(order_id?) | Collection of Documents | Get documents for an order or all orders |
cw_get_customer_wishlist() | Collection of Wishlist Items | Get customer wishlist items |
cw_add_to_customer_wishlist(product_id) | object | Add a product to wishlist |
cw_remove_from_customer_wishlist(product_id) | object | Remove a product from wishlist |
cw_get_customer_returns(page?, limit?) | object | Get paginated return requests |
cw_create_customer_return(order_id, reason, product_id?) | object | Create a return request |
cw_submit_return_request(order_id, reason, product_id?) | object | Alias for cw_create_customer_return â create a return request |
cw_cancel_return_request(return_id) | object | Cancel a pending return request |
cw_get_customer_reviews(page?, limit?) | object | Get paginated customer product reviews |
cw_get_customer_dashboard() | object | Get 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}):
| Accessor | Resolved Path | Description |
|---|---|---|
.home | {baseUrl}/ | Homepage URL |
.catalogue | {baseUrl}/ | Catalogue/listing page (same as home) |
.shop | {baseUrl}/shop | Shop listing page |
.pages | {baseUrl}/pages | Pages 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}/dashboard | Customer account dashboard |
.account_orders | {baseUrl}/{accountBase}/orders | Order history page |
.account_wishlist | {baseUrl}/{accountBase}/wishlist | Wishlist page |
.account_returns | {baseUrl}/{accountBase}/returns | Returns page |
.account_reviews | {baseUrl}/{accountBase}/reviews | Reviews page |
.account_profile | {baseUrl}/{accountBase}/profile | Profile page |
.account_addresses | {baseUrl}/{accountBase}/addresses | Addresses page |
.account_order_detail | {baseUrl}/{accountBase}/order-detail | Single order detail page |
.account_invoices | {baseUrl}/{accountBase}/invoices | Invoices page |
.account_documents | {baseUrl}/{accountBase}/documents | Documents page |
.cart | {baseUrl}/cart | Shopping cart page |
.checkout | {baseUrl}/checkout | Checkout page |
.login | {baseUrl}/login | Login page |
.register | {baseUrl}/register | Registration page |
.not_found | {baseUrl}/not-found | 404 not-found page |
@query Supported Types & Parameters
| Type | Parameters | Returns |
|---|---|---|
products | limit, orderby, order, category, category_id, tag/tags, ids, q/search, brand, product_type, min_price, max_price, in_stock, on_sale, featured | Collection of Product objects |
posts | limit, orderby, order, blog/blogHandle/blog_id, tag/tags, ids, q/search | Collection of Blog Post objects |
categories | limit, orderby, order, q/search, code, parent_id, active | Collection of Category objects |
collections | limit, orderby (title) | Collection of Collection objects |
blogs | limit, orderby, order, q/search, status | Collection of Blog objects |
discounts | limit | Collection of Discount objects |
tags | scope (products/posts/both), limit | Collection of Tag objects |
countries | limit, orderby (name) | Collection of Country objects |
regions | limit, orderby (name), country_id (int) | Collection of Region objects |
cities | limit, 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.
| Property | Type | Description |
|---|---|---|
__product.id | int | Product ID |
__product.title | string | Product name / title |
__product.slug | string | URL-safe product segment. Derived from SKU, then name, then ID. |
__product.url | string | Product detail URL using the configured product base, e.g. /{productBase}/{slug}. |
__product.sku | string | Stock keeping unit |
__product.description | string | Full description (HTML) |
__product.short_description | string | Truncated description (200 chars) |
__product.barcode | string | Barcode / UPC |
__product.price | decimal | Current unit price |
__product.compare_price | decimal? | Compare-at / original price (set via admin product form) |
__product.on_sale | bool | True when product is marked as on sale (set via admin product form) |
__product.discount_percent | decimal? | Discount percent (0-100) applied when on_sale is true |
__product.cost_price | decimal | Cost price (internal use) |
__product.stock_quantity | int | Current stock count |
__product.in_stock | bool | True when stock_quantity > 0 |
__product.rating | decimal | Average rating (0.0-5.0, computed from customer reviews) |
__product.review_count | int | Number of customer reviews |
__product.brand | string | Brand name |
__product.manufacturer | string | Manufacturer name |
__product.product_type | string | Product type enum text. |
__product.template_key | string? | Optional product-specific template key. |
__product.image | string? | Primary image URL (may be null) |
__product.thumbnail | string? | Primary thumbnail URL |
__product.images | array | Array of Image objects (see Image Object below) |
__product.categories | array | Array of {id, name, slug} objects |
__product.tags | array | Array of {id, name, slug} objects |
__product.created_at | datetime | Creation timestamp |
__product.updated_at | datetime | Last update timestamp |
Image Object
Returned inside __product.images array and __post.images array.
| Property | Type | Description |
|---|---|---|
__image.url | string | Full image URL |
__image.alt | string | Alt text |
__image.width | int | Image width in pixels |
__image.height | int | Image height in pixels |
__image.thumbnail | string? | Thumbnail-size image URL |
__image.is_cover | bool | True if this is the primary/cover image |
Category Object
Returned by @query(['type' => 'categories', ...]). Automatically available as __category on category pages.
| Property | Type | Description |
|---|---|---|
__category.id | int | Category ID |
__category.name | string | Display name |
__category.slug | string | URL slug derived from category code, then name. |
__category.code | string | Category code used by URL/category filtering when available. |
__category.description | string | Description |
__category.parent_id | int? | Parent category ID. |
__category.display_order | int | Manual display order. |
__category.template_key | string? | Optional category-specific template key. |
__category.product_count | int | Active products in category |
Collection Object
Returned by @query(['type' => 'collections', ...]).
| Property | Type | Description |
|---|---|---|
__collection.id | int | Collection ID |
__collection.title | string | Collection title |
__collection.description | string | Collection description |
__collection.duration | string | Optional duration (e.g. "3 months") |
__collection.thumbnail | string? | Thumbnail image URL |
__collection.display_order | int | Display order |
__collection.is_active | bool | Is the collection active? |
__collection.handle | string | URL handle (same as title) |
__collection.product_count | int | Active products in collection |
Blog Object
Returned by @query(['type' => 'blogs']).
| Property | Type | Description |
|---|---|---|
__blog.id | int | Blog ID |
__blog.name | string | Blog name |
__blog.slug | string | URL handle |
__blog.description | string | Description (HTML) |
__blog.post_count | int | Number of posts |
Blog Post Object
Returned by @query(['type' => 'posts', ...]). Automatically available as __post on post detail pages.
| Property | Type | Description |
|---|---|---|
__post.id | int | Post ID |
__post.title | string | Post title |
__post.slug | string | URL slug (handle) |
__post.excerpt | string | Summary / excerpt |
__post.content | string | Full content (JSON/HTML) |
__post.content_json | string | Raw content JSON |
__post.cover_image | string? | Cover image URL (may be null) |
__post.published_at | datetime | Publish timestamp |
__post.status | string | Published / Draft |
__post.blog_id | int | Parent blog ID |
__post.categories | array | Array of {id, name, slug} objects |
__post.tags | array | Array of {id, name, slug} objects |
__post.created_at | datetime | Creation timestamp |
__post.updated_at | datetime | Last update timestamp |
Discount Code Object
Returned by @query(['type' => 'discounts', ...]) and cw_get_discount().
| Property | Type | Description |
|---|---|---|
__discount.id | int | Discount ID |
__discount.code | string | Discount code |
__discount.display_name | string | Display name |
__discount.type | string | Discount type |
__discount.value | decimal | Amount or percentage |
__discount.description | string | Description |
__discount.minimum_subtotal | decimal | Minimum order subtotal |
__discount.maximum_discount_amount | decimal | Maximum discount cap |
__discount.is_free_shipping | bool | Grants free shipping |
__discount.starts_at | datetime | Start date |
__discount.ends_at | datetime | Expiry date |
__discount.applies_to_all | bool | Applies to all products |
Tag Object
Returned by @query(['type' => 'tags', ...]) and tag lookup functions.
| Property | Type | Description |
|---|---|---|
__tag.id | int | Tag ID |
__tag.name | string | Display name |
__tag.slug | string | URL-safe slug |
__tag.scope | string | Scope (products / posts / both) |
__tag.product_count | int | Tagged products count |
__tag.post_count | int | Tagged posts count |
Country Object
Returned by @query(['type' => 'countries', ...]).
| Property | Type | Description |
|---|---|---|
__country.id | int | Country ID |
__country.name | string | Country name |
__country.country_code_2 | string | Two-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.
| Property | Type | Description |
|---|---|---|
__region.id | int | Region ID |
__region.name | string | Region / state name |
__region.country_id | int | Parent country ID |
__region.short_code | string? | 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.
| Property | Type | Description |
|---|---|---|
__city.id | int | City ID |
__city.name | string | City / district name |
__city.region_id | int | Parent region ID |
Store Object
Returned by cw_get_store().
| Property | Type | Description |
|---|---|---|
__store.id | int | Storefront ID |
__store.name | string | Store / business name |
__store.slug | string | Store URL handle |
__store.tagline | string? | Tagline (currently null) |
__store.logo | string? | Logo URL from theme design |
__store.logo_url | string? | Logo URL alias |
__store.favicon | string? | Favicon URL from theme design |
__store.favicon_url | string? | Favicon URL alias |
__store.primary_color | string? | Primary brand color (hex) |
__store.secondary_color | string? | Secondary brand color (hex) |
__store.currency | string | Currency code (NGN) |
__store.currency_code | string | Currency code alias |
__store.current_currency_code | string | Shopper's preferred currency code (from session/localStorage cache), falls back to base currency |
__store.current_currency_symbol | string | Shopper's preferred currency symbol (e.g., $) |
__store.current_currency_name | string | Shopper's preferred currency name (e.g., US Dollar) |
__store.current_currency_is_base | bool | Whether the shopper's preferred currency is the store's base currency |
__store.language | string | Language code (en) |
__store.language_code | string | Language code alias |
__store.timezone | string | Timezone (Africa/Lagos) |
__store.email | string? | Contact email from CMS |
__store.phone | string? | Contact phone from CMS |
__store.address | string? | Contact address from CMS |
__store.social_links | string? | Social links (JSON) |
Product Review Functions
Review functions allow customers to submit and browse product reviews directly from templates.
{{-- 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:
| Accessor | Type | Description |
|---|---|---|
__review.author | string | Reviewer name (from Customer record) |
__review.avatar | string? | Profile picture URL for the reviewer (if they have one) |
__review.rating | int | Rating (1-5) |
__review.comment | string | Review text |
__review.date | string | Submission 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.
{{-- 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.
{{-- 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:
| Accessor | Type | Description |
|---|---|---|
__comment.author | string | Commenter name |
__comment.avatar | string | Gravatar / avatar URL |
__comment.comment | string | Comment content |
__comment.date | string | Submission 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.
{{-- 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Subscriber email address |
name | string | No | Subscriber name (optional) |
The function returns an object with the following properties:
| Property | Type | Description |
|---|---|---|
__result.success | bool | Whether the subscription was successful |
__result.message | string | Status 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:
- Call
cw_login_user(email, password, redirect_url?)â authenticates with email and password. Optionally passredirect_urlto specify where the customer should be redirected after successful login - 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 - Once authenticated, call any customer account function (orders, wishlist, profile, etc.). The session token is automatically stored in the template context
- Call
cw_logout_user()to end the session
{{-- 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
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:
- Client-side redirect (AJAX mode): Call
cw_login_user(email, password, redirect_url)orcw_register_user(first_name, last_name, email, password, ..., redirect_url)with aredirect_urlparameter. The response includes aredirectUrlfield that your frontend JS can use to navigate the customer (e.g.,window.location.href = result.redirectUrl). - 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 aredirectUrlfield, and the server sets a session cookie so the next page load recognizes the authenticated customer. - 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 theredirect_urlpreviously set during login/register, falling back to/account. This is useful for non-JS fallback scenarios.
{{-- 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_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
{{-- 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
{{-- 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().
| Property | Type | Description |
|---|---|---|
__customer.id | int | Customer ID |
__customer.first_name | string | Customer first name |
__customer.last_name | string | Customer last name |
__customer.other_names | string? | Customer other/middle names (optional) |
__customer.email | string | Customer email address |
__customer.phone | string? | Customer phone number |
__customer.zip_code | string? | Customer zip/postal code |
__customer.billing_address | string? | Billing address |
__customer.shipping_address | string? | Shipping address |
__customer.extra_info | Collection | Additional custom fields (title, value pairs) |
__customer.profile_picture_asset_id | int? | Media Library asset ID for the customer's profile picture |
__customer.profile_picture_url | string? | Public URL of the customer's profile picture (resolved from Media Library) |
__customer.country_id | int? | Country ID for geographic location |
__customer.region_id | int? | Region/State ID for geographic location |
__customer.city_id | int? | City ID for geographic location |
__customer.country_id | int? | Country ID for geographic location |
__customer.region_id | int? | Region/State ID for geographic location |
__customer.city_id | int? | City ID for geographic location |
__customer.country_id | int? | Country ID for geographic location |
__customer.region_id | int? | Region/State ID for geographic location |
__customer.city_id | int? | City ID for geographic location |
Customer Summary Object
Returned as __profile.summary from cw_get_customer_profile() and __session.summary from cw_verify_login().
| Property | Type | Description |
|---|---|---|
__summary.total_orders | int | Total number of orders placed |
__summary.paid_orders | int | Number of paid/completed orders |
__summary.total_spend | decimal | Total amount spent across all orders |
__summary.currency | string | Currency code (e.g., NGN) |
__summary.latest_order_at | string? | 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.
{{-- 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.
{{-- 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
{{-- 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().
| Property | Type | Description |
|---|---|---|
__order.id | int | Order/checkout ID |
__order.reference | string | Order reference number |
__order.status | string | Order status (Pending, Paid, Failed, etc.) |
__order.currency_code | string | Currency code (e.g., NGN) |
__order.amount | decimal | Total order amount paid by customer (includes subtotal, discount, delivery, tax, processing fee, and regulatory taxes) |
__order.fee_amount | decimal? | Transaction/payment fee charged to customer (or absorbed by seller if PassProcessingFeeToCustomer is false) |
__order.net_amount | decimal? | Net amount after fees (merchant receives this after all fees and regulatory charges) |
__order.grand_total | decimal? | Full grand total: subtotal - discount + delivery + tax + processing fee + processing fee tax + shipping fee tax |
__order.processing_fee | decimal? | Processing/gateway fee amount (before regulatory charge) |
__order.processing_fee_tax | decimal? | Regulatory charge (7.5%) on the processing fee. Hidden from customer breakdown, deducted from merchant revenue |
__order.shipping_fee_tax | decimal? | Regulatory charge (7.5%) on the shipping/delivery fee. Hidden from customer breakdown, deducted from merchant revenue |
__order.customer_name | string | Customer name on order |
__order.customer_email | string | Customer email on order |
__order.customer_phone | string? | Customer phone on order |
__order.customer_address | string? | Customer address on order |
__order.created_at | string | Order creation date/time |
__order.paid_at | string? | Payment completion date |
__order.delivery_status | string? | Delivery status (Pending, Shipped, Delivered) |
__order.delivered_at | string? | Delivery completion date |
__order.items | Collection | Order line items (products with quantity, price) |
__order.item_count | int | Number of items in the order |
__order.lifecycle | object | Order lifecycle phases (pending, confirmed, paid, fulfilled, delivered) |
__order.has_invoice | bool | Whether an invoice exists for this order |
__order.invoice_id | int? | Invoice ID if generated |
__order.invoice_reference | string? | Invoice reference number |
__order.invoice_html | string? | Rendered invoice HTML |
__order.document_count | int | Number of documents attached to this order |
__order.documents | Collection | Order documents (receipts, invoices, contracts) |
__order.return_request_count | int | Number of return requests for this order |
__order.returns | Collection | Return requests for this order |
__order.payment_reference | string? | Payment gateway reference |
Invoices
{{-- 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
{{-- 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
{{-- 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:
| Property | Type | Description |
|---|---|---|
__doc.order_id | int | Order/checkout ID |
__doc.order_reference | string | Order reference number |
__doc.document_id | int | Document ID |
__doc.document_type | string | Document type (receipt / invoice / contract) |
__doc.rendered_html | string? | Rendered document HTML |
__doc.file_url | string? | Document file URL (PDF or other format) |
__doc.created_at | string | Document creation date |
Wishlist
{{-- 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:
| Property | Type | Description |
|---|---|---|
__item.id | int | Wishlist item ID |
__item.product_id | int | Product ID |
__item.product_name | string | Product name |
__item.product_image | string? | Product image URL |
__item.product_price | decimal | Product price |
__item.added_at | string | Date added to wishlist |
Returns
{{-- 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:
| Property | Type | Description |
|---|---|---|
__return.id | int | Return request ID |
__return.checkout_id | int | Order/checkout ID |
__return.order_reference | string | Order reference number |
__return.product_id | int? | Product ID (if item-specific) |
__return.product_name | string? | Product name |
__return.reason | string | Return reason |
__return.status | string | Return status (Pending, Approved, Declined, Refunded) |
__return.created_at | string | Request creation date |
__return.updated_at | string? | Last update date |
Reviews
{{-- 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:
| Property | Type | Description |
|---|---|---|
__review.id | int | Review ID |
__review.product_id | int | Product ID |
__review.product_name | string | Product name |
__review.rating | int | Rating (1-5) |
__review.review_text | string? | Review content text |
__review.created_at | string | Review submission date |
Dashboard Object
Returned by cw_get_customer_dashboard().
| Property | Type | Description |
|---|---|---|
__dashboard.summary | object | Customer summary (total_orders, paid_orders, total_spend, currency, latest_order_at) |
__dashboard.wishlist | Collection | Wishlist items |
__dashboard.returns | Collection | Return requests |
__dashboard.reviews | Collection | Customer reviews |
__dashboard.spend_chart | Collection | Spending chart data (period, amount pairs) |
Filter Reference
Filters transform content inline using pipe syntax:
{{-- 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
| Filter | Arguments | Description |
|---|---|---|
| money | currency (default: NGN) | Format amount as currency string |
| date | format (default: M d, Y) | Format date string |
| slugify | â | Convert to URL-safe slug |
| truncate | length (default: 100), ellipsis (default: ...) | Truncate text |
| default | default | Fallback 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 Key | Purpose | Available 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:
| Variable | Description |
|---|---|
[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 |
{{-- 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
{{-- 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
{{-- 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.
| Route | Kind | Injected Variables | Default Template |
|---|---|---|---|
/ | catalogue or page | __route.kind | catalogue.home / configured homepage |
/pages/{pageHandle} | page | __route.pageHandle, __page | page.{handle} then page.default |
/{productBase}/{id|sku|slug} | product | __route.productSlug, __product | Item TemplateKey, Storefront Overview default, then product.detail |
/{categoryArchiveBase}/{category} | catalogue | __route.categorySlug, __category | catalogue.home |
/{blogBase} | blogs | __route.kind | blogs.default |
/{blogBase}/{blogHandle} | blog | __route.blogHandle, __blog | Blog TemplateKey, Storefront Overview default, then blog.default |
/{blogBase}/{blogHandle}/{postHandle} | post | __route.blogHandle, __route.postHandle, __blog, __post | Post TemplateKey, Storefront Overview default, then post.default |
Current Route Functions
@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
| Function | Returns | Description |
|---|---|---|
cw_current_route() | object | Returns kind, productSlug, categorySlug, blogSlug, postSlug, and related route fields. |
cw_current_product_id() | string | Current product ID when the product was resolved. |
cw_current_product_slug() | string | The product URL segment. Product URLs accept numeric ID, SKU, or slugified product name. |
cw_current_category_slug() | string | Current category route segment. |
cw_current_blog_id() | string | Current blog ID when available. |
cw_current_blog_slug() / cw_current_blog_handle() | string | Current blog handle from the URL. |
cw_current_post_id() | string | Current blog post ID when available. |
cw_current_post_slug() / cw_current_post_handle() | string | Current 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.
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.
{{-- 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()).
{{-- 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.
{{-- 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
| Function | Lookup Arguments | Returns |
|---|---|---|
cw_get_product() | id, productId, slug, handle, sku | One product object or null. |
cw_get_category() | id, categoryId, slug, code | One category object or null. |
cw_get_blog() | id, blogId, slug, handle | One published blog object or null. |
cw_get_post() | id, postId, slug, handle, optional blog/blog_id | One 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
@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 Filter | Description |
|---|---|
category, category_id | Matches category ID, code, exact name, slugified code, or slugified name. |
tag, tags | Matches one or more product tag slugs. Comma-separated values are accepted. |
ids, id | Comma-separated product IDs. |
q, search | Searches name, description, SKU, brand, and manufacturer. |
brand | Exact brand match. |
product_type | Matches product type enum text such as Goods or Service. |
min_price, max_price | Unit price range. |
in_stock, on_sale, featured | Boolean filters. featured maps to on-sale/discounted products in the current runtime. |
orderby | name, title, sku, brand, price, stock, stock_quantity, created_at, updated_at. |
Categories
@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 Filter | Description |
|---|---|
q, search | Searches name, description, and code. |
code | Exact category code match. |
parent_id | Limits results to children of a parent category. |
active | Boolean active/inactive filter. |
orderby | name, display_order, or created_at. |
Blogs & Posts
@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 Filter | Description |
|---|---|
blogs: q/search | Searches blog name, handle, and description. |
blogs: status | published by default. Use any to include all statuses in trusted/admin previews. |
blogs: orderby | name, published_at, or created_at. |
posts: blog, blogHandle, blog_id | Filters posts by parent blog handle/name/ID. |
posts: tag, tags | Filters by one or more tag slugs. |
posts: ids, id | Comma-separated post IDs. |
posts: q/search | Searches title, excerpt, and content JSON. |
posts: orderby | title, published_at, created_at, or updated_at. |
Template Examples #
Product Card Partial
File: sections/product-card.cw
{{--
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
{{--
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
{{--
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
| Directory | Purpose |
|---|---|
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
{{-- 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:
| Priority | Files |
|---|---|
| 1 (First) | bootstrap.min.css |
| 2 | font-awesome.css, icofont.css, flaticon.css, themify.css |
| 3 | animate.css, swiper.css, owl.carousel.css |
| 4 | magnific-popup.css, jquery-ui.css |
| 5 | preloader.css |
| 6 | global.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.
<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.
| Key | Label | Description |
|---|---|---|
home | Home | Storefront landing/index page |
page | Generic Page | Standard CMS content pages |
blog.archive | Blog Archive | Blog listing/index page |
blog.single | Blog Single | Individual blog post page |
product.archive | Product Archive | Product catalog listing page |
product.single | Product Single | Individual product detail page |
category.archive | Category Archive | Product category listing page |
search | Search Results | Search results page |
cart | Cart | Shopping cart page |
checkout | Checkout | Checkout/payment page |
account | Account | Customer account dashboard |
header | Header | Header surface slot |
footer | Footer | Footer surface slot |
404 | 404 | Page 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.
@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
{
"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-commandblock
Available Hooks
| Hook | Description |
|---|---|
corewave.customer.after_signup | Fires after a customer registers |
corewave.customer.after_login | Fires after customer login |
corewave.customer.after_logout | Fires after customer logout |
corewave.customer.profile.updated | Fires when customer updates profile |
corewave.order.created | Fires when a new order is placed |
corewave.order.paid | Fires when payment is confirmed |
corewave.order.fulfilled | Fires when order is fulfilled |
corewave.order.cancelled | Fires when order is cancelled |
corewave.checkout.before_submit | Fires before checkout submission |
corewave.checkout.after_submit | Fires after checkout submission |
corewave.catalogue.product.created | Fires when a product is created |
corewave.catalogue.product.updated | Fires when a product is updated |
corewave.inventory.low_stock | Fires when stock falls below threshold |
corewave.inventory.out_of_stock | Fires when product goes out of stock |
corewave.blog.post.created | Fires when a blog post is published |
corewave.blog.comment.created | Fires when a new comment is added |
corewave.notifications.email.send | Fires before sending an email notification |
corewave.notifications.sms.send | Fires before sending an SMS notification |
corewave.theme.page.render | Fires before a storefront page is rendered |
corewave.theme.page.rendered | Fires after a storefront page is rendered |
Available Actions
| Action | Description |
|---|---|
send_email | Send a transactional email using a template |
send_sms | Send an SMS notification |
redirect | Redirect customer to a specific URL |
apply_discount | Auto-apply a discount code to the cart |
add_order_note | Add a note to the order |
add_order_tag | Tag an order with a label |
tag_customer | Assign a tag to the customer |
assign_segment | Assign customer to a segment |
inject_html | Inject HTML at page head_end or body_end |
log_event | Log an event for debugging/audit |
Block Fields Reference
| Field | Required | Description |
|---|---|---|
id | Yes | Unique identifier (a-z, A-Z, 0-9, _, -, max 80 chars) |
hook | Yes | The lifecycle event to bind to |
action | Yes | The action to execute when the hook fires |
when | No | Optional condition expression (max 500 chars) |
args | No | Configuration 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
{
"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:
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:
{{-- 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:
"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:
{-- 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
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:
{-- 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:
{-- Widget area â admins can place any widget here --}
<aside class="sidebar">
@widget('blog-sidebar')
</aside>
@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
| Field | Type | Description |
|---|---|---|
label | string | Display name for the widget shown in admin UI |
description | string | Short description of what the widget does |
template | string | Relative path to the .cw template file (e.g. widgets/featured-products.cw) |
fields | object | Map of field names to their schemas (label, type, default, options, enum, source) |
Field Type Reference
| Type | Description | Default Value |
|---|---|---|
text | Single-line text input | "" |
textarea | Multi-line text input | "" |
number | Numeric input | 0 |
boolean | Yes/No toggle | false |
select | Dropdown 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, cities | First option or "" |
multi-select | Checkbox 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. | [] |
color | Color picker | #000000 |
image | Media library image picker | "" |
link | URL/link picker | "" |
Best Practices
- Keep widget templates focused â each widget should do one thing well
- Provide sensible defaults for all configuration fields
- Use
@queryinside 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:
| Mode | Property | Description |
|---|---|---|
| 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):
{
"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.
@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:
{-- 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:
{
"fields": {
"category": {
"label": "Category Filter",
"type": "select",
"default": "",
"source": "categories"
},
"products": {
"label": "Featured Products",
"type": "multi-select",
"default": [],
"source": "products"
}
}
}
{-- 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
| Source | Returns | Available Parameters |
|---|---|---|
products | Product options (id / title) | limit, orderBy, order, ids |
categories | Category options (slug / title) | limit, orderBy, order |
collections | Collection options (slug / title) | limit, orderBy |
blogs | Blog options (slug / title) | |
posts | Blog post options (id / title) | limit, orderBy, order, blog, tag |
tags | Tag options (slug / name) | scope (products, posts, or both), limit |
discounts | Discount options (code / title) | limit |
countries | Country options (code / name) | limit, orderBy |
regions | Region options (code / name) | limit, orderBy, parentId (country ID) |
cities | City options (id / name) | limit, orderBy, parentId (region ID) |
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.
{
"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
- Fetch institution bootstrap data (settings, active theme info)
- Fetch the active theme's runtime index via the theme runtime client
- Load CSS assets sequentially in priority order
- Load JavaScript assets sequentially in priority order
- Resolve the appropriate template for the current page
- Fetch the rendered
.cwtemplate via the Template Engine API - 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.
| Endpoint | Purpose | Returns |
|---|---|---|
GET /v1/public/storefront/templates/render | Render one .cw template fragment by templateKey. Used by the React storefront runtime. | text/html fragment |
GET /v1/public/storefront/ssr | Resolve 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.
"starterContent": { "catalogue.home": { "name": "Home", "status": "Published" }, "catalogue.default": { "name": "Catalog", "status": "Published" } }
| Field | Type | Description |
|---|---|---|
name | String | Display name for the template instance |
status | String | "Published" or "Draft" |
homeTemplateKey | String | Optional override for the recommended home template key |
recommendedAssignments | Object | Optional route-to-template mapping overrides |
menus | Object | Named 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) |
pages | Array | Array 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.
"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 } ] }
| Field | Type | Description |
|---|---|---|
title | String | Page title displayed in the browser tab and admin UI |
handle | String | URL slug for the page (e.g. "about-us" â /about-us) |
templateKey | String? | Template key to render this page (e.g. "page.default") |
headerPresetKey | String? | Key of the header preset to render on this page. Omit or set null to use the storefront's defaultHeaderPresetKey |
footerPresetKey | String? | Key of the footer preset to render on this page. Omit or set null to use the storefront's defaultFooterPresetKey |
contentHtml | String? | Optional raw HTML content body for the page (seeded as ContentJson) |
showInNavigation | Boolean | Whether the page appears in automatic navigation menus |
sortOrder | Int | Display order for navigation sorting (lower = first) |
publish | Boolean | Whether the page is published immediately (true) or created as draft (false) |
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.
Menu Seeding
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.
"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 } ] } }
| Field | Type | Description |
|---|---|---|
label | String | Display text for the menu link |
pageHandle | String? | Handle of a seeded page to link to (e.g. "about-us") |
url | String? | Custom URL override (e.g. "/shop", "https://example.com") |
sortOrder | Int | Display order (lower = first) |
children | Array? | Nested child items (same structure), enabling dropdown/submenu navigation. Supports arbitrary depth |
children array supports multilevel nesting for dropdown menus.
"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.
| Property | Type | Description |
|---|---|---|
__product.id | int | Product ID |
__product.title | string | Product name / title |
__product.sku | string | Stock keeping unit |
__product.description | string | Full description (HTML) |
__product.short_description | string | Truncated description (200 chars) |
__product.barcode | string | Barcode / UPC |
__product.price | decimal | Current unit price |
__product.compare_price | decimal? | Compare-at / original price |
__product.discount_percent | decimal? | Discount percent (0-100) applied when on_sale is true |
__product.cost_price | decimal | Cost price (internal use) |
__product.stock_quantity | int | Current stock count |
__product.in_stock | bool | true when stock_quantity > 0 |
__product.on_sale | bool | Sale status (currently false) |
__product.brand | string | Brand name |
__product.manufacturer | string | Manufacturer name |
__product.image | string | Primary thumbnail URL |
__product.thumbnail | string | Primary thumbnail URL (same as image) |
__product.images | array | Array of Image objects {url, alt, width, height} |
__product.categories | array | Array of {id, name, slug} objects |
__product.tags | array | Array of {id, name, slug} objects |
__product.created_at | datetime | Creation timestamp |
__product.updated_at | datetime | Last update timestamp |
Image Object
Returned inside __product.images array:
| Property | Type | Description |
|---|---|---|
__image.url | string | Full image URL |
__image.alt | string | Alt text (product name) |
__image.width | int | Image width (may be 0) |
__image.height | int | Image height (may be 0) |
Category Object
Returned by @query(['type' => 'categories', ...]):
| Property | Type | Description |
|---|---|---|
__category.id | int | Category ID |
__category.name | string | Category display name |
__category.slug | string | URL slug (same as name) |
__category.description | string | Category description |
__category.product_count | int | Number of active products in category |
Blog Object
Returned by @query(['type' => 'blogs']):
| Property | Type | Description |
|---|---|---|
__blog.id | int | Blog ID |
__blog.name | string | Blog name |
__blog.slug | string | URL slug (blog handle) |
__blog.description | string | Blog description (HTML) |
__blog.post_count | int | Number 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.
| Property | Type | Description |
|---|---|---|
__post.id | int | Post ID |
__post.title | string | Post title |
__post.slug | string | URL slug (post handle) |
__post.excerpt | string | Post excerpt / summary |
__post.content | string | Full post content (JSON/HTML) |
__post.content_json | string | Raw content JSON |
__post.cover_image | string? | Cover image URL (may be null) |
__post.published_at | datetime | Publish timestamp |
__post.status | string | Post status (Published/Draft/etc) |
__post.blog_id | int | Parent blog ID |
__post.categories | array | Array of {id, name, slug} objects |
__post.tags | array | Array of {id, name, slug} objects |
__post.created_at | datetime | Creation timestamp |
__post.updated_at | datetime | Last update timestamp |
Store Object
Returned by cw_get_store():
| Property | Type | Description |
|---|---|---|
__store.id | int | Storefront ID |
__store.name | string | Store / business name |
__store.slug | string | Store URL handle |
__store.tagline | string? | Store tagline (may be null) |
__store.logo | string? | Logo URL from theme design |
__store.logo_url | string? | Logo URL (alias of logo) |
__store.favicon | string? | Favicon URL from theme design |
__store.favicon_url | string? | Favicon URL (alias of favicon) |
__store.primary_color | string? | Brand primary color (hex) |
__store.secondary_color | string? | Brand secondary color (hex) |
__store.currency | string | Currency code (currently "NGN") |
__store.currency_code | string | Currency code (alias of currency) |
__store.language | string | Language code (currently "en") |
__store.language_code | string | Language code (alias of language) |
__store.timezone | string | Timezone (currently "Africa/Lagos") |
__store.email | string? | Contact email from CMS settings |
__store.phone | string? | Contact phone from CMS settings |
__store.address | string? | Contact address from CMS settings |
__store.social_links | string? | 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):
| Property | Type | Description |
|---|---|---|
__nav.id | int | Menu ID |
__nav.name | string | Menu display name |
__nav.slug | string | URL-friendly key (e.g., "main-menu") |
__nav.location | string | Menu location ("main" or "footer") |
__nav.active | bool | Whether the menu is active |
__nav.items | array | Array of Menu Item objects with optional nested children |
Menu Item Object
Returned inside __nav.items arrays:
| Property | Type | Description |
|---|---|---|
__item.id | int | Menu item ID |
__item.menu_id | int | Parent menu ID |
__item.parent_item_id | int? | Parent item ID (null for root items) |
__item.label | string | Display text for the link |
__item.url | string? | Resolved URL (custom URL or page link) |
__item.page_id | int? | Linked page ID (if any) |
__item.sort_order | int | Display order |
__item.visible | bool | Whether the item is visible |
__item.children | array | Nested child items (same structure, for dropdown/sub-menus) |
Discount Code Object
Returned by @query(['type' => 'discounts']) and cw_get_discount():
| Property | Type | Description |
|---|---|---|
__discount.id | int | Discount ID |
__discount.code | string | Discount code |
__discount.display_name | string | Display name for admin |
__discount.type | string | Discount type |
__discount.value | decimal | Discount value (amount or percentage) |
__discount.description | string | Discount description |
__discount.minimum_subtotal | decimal | Minimum order subtotal required |
__discount.maximum_discount_amount | decimal | Maximum discount cap |
__discount.is_free_shipping | bool | Grants free shipping |
__discount.starts_at | datetime | Discount start date |
__discount.ends_at | datetime | Discount expiry date |
__discount.applies_to_all | bool | Applies to all products |
Tag Object
Returned by @query(['type' => 'tags', ...]) and tag functions:
| Property | Type | Description |
|---|---|---|
__tag.id | int | Tag ID |
__tag.name | string | Display name |
__tag.slug | string | URL-safe slug |
__tag.scope | string | Scope (products/posts/both) |
__tag.product_count | int | Number of tagged products |
__tag.post_count | int | Number 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 Kind | Template Key Pattern | Example |
|---|---|---|
| Home / Landing | catalogue.home or catalogue.{custom} | catalogue.home, catalogue.landing |
| Catalog / Category | catalogue.default | catalogue.default |
| Product Detail | product.detail | product.detail |
| Cart | cart.default | cart.default |
| Checkout | checkout.default | checkout.default |
| Blog Index | blogs.default | blogs.default |
| Blog Post | post.default | post.default |
| CMS Page | page.default | page.default |
| 404 | page.not-found | page.not-found |
| Login | login.default | login.default |
| Register | register.default | register.default |
| Account Dashboard | account.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.
| Field | Type | Default | JSON Path | Description |
|---|---|---|---|---|
| Default logo | string (URL) | "" | appearanceJson.branding.logoDefault | Primary logo displayed on the storefront. Falls back to logo if logoDefault is empty. |
| Logo for light background | string (URL) | "" | appearanceJson.branding.logoLight | Alternative logo optimized for light-colored backgrounds. |
| Logo for dark background | string (URL) | "" | appearanceJson.branding.logoDark | Alternative logo optimized for dark-colored backgrounds. |
| Favicon | string (URL) | "" | appearanceJson.branding.favicon | Browser tab icon and bookmark icon for the storefront. |
| Site tagline | string | "" | appearanceJson.branding.siteTagline | Short descriptive phrase shown alongside the site title on the storefront. |
{
"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 Label | JSON Path | Type | Default | Range | Description |
|---|---|---|---|---|---|
| Blog pages show at most | cmsSettingsJson.reading.postsPerPage | integer | 10 | 1 â 200 | Maximum number of blog posts displayed per page on blog index views. |
| Syndication feeds show the most recent | cmsSettingsJson.reading.feedItemsCount | integer | 10 | 1 â 200 | Number of most recent items included in RSS/Atom syndication feeds. |
| For each post in a feed, include | cmsSettingsJson.reading.feedContentMode | enum | "full" | "full" | "excerpt" | Whether feed items contain the full post body or just an excerpt. |
| Discourage search engines from indexing | cmsSettingsJson.reading.discourageSearchIndexing | boolean | false | true / false | When 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 Label | JSON Path | Type | Default | Description |
|---|---|---|---|---|
| Attempt to notify any blogs linked to from the post | cmsSettingsJson.discussion.defaultPost.notifyLinkedBlogs | boolean | true | Sends pingback notifications to URLs referenced in new posts. |
| Allow link notifications from other blogs (pingbacks and trackbacks) | cmsSettingsJson.discussion.defaultPost.allowPingbacks | boolean | true | Accepts incoming pingback/trackback notifications from other blogs. |
| Allow people to submit comments on new posts | cmsSettingsJson.discussion.defaultPost.allowCommentsOnNewPosts | boolean | true | Globally enables comments on new blog posts (can be overridden per-post). |
Other Comment Settings
| UI Label | JSON Path | Type | Default | Range | Description |
|---|---|---|---|---|---|
| Comment author must fill out name and email | cmsSettingsJson.discussion.other.requireNameEmail | boolean | true | true / false | Requires comment authors to provide both name and email fields. |
| Users must be registered and logged in to comment | cmsSettingsJson.discussion.other.requireLogin | boolean | false | true / false | Only allows authenticated users to submit comments. |
| Automatically close comments on old posts | cmsSettingsJson.discussion.other.autoCloseComments | boolean | false | true / false | Enables automatic comment closing after a configurable number of days. |
| Close comments when post is this many days old | cmsSettingsJson.discussion.other.closeAfterDays | integer | 14 | 1 â 3650 | Number of days after which comments are automatically closed. |
| Show comments cookies opt-in checkbox | cmsSettingsJson.discussion.other.showCookiesOptIn | boolean | true | true / false | Displays a GDPR/privacy cookie consent checkbox on the comment form. |
| Enable threaded (nested) comments | cmsSettingsJson.discussion.other.enableThreadedComments | boolean | true | true / false | Allows replies to comments, creating nested comment threads. |
| Number of levels for threaded comments | cmsSettingsJson.discussion.other.threadedLevels | integer | 5 | 2 â 10 | Maximum nesting depth for threaded comment replies. |
| Break comments into pages | cmsSettingsJson.discussion.other.breakCommentsIntoPages | boolean | true | true / false | Paginates comments when there are more than the per-page limit. |
| Top level comments per page | cmsSettingsJson.discussion.other.commentsPerPage | integer | 50 | 1 â 500 | Number of top-level comments displayed per comment page. |
| Comments page to display by default | cmsSettingsJson.discussion.other.defaultCommentsPage | enum | "last" | "last" | "first" | Which comment page to show by default (newest or oldest first). |
| Comments to display at top of each page | cmsSettingsJson.discussion.other.commentsSort | enum | "older" | "older" | "newer" | Sort order of comments within each page. |
Email Me Whenever
| UI Label | JSON Path | Type | Default | Description |
|---|---|---|---|---|
| Anyone posts a comment | cmsSettingsJson.discussion.other.emailOnAnyComment | boolean | false | Sends an email notification for every new comment. |
| A comment is held for moderation | cmsSettingsJson.discussion.other.emailOnModeration | boolean | false | Sends an email when a comment is queued for manual moderation. |
| Anyone posts a note | cmsSettingsJson.discussion.other.emailOnNote | boolean | false | Sends an email when a note (internal moderation note) is posted. |
Before a Comment Appears
| UI Label | JSON Path | Type | Default | Range | Description |
|---|---|---|---|---|---|
| Comment must be manually approved | cmsSettingsJson.discussion.other.mustApproveManually | boolean | false | true / false | All comments must be approved by a moderator before becoming visible. |
| Comment author must have a previously approved comment | cmsSettingsJson.discussion.other.requirePreviouslyApproved | boolean | false | true / false | Auto-approves comments from authors who have had at least one comment approved before. |
| Hold a comment if it contains this many links or more | cmsSettingsJson.discussion.other.moderationLinksThreshold | integer | 2 | 0 â 50 | Number of links allowed before a comment is automatically held for moderation. |
| Comment moderation keys (one per line) | cmsSettingsJson.discussion.other.moderationKeywords | string (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.disallowedKeys | string (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 Label | JSON Path | Type | Default | Description |
|---|---|---|---|---|
| Show avatars | cmsSettingsJson.avatars.showAvatars | boolean | true | Globally enables or disables avatar display on comments. |
| Maximum rating | cmsSettingsJson.avatars.maxRating | enum | "G" | Filters avatars by rating: "G", "PG", "R", or "X". |
| Default avatar | cmsSettingsJson.avatars.defaultAvatar | enum | "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.
| Size | JSON Path | Default Width | Default Height | Crop | Description |
|---|---|---|---|---|---|
| Thumbnail | cmsSettingsJson.media.thumbnail | 160 | 160 | false | Small square thumbnail used in listings and grids. |
| Medium | cmsSettingsJson.media.medium | 640 | 0 (auto) | â | Medium-sized image for post content and galleries. |
| Large | cmsSettingsJson.media.large | 1280 | 0 (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 Label | JSON Path | Type | Default | Description |
|---|---|---|---|---|
| Privacy policy page | cmsSettingsJson.privacy.policyPageId | string (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:
| Column | Contents | API Field |
|---|---|---|
AppearanceJson | branding, theme, layout, typography | appearanceJson |
CmsSettingsJson | reading, discussion, avatars, media, permalink, privacy | cmsSettingsJson |
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 Path | Type | Description |
|---|---|---|
defaultTemplateKeys.productDetail | string | Default active .cw template key for product detail routes when the product has no item-level TemplateKey. |
defaultTemplateKeys.blogArchive | string | Default active .cw template key for single blog archive routes when the blog has no item-level TemplateKey. |
defaultTemplateKeys.blogPost | string | Default active .cw template key for blog post routes when the post has no item-level TemplateKey. |
{
"defaultTemplateKeys": {
"productDetail": "product.detail",
"blogArchive": "blog.magazine",
"blogPost": "post.editorial"
}
}
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.
Permalink Configuration #
The storefront manager can configure URL patterns for categories, tags, products, category archives, blogs, and customer accounts through the Storefront Appearance â Permalinks panel. This allows customizing public-facing route prefixes without modifying theme templates.
| Setting | Default | Description |
|---|---|---|
Product base | products | URL prefix for product detail pages (e.g., /products/my-product). Change to /shop to use /shop/my-product. |
Category base | category | URL prefix exposed for category taxonomy links and theme helpers. |
Tag base | tag | URL prefix exposed for tag taxonomy links and theme helpers. |
Category archive base | category | URL prefix for category archive pages (e.g., /category/clothing). Change to /shop to use /shop/clothing. |
Blog base | blogs | URL prefix for blog index and post pages (e.g., /blogs/news/my-post). Change to /news to use /news/my-post. |
Account base | account | URL prefix for customer account pages (e.g., /account/dashboard). Change to /my-account to use /my-account/dashboard. |
Example Configurations
Default URLs: /blogs/news/first-post Blog post /category/clothing Category archive /products/widget-123 Product detail Customised URLs (Blog base = "updates", Category archive base = "collections", Product base = "shop"): /updates/news/first-post Blog post /collections/clothing Category archive /shop/widget-123 Product detail
Theme Template Usage
Theme templates can use cw_base_paths() and entity url properties to generate links that respect the configured permalink bases. Template authors should not hardcode /products, /blogs, or /category in reusable themes.
@code
__paths = cw_base_paths();
@endcode
<a href="{{ __paths.blog }}">Blog</a>
<a href="{{ __paths.category }}/clothing">Clothing</a>
<a href="{{ __product.url }}">{{ __product.title }}</a>
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:
- The server returns an
HTTP 401response with a redirect URL. - The storefront frontend detects the 401 and automatically redirects the user to the login page.
- 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:
- Require authentication to view this surface â A switch toggle that sets
isAuthProtected. When enabled, guest visitors are redirected to the login page. - Page link (optional) â A text input where you can enter a URL path (e.g.,
/my-custom-page) that other templates can use to link to this template.
Runtime Data
When fetching a published template via the storefront render API, the response includes:
{
"templateKey": "my.custom.page",
"isAuthProtected": true,
"pageLink": "/my-custom-page"
}
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:
{-- 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:
{-- 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:
{-- 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:
{-- 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
Welcome, { __customer.first_name } { __customer.last_name }
<div class="account-nav">
<a href="{ cw_route('account.orders') }" class="account-nav-item">
<span class="count">{ cw_count(var: __recent_orders) }</span>
<span class="label">Orders</span>
</a>
<a href="{ cw_route('account.wishlist') }" class="account-nav-item">
<span class="count">{ __wishlist_count }</span>
<span class="label">Wishlist</span>
</a>
<a href="{ cw_route('account.profile') }" class="account-nav-item">
<span class="label">Profile</span>
</a>
<a href="{ cw_route('account.addresses') }" class="account-nav-item">
<span class="label">Addresses</span>
</a>
</div>
@if(cw_count(var: __recent_orders) > 0)
<h2>Recent Orders</h2>
<div class="orders-table">
@foreach(__recent_orders as __order)
<div class="order-row">
<span>#{ __order.id }</span>
<span>{ cw_format_date(__order.created_at) }</span>
<span>{ cw_format_money(__order.amount, __order.currency_code) }</span>
<span class="status { __order.status }">{ __order.status }</span>
<a href="{ cw_route('account.order-detail', ['id' => __order.id]) }">View</a>
</div>
@endforeach
</div>
@endif
@hook('account.dashboard.after')
</div>
@endsection
Account Section Template Keys
The following account sub-templates are available, each mapping to a specific route:
| Template Key | File | Description |
|---|---|---|
account.default | templates/account/dashboard.cw | Account dashboard with overview, recent orders, quick links |
account.orders | templates/account/orders.cw | Full order history with pagination and filtering |
account.order-detail | templates/account/order-detail.cw | Single order view with items, status, tracking, and documents |
account.wishlist | templates/account/wishlist.cw | Wishlist items with add-to-cart and remove actions |
account.returns | templates/account/returns.cw | Return requests history and create return form |
account.reviews | templates/account/reviews.cw | Product reviews written by the customer |
account.profile | templates/account/profile.cw | Profile information edit form |
account.addresses | templates/account/addresses.cw | Saved addresses management (add, edit, delete) |
account.invoices | templates/account/invoices.cw | Invoice history and download links |
account.documents | templates/account/documents.cw | Post-purchase document downloads (receipts, contracts, etc.) |
Auth-Related Data Functions
These functions are available in account and auth templates:
| Function | Returns | Description |
|---|---|---|
cw_get_customer_profile() | object or null | Returns the logged-in customer profile object (contains customer and summary), or null if not authenticated |
cw_user_logged_in() | boolean | Returns true if a customer session is active |
cw_get_customer_orders(page, limit) | object | Returns paginated orders for the current customer. Accepts named params: page, limit, sort_by, sort_dir |
cw_get_customer_order(order_id) | object or null | Returns a single order by its ID, or null if not found |
cw_get_customer_invoices(page, limit) | object | Returns paginated invoices (orders with invoice) for the current customer |
cw_get_customer_receipts(page, limit) | object | Returns paginated receipts (paid orders) for the current customer |
cw_get_customer_documents(order_id) | array | Returns documents for a specific order, or all documents if no order_id given |
cw_get_customer_wishlist() | array | Returns wishlist items for the current customer |
cw_add_to_customer_wishlist(product_id) | object | Adds a product to the customer's wishlist |
cw_remove_from_customer_wishlist(product_id) | void | Removes a product from the customer's wishlist |
cw_get_customer_returns(page, limit) | object | Returns paginated return requests for the current customer |
cw_create_customer_return(order_id, reason, product_id) | object | Creates a return request for an order. product_id is optional |
cw_get_customer_reviews(page, limit) | object | Returns paginated product reviews written by the customer |
cw_get_customer_dashboard() | object | Returns 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?) | object | Updates the customer's profile. All params are optional. Supports location fields (country_id, region_id, city_id) |
cw_login_user(email, password, redirect_url?) | object | Authenticates with email and password. Returns session token, customer profile, and summary |
cw_verify_login(challenge_token, code) | object | Verifies the OTP code and establishes a customer session |
cw_logout_user() | void | Clears the current customer session |
cw_register_user(first_name, last_name, email, password, phone?, country_id?, region_id?, city_id?, address?, zip_code?) | object | Registers a new customer account with email and password. Supports location fields and optional address/phone |
cw_count(var) | int | Returns the count of items in a collection variable |
cw_format_money(amount, currency_code) | string | Formats a monetary value with currency symbol (e.g. $19.99) |
cw_route(name, params) | string | Generates a route URL by name (e.g. login, account.orders, account.order-detail) |
@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
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
.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
- Place shared partials (header, footer) in
templates/and include them with@include('name') - Use layout inheritance (
@extends,@section,@yield) for consistent page structure - Keep reusable sections in
sections/â they can be included from any template - Name template files consistently with their template key (e.g.,
product.detail.cwforproduct.detail)
Performance
- Fetch data with
@codeblocks at the top of templates, not inside loops - Use
@each('partial', __items, 'item')instead of manual@foreachwith@includefor cleaner code - Set
loading="lazy"on images below the fold - Minimize asset file sizes â use minified CSS/JS in production
- Use
@css()and@js()directives conditionally (e.g., only on pages that need them)
SEO
- Always set
<title>via@section('head')in layout inheritance - Include meta descriptions with
@section('head') - Use semantic HTML elements (
<header>,<nav>,<main>,<article>,<footer>) - Set descriptive
alttext on all images
Plugin Compatibility
- Add
@hook()directives at strategic points to allow plugin extensibility - Use
@filter()for values that plugins might need to modify - Add widget areas with
@widget('area-name')where dynamic content should appear
v2 Package Compatibility
- Set
formatVersion: 3inmanifest.json - Use
.cwfiles for all route templates, sections, and partials - Use JSON only for package metadata, design settings, widget schemas, and other configuration files
- Publish every route that should render publicly; missing templates show a v2 template error instead of rendering another format
v2 Validation Guide V2 #
Use this checklist before publishing a theme package.
Required Checks
| Area | Requirement |
|---|---|
| Manifest | formatVersion is 3; route assignments point to existing .cw template keys. |
| Templates | Home, product detail, blog archive, blog post, cart, checkout, auth, and account routes have matching .cw files when enabled. |
| Sections | Reusable sections live in sections/*.cw and are included with @include, @each, or @section/@yield. |
| Data | Templates use @query and cw_get_*() functions for product, blog, category, cart, checkout, and account data. |
| SEO | Document pages rendered through /v1/public/storefront/ssr include title, description, canonical URL, and semantic HTML. |
| Visual Editor | Theme edits target the installed runtime templates, sections, partials, settings, and route assignments. |