CoreWave360 Storefront Theme Guide
Build modern, server-rendered storefront themes using the CoreWave360Template Engine â a CoreWave360 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 CoreWave360 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 â CoreWave360
@directives(@query,@foreach,@if,@include,@includeonce) 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 â CoreWave360
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
âââââââââââââââââââââââââââââââââââââââââââââââââââ
â 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 CoreWave360 storefront theme, ensure you have:
- A CoreWave360 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 HTML and simple directive-based templates
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 CoreWave360 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, widgets
âââ 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
âââ widgets/ // Custom widget .cw templates
âââ 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, widgets, 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 |
Responsive Design #
CoreWave themes should be built mobile-first using CSS media queries. Theme assets like responsive.css are loaded after the main stylesheet.
Asset Loading Order
CSS assets are injected as inline <style> blocks in this priority:
theme.css(base styles)responsive.css(breakpoint overrides â loaded AFTER base)custom.css(theme-specific overrides)@cw_custom_css(storefront admin custom CSS)
Responsive Breakpoints
Theme authors should target these common breakpoints:
| Breakpoint | Targets | Example |
|---|---|---|
max-width: 480px | Small phones | Single-column layouts, stacked elements |
max-width: 768px | Tablets and large phones | Two-column â single-column, collapsed navigation |
max-width: 1024px | Small desktops/landscape tablets | Reduced sidebar widths, adjusted grid gaps |
min-width: 1025px | Desktop | Full multi-column layouts |
Widget Responsive Settings
Widgets support responsive visibility controls via the responsiveConfig object:
| Setting | Description |
|---|---|
hideDesktop | Hide widget on screens wider than 1024px |
hideTablet | Hide widget on screens between 481px and 1024px |
hideMobile | Hide widget on screens narrower than 480px |
columnsDesktop / columnsMobile | Grid column count for product/collection grids (desktop vs mobile) |
Manifest.json #
The manifest.json is the entry point for your theme. It declares metadata, template keys, starter content, header/footer presets, widgets, 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": {
"home.default": { "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" },
"maintenance.default": {
"label": "Coming Soon",
"icon": "clock",
"format": "cw",
"showHeader": false,
"showFooter": false
}
},
"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",
"previewImageUrl": "https://files.corewave360.com/platform/storefront-marketplace/media/05-2026/footer-dark-preview.png"
},
{
"key": "footer-minimal",
"label": "Minimal Footer",
"partial": "partials/footer-minimal.cw",
"previewImageUrl": "https://files.corewave360.com/platform/storefront-marketplace/media/05-2026/footer-minimal-preview.png"
}
],
"defaultHeaderPresetKey": "header-light",
"defaultFooterPresetKey": "footer-dark",
"starterContent": {
"home.default": { "defaultHome": true },
"product.detail": { "defaultProductDetails": true },
"blog.default": { "defaultBlogPage": true },
"post.default": { "defaultBlogPost": true },
"maintenance.default": {
"defaultMaintenance": true,
"showHeader": false,
"showFooter": false
},
"pages": [
{
"title": "About Us",
"handle": "about-us",
"templateKey": "page.default",
"pageHeaderKey": "header-dark",
"pageFooterKey": "footer-minimal",
"previewImageUrl": "https://files.corewave360.com/platform/storefront-marketplace/media/05-2026/about-page-preview.png",
"sortOrder": 2,
"publish": true
},
{
"title": "Contact",
"handle": "contact",
"templateKey": "page.default",
"previewImageUrl": "https://files.corewave360.com/platform/storefront-marketplace/media/05-2026/contact-page-preview.png",
"sortOrder": 3,
"publish": true
},
{
"title": "Coming Soon",
"handle": "coming-soon",
"templateKey": "maintenance.default",
"showHeader": false,
"showFooter": false,
"pageHeaderKey": "__none",
"pageFooterKey": "__none",
"previewImageUrl": "https://files.corewave360.com/platform/storefront-marketplace/media/05-2026/coming-soon-preview.png",
"sortOrder": 4,
"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 }
]
}
}
}
Manifest Properties
"2.0.0")3. CoreWave360 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.headerPresetsfooterPresetsfalse for route templates that should normally render without a header, such as coming-soon, maintenance, landing, or embedded pages. This declares the theme author's intent and is also copied into starter content defaults when applicable.false for route templates that should normally render without a footer. When the page imports with no footer, @cw_footer() resolves to an empty string for that page.templateKeys (manifest.json)
Each template key in templates maps to a route kind and must point to a .cw directive template.
| Key | Route Kind | Description |
|---|---|---|
home.default | 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._-]+. Mark the intended home template with defaultHome: true; otherwise the runtime scans for a key containing home as a dot-segment.
Template System V2 #
CoreWave360 v2 templates use .cw files with CoreWave's 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: home.default
--}}
@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/
â âââ home.default.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 #
Write @@ when a template needs a literal at-sign.
For example, support@@example.com renders as
support@example.com and is not parsed as an
@example directive.
Values returned by expressions are not parsed again, so
{{ cw_customer().email }} and the shorthand
@cw_customer().email can safely render customer
email addresses without escaping the returned value.
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 |
@switch(__route.kind)
@case('product')
<h1>Product</h1>
@break
@case('blog')
<h1>Blog</h1>
@break
@default
<h1>Storefront</h1>
@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') |
@set(__key, __value) | Alias for @var; set a template variable outside a @code block | @set(__layout, 'full-width') |
@session('key') | Read a safe storefront session value | @session('customer_email') |
@csrf | Render a hidden storefront CSRF input | @csrf |
@debug(__value) | Render debug output for local/theme development | @debug(__route) |
@php ... | Unsupported migration placeholder | Use @code ... @endcode instead. Raw PHP is never executed by the C# template engine. |
@json(__expr) | Serializes a template expression as a JSON string for use inside <script> blocks (strings, numbers, booleans, objects, arrays, null) | <script>var email = @json(__email ?? '');</script> |
Template Parts & Inheritance
| Directive | Purpose | Example |
|---|---|---|
@include('partial') | Include a template part every time the directive is encountered | @include('partials/header.cw') |
@includeonce('partial') | Include a template part only once per page render, even if the directive is reached again | @includeonce('partials/product-pagination.cw') |
@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') |
@image($entity, 'size') | Render an <img> tag for an entity object. The entity must have image-related fields. Size options: 'thumbnail', 'medium', 'large', 'full'. | @image(__product, 'medium') |
Conditional Tags
| Directive | Purpose | Example |
|---|---|---|
@is_home() ... @endis | Render only on the storefront home/catalogue root. | @is_home() <h1>Welcome</h1> @endis |
@is_page('slug') ... @endis | Render only on a CMS page. Passing a slug restricts the block to that page handle. | @is_page('about-us') ... @endis |
@is_product() ... @endis | Render only on a single product detail route. | @is_product() @include('partials/product-breadcrumb') @endis |
@is_category('slug') ... @endis | Render only on a category archive. Passing a slug restricts the block to that category. | @is_category('clothing') ... @endis |
@is_blog() ... @endis | Render on the blog index or a blog archive route. | @is_blog() <aside>Blog filters</aside> @endis |
@is_single() ... @endis | Render only on a single blog post route. | @is_single() @include('partials/post-share') @endis |
@is_search() ... @endis | Render only on a search results route. | @is_search() <p>Search results</p> @endis |
@is_account() ... @endis | Render only inside the customer account area. | @is_account() @include('account/sidebar') @endis |
@is_cart() ... @endis | Render only on the cart route. | @is_cart() @include('cart/summary') @endis |
@is_checkout() ... @endis | Render only on the checkout route. | @is_checkout() @include('checkout/steps') @endis |
@has_products() ... @endis | Render only when the current products collection is not empty. Defaults to __products; pass a variable to check another collection. | @has_products(__featured) ... @endis |
@has_image(__entity) ... @endis | Render only when an entity exposes an image, thumbnail, or URL image field. | @has_image(__product) <img src="{ __product.image }"> @endis |
@user_logged_in() ... @endis | Render only when a customer session is active. | @user_logged_in() <a href="/account">Account</a> @endis |
@is_logged_in ... @endis_logged_in | Alias for @user_logged_in(); useful for simpler auth-gated blocks. | @is_logged_in Welcome back @endis_logged_in |
@is_product()
@code
__product = cw_get_product();
@endcode
<a href="{ cw_product_url(__product) }">View product</a>
@endis
@is_account()
<a href="{ cw_route('account.orders') }">Orders</a>
@endis
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')
@includeonce('partials/product-pagination.cw')
@query(__products, ['type' => 'products', 'limit' => 8, 'orderby' => 'price'])
@hook('product.card.after', __product)
@filter(name: 'product.price_html', input: __html)
@var(__title, 'Featured Products')
@set(__layout, 'shop')
@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 as an alternate array argument syntax. |
__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. |
Inline Expression Syntax
Three inline expression forms are supported:
| Syntax | Description | Example |
|---|---|---|
{{ expr }} | HTML-escaped output. Equivalent to @echo(expr). | {{ __product.title }} |
{ expr } | Raw (unescaped) output. | { __product.description } |
{!! expr !!} | Raw output with literal rendering (no transformation). | {!! __page.content !!} |
{{-- comment --}} | Template comment. Stripped from output entirely. | {{-- This is a comment --}} |
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, @includeonce, 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. Prefer explicit paths such as @include('partials/product-card.cw') when a widget and a partial have similar names. Use @includeonce for expensive fragments or singleton fragments that must render only once per page render, such as pagination, shared filter controls, or setup markup. 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>
For migration-friendly templates, the expression evaluator also supports a small PHP-style helper set: function_exists('cw_name'), strip_tags(value), trim(value), strtolower(value), strtoupper(value), ucfirst(value), number_format(value, decimals?), htmlspecialchars(value), and htmlentities(value). These run inside CoreWave's C# renderer; there is no PHP runtime.
@code
__customer = function_exists('cw_customer') ? cw_customer() : null;
__excerpt = cw_str_limit(text: strip_tags(__post.content), limit: 120);
@endcode
<p>{ __excerpt }</p>
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 #
Storefront templates get data in two ways: @query for lists and cw_*() helpers for single records, URLs, cart actions, checkout, account data, formatting, and store settings. These are rendered on the server. A helper listed here is available in live storefront rendering and in the VS Code DevKit preview.
@query when you are drawing a list. Use cw_*() when you need one value, one current object, a URL, a form token, or an action result.
Common Listing Queries
@query fills the variable you name first. The block does not automatically loop; after the query, use @foreach to render each item.
@query(__products, ['type' => 'products', 'limit' => 8, 'orderby' => 'created_at', 'order' => 'desc'])
@endquery
@if(cw_count(var: __products) > 0)
@foreach(__products as __product)
<a href="{ cw_product_url(__product) }">
<span>{ __product.title ?? __product.name }</span>
<strong>{ cw_money(amount: __product.price) }</strong>
</a>
@endforeach
@endif
| Need | Use | Example |
|---|---|---|
| Products | @query with type: products | @query(__products, ['type' => 'products', 'limit' => 12]) |
| Featured products | @query or cw_get_featured_products() | @query(__products, ['type' => 'products', 'featured' => true]) |
| Products by category | category filter | @query(__products, ['type' => 'products', 'category' => 'shirts']) |
| Products by tag | tag filter or cw_get_products_by_tag() | cw_get_products_by_tag(tag: 'sale', limit: 8) |
| Categories | @query or cw_get_categories() | @query(__categories, ['type' => 'categories', 'limit' => 50]) |
| Collections | @query or cw_get_collections() | @query(__collections, ['type' => 'collections']) |
| Blogs | @query or cw_get_blogs() | @query(__blogs, ['type' => 'blogs']) |
| Blog posts | @query or cw_get_posts() | @query(__posts, ['type' => 'posts', 'limit' => 6]) |
| Tags | @query, cw_get_tags(), or cw_get_all_tags() | cw_get_tags(scope: 'products') |
| Locations | @query for countries only; load states/cities after selection | @query(__countries, ['type' => 'countries', 'limit' => 300]) |
Single Records and Current Page Data
Detail pages usually already have the current object available. Product detail templates can use __product or cw_get_product(). Blog post templates can use __post or cw_get_post(). Use current-route helpers when a template needs to know which slug or section is being rendered.
@code
__product = __product ?? cw_get_product();
__tags = cw_get_product_tags(productId: __product.id);
__related = cw_get_related_products(productId: __product.id, limit: 4);
@endcode
<h1>{ __product.title ?? __product.name }</h1>
<p>SKU: { __product.sku ?? '-' }</p>
<p>{ cw_money(amount: __product.price) }</p>
Cart, Wishlist, and Checkout
Cart action helpers such as cw_add_to_cart() return JavaScript call strings for use in buttons. For normal theme buttons, the preferred pattern is a button with js-cw-add-to-cart and data-cw-product-id; the storefront runtime handles the request and mini-cart refresh.
@code
__cart = cw_get_cart();
__cart_items = cw_cart_items();
__checkout = cw_get_checkout_breakdown();
@endcode
<button type="button" class="js-cw-add-to-cart" data-cw-product-id="{ __product.id }">
Add to cart
</button>
<form class="js-cw-storefront-checkout"
data-cw-checkout-version="storefront-commerce"
data-institution-id="{ cw_storefront_institution_id() }"
data-api-base-url="{ cw_public_api_base_url() }">
<input type="hidden" name="csrf_token" value="{ cw_csrf_token() }">
<input type="hidden" name="cart_token" value="{ cw_cart_token() }">
@widget('core.checkout-delivery-options', [
'title' => 'Delivery Options',
'loadingText' => 'Loading pickup and delivery options...',
'emptyText' => 'No pickup or delivery option is available for this store right now.'
])
@widget('core.checkout-payment-options', [
'title' => 'Payment Method',
'loadingText' => 'Loading payment methods...',
'emptyText' => 'This store has not enabled a payment method yet.'
])
<button type="submit">Place order</button>
</form>
Checkout delivery must use @widget('core.checkout-delivery-options'). The widget is theme-neutral: Anton, Bloxic, Beauten, and new themes all receive the same pickup, shipping, and digital-delivery flow. It writes the hidden checkout fields CoreWave expects, refreshes delivery choices when the customer address changes, and keeps pickup available even when a live shipping provider cannot return a quote.
Checkout payment must use @widget('core.checkout-payment-options'). The widget shows only payment methods that are active for the store and ready to collect payment. It writes both simple and nested hidden payment fields so older theme submit scripts and newer checkout scripts read the same selected provider.
Delivery Details Saved On Orders
CoreWave360 saves one clear delivery record on the order. Themes should show delivery choices, but they should not invent their own delivery fields. CoreWave checks the selected pickup location or shipping quote again before payment starts.
| Part | Fields | Meaning |
|---|---|---|
deliveryPreference | pickup, ship, or digital | How the order will be fulfilled. Digital-only carts do not need pickup or shipping. |
| Pickup | pickupLocationId, warehouseId, pickupName, pickupAddress, pickupPhone | The selected store, warehouse, or pickup point. Pickup is selected by default when available. |
| Shipping | providerCode, courierId, courierName, courierImageUrl, serviceCode, rateRequestToken, shippingRateId, shippingProfileId, shippingZoneId | The selected flat/free/weight/price rate or live courier rate. |
| Quote | amount, currencyCode, estimatedDeliveryText, estimatedDeliveryDate, quoteExpiresAt | The customer-facing delivery cost and ETA. Expired quotes are rejected and must be refreshed. |
| Destination | countryCode, countryName, regionName, cityName, address, postalCode, customerName, customerEmail, customerPhone | The delivery address used to quote and validate the order. |
| Hidden fields | checkout[delivery_json], checkout[delivery_preference], checkout[pickup_location_id], checkout[warehouse_id], checkout[shipping_method_code], checkout[service_code], checkout[courier_id], checkout[rate_request_token], checkout[shipping_rate_id], checkout[quote_expires_at], checkout[delivery_amount] | Generated by the shared widget. Do not duplicate these fields manually in a theme checkout form. |
Storefront Delivery And Payment Setup
Store owners configure delivery and payment from Storefront > Store Operations > Delivery & Payment. Themes should only show the choices returned by CoreWave and submit the selected option through the shared checkout widget.
| Choice | How it works | What shoppers see |
|---|---|---|
| Pickup only | The store accepts pickup from saved pickup locations, storefront warehouses, or the store address. | Pickup options first, with the address and contact details. |
| CoreWave delivery account | CoreWave uses the platform Shipbubble account for live courier prices when it is enabled. The store can apply it to local delivery, international delivery, or both. | Pickup first, then available courier choices with price and delivery time. |
| Store delivery account | The institution saves its own Shipbubble credentials. CoreWave stores them encrypted and uses them for live quotes when active. | Pickup first, then courier prices from the store's own provider account. |
| Store delivery prices only | The store uses saved flat, free-over-amount, price-based, or weight-based delivery prices. No live provider is required. | Pickup first, then matching saved delivery prices. |
Payment collection is also configured in that same page. The safest default is CoreWave collects and settles to me. Institutions can connect their own provider account or use split settlement only through the controlled payment connection setup. CoreWave keeps delivery fees, delivery tax, and processing fees separate from product sales so payouts and reports stay clear.
| Payment choice | Best use | What to configure |
|---|---|---|
| CoreWave-managed payment | The store wants CoreWave to collect checkout money and settle the product amount after platform, delivery, and processing charges are separated. | Select Paystack, Flutterwave, Stripe, or PayPal. Platform admins save and test CoreWave provider credentials from the admin dashboard. |
| Store-owned provider account | The store already owns a provider account and wants checkout payments to use that account. | Select the provider, enter the provider keys, save, and test the connection before going live. Keys are stored encrypted. |
| Manual payment | The store wants shoppers to place the order and pay outside the online checkout flow. | No provider key is needed. The order stays unpaid until the store records payment. |
Supported online providers are Paystack, Flutterwave, Stripe, and PayPal. Do not hard-code a provider in a theme. Store owners choose which payment methods are active, and shoppers choose from those active methods at checkout. If a store has not configured its own payment connection, CoreWave-managed payment appears only when the platform payment provider is enabled and has saved credentials.
Use Test payment on the Delivery & Payment page before going live. It checks the selected provider credentials without placing an order or charging a customer.
For online payments, add the CoreWave webhook URL inside the provider dashboard. Use
/v1/public/storefront/checkout/webhooks/paystack,
/v1/public/storefront/checkout/webhooks/flutterwave,
/v1/public/storefront/checkout/webhooks/stripe, or
/v1/public/storefront/checkout/webhooks/paypal. PayPal approvals are captured by CoreWave after PayPal sends the approval notification, so the order becomes paid only after capture succeeds.
Platform admins manage CoreWave provider credentials from System Operations > Delivery & Payments. The screen shows each provider webhook URL, lets admins enable or disable CoreWave processing, controls whether stores may offer that provider, and stores provider keys encrypted in the database.
Delivery QA Checklist For Themes
Test these cases in DevKit and on a real storefront before marking a checkout theme ready:
- No shipping provider configured, but a pickup warehouse exists: pickup appears first and checkout works.
- Shipbubble configured: courier choices show logo, price, ETA, and update the order total immediately.
- Customer changes country, state, city, or address: delivery options refresh and old quotes are not reused silently.
- Logged-in customer autofill runs: delivery options refresh after the saved address fills the form.
- No warehouse, pickup point, or shipping provider exists: show a clear setup message instead of a broken blank form.
- Digital-only cart: no delivery selection is required and the order is marked for digital fulfillment.
- Mixed physical products: do not show pickup if the selected warehouse cannot fulfill the cart quantity.
- Order owner view, tracking page, and emails show pickup address or courier details after checkout.
Customer Account and Auth
Use cw_customer() for the active customer. It returns flat fields such as first_name, last_name, email, phone, default_billing_address, and addresses. Use the block directive @is_logged_in when you want to show different markup for guests and logged-in customers.
@is_logged_in
@code
__customer = cw_customer();
__orders = cw_get_customer_orders(page: 1, limit: 5);
@endcode
<a href="{ cw_route('account.default') }">My account</a>
@else
<a href="{ cw_route('login') }">Log in</a>
@endis_logged_in
Country, State, and City Selects
Render countries on the server. States should load only after a country is selected. Cities should load only after a state is selected. Use real select elements and either conventional names or explicit targets.
@query(__countries, ['type' => 'countries', 'limit' => 300, 'orderby' => 'name', 'order' => 'asc'])
@endquery
<select name="country_id" id="billing-country" data-region-target="billing-region" data-city-target="billing-city" required>
<option value="">Select country</option>
@foreach(__countries as __country)
<option value="{ __country.id }">{ __country.name }</option>
@endforeach
</select>
<select name="region_id" id="billing-region" data-city-target="billing-city" required>
<option value="">Select state</option>
</select>
<select name="city_id" id="billing-city" required>
<option value="">Select city</option>
</select>
Routes, Assets, and Store Settings
Do not hardcode storefront URLs in reusable themes. Use URL helpers so custom permalink settings and storefront handles continue to work.
| Need | Use |
|---|---|
| Theme image, CSS, or JS file | @asset('assets/images/logo.png'), @css('assets/css/style.css'), @js('assets/js/main.js') |
| Named storefront route | cw_route('cart'), cw_route('checkout'), cw_route('account.default') |
| Product URL | cw_product_url(__product) |
| Category, brand, collection URLs | cw_category_url(__category), cw_brand_url(__brand), cw_collection_url(__collection) |
| Permalink bases | cw_base_paths() |
| Store details and logos | cw_get_store(), cw_get_store_name(), cw_get_appearance() |
| Merchant snippets | @cw_custom_css, @cw_header_html, @cw_footer_html |
External API Fetching
Use cw_fetch_json() for public JSON APIs and cw_fetch() when you need status, headers, text fallback, or error details. Do not call external APIs inside product loops.
@code
__rates = cw_fetch_json(url: 'https://api.example.com/public/rates', timeout: 5, maxBytes: 65536);
@endcode
@if(__rates.items)
@foreach(__rates.items as __rate)
<p>{ __rate.currency }: { __rate.value }</p>
@endforeach
@endif
http/https URLs are allowed. Localhost, private networks, and link-local addresses are blocked. Allowed methods are GET, POST, PUT, PATCH, and DELETE. Timeout is capped at 15 seconds and response size at 1 MB.
Supported Helper Index
This is the actual helper list supported by the current server renderer and recognized by the VS Code DevKit. If a helper is not listed here, do not use it in new themes.
| Helper | Use |
|---|---|
| Products and catalog | |
cw_brand_url() | Use for product pages, catalog grids, category/brand/collection links, stock, images, reviews, and related products. |
cw_category_url() | Use for product pages, catalog grids, category/brand/collection links, stock, images, reviews, and related products. |
cw_collection_url() | Use for product pages, catalog grids, category/brand/collection links, stock, images, reviews, and related products. |
cw_current_brand_slug() | Use for product pages, catalog grids, category/brand/collection links, stock, images, reviews, and related products. |
cw_current_category_slug() | Use for product pages, catalog grids, category/brand/collection links, stock, images, reviews, and related products. |
cw_current_collection_slug() | Use for product pages, catalog grids, category/brand/collection links, stock, images, reviews, and related products. |
cw_current_product_id() | Use for product pages, catalog grids, category/brand/collection links, stock, images, reviews, and related products. |
cw_current_product_slug() | Use for product pages, catalog grids, category/brand/collection links, stock, images, reviews, and related products. |
cw_get_category() | Use for product pages, catalog grids, category/brand/collection links, stock, images, reviews, and related products. |
cw_get_collections() | Use for product pages, catalog grids, category/brand/collection links, stock, images, reviews, and related products. |
cw_get_featured_products() | Use for product pages, catalog grids, category/brand/collection links, stock, images, reviews, and related products. |
cw_get_product() | Use for product pages, catalog grids, category/brand/collection links, stock, images, reviews, and related products. |
cw_get_product_details() | Use for product pages, catalog grids, category/brand/collection links, stock, images, reviews, and related products. |
cw_get_product_filter_options() | Use for product pages, catalog grids, category/brand/collection links, stock, images, reviews, and related products. |
cw_get_product_images() | Use for product pages, catalog grids, category/brand/collection links, stock, images, reviews, and related products. |
cw_get_product_images_zoom() | Use for product pages, catalog grids, category/brand/collection links, stock, images, reviews, and related products. |
cw_get_product_tags() | Use for product pages, catalog grids, category/brand/collection links, stock, images, reviews, and related products. |
cw_get_products() | Use for product pages, catalog grids, category/brand/collection links, stock, images, reviews, and related products. |
cw_get_products_by_tag() | Use for product pages, catalog grids, category/brand/collection links, stock, images, reviews, and related products. |
cw_get_related_products() | Use for product pages, catalog grids, category/brand/collection links, stock, images, reviews, and related products. |
cw_get_size_guide() | Use for product pages, catalog grids, category/brand/collection links, stock, images, reviews, and related products. |
cw_get_stock() | Use for product pages, catalog grids, category/brand/collection links, stock, images, reviews, and related products. |
cw_have_products() | Use for product pages, catalog grids, category/brand/collection links, stock, images, reviews, and related products. |
cw_product_details() | Use for product pages, catalog grids, category/brand/collection links, stock, images, reviews, and related products. |
cw_product_reviews() | Use for product pages, catalog grids, category/brand/collection links, stock, images, reviews, and related products. |
cw_product_url() | Use for product pages, catalog grids, category/brand/collection links, stock, images, reviews, and related products. |
cw_submit_product_review() | Use for product pages, catalog grids, category/brand/collection links, stock, images, reviews, and related products. |
| Blogs, posts, and tags | |
cw_current_blog_handle() | Use for blog archives, single posts, comments, post images, product tags, and tag pages. |
cw_current_blog_id() | Use for blog archives, single posts, comments, post images, product tags, and tag pages. |
cw_current_blog_slug() | Use for blog archives, single posts, comments, post images, product tags, and tag pages. |
cw_current_post_handle() | Use for blog archives, single posts, comments, post images, product tags, and tag pages. |
cw_current_post_id() | Use for blog archives, single posts, comments, post images, product tags, and tag pages. |
cw_current_post_slug() | Use for blog archives, single posts, comments, post images, product tags, and tag pages. |
cw_get_all_tags() | Use for blog archives, single posts, comments, post images, product tags, and tag pages. |
cw_get_blog() | Use for blog archives, single posts, comments, post images, product tags, and tag pages. |
cw_get_blogs() | Use for blog archives, single posts, comments, post images, product tags, and tag pages. |
cw_get_post() | Use for blog archives, single posts, comments, post images, product tags, and tag pages. |
cw_get_post_images() | Use for blog archives, single posts, comments, post images, product tags, and tag pages. |
cw_get_post_tags() | Use for blog archives, single posts, comments, post images, product tags, and tag pages. |
cw_get_posts() | Use for blog archives, single posts, comments, post images, product tags, and tag pages. |
cw_get_posts_by_tag() | Use for blog archives, single posts, comments, post images, product tags, and tag pages. |
cw_get_related_posts() | Use for blog archives, single posts, comments, post images, product tags, and tag pages. |
cw_get_tag() | Use for blog archives, single posts, comments, post images, product tags, and tag pages. |
cw_get_tags() | Use for blog archives, single posts, comments, post images, product tags, and tag pages. |
cw_post_comments() | Use for blog archives, single posts, comments, post images, product tags, and tag pages. |
cw_submit_post_comments() | Use for blog archives, single posts, comments, post images, product tags, and tag pages. |
| Cart and discounts | |
cw_add_to_cart() | Use for cart state, cart totals, cart tokens, coupon validation/application, and cart JavaScript action strings. |
cw_apply_coupon() | Use for cart state, cart totals, cart tokens, coupon validation/application, and cart JavaScript action strings. |
cw_cart_count() | Use for cart state, cart totals, cart tokens, coupon validation/application, and cart JavaScript action strings. |
cw_cart_items() | Use for cart state, cart totals, cart tokens, coupon validation/application, and cart JavaScript action strings. |
cw_cart_shipping_total() | Use for cart state, cart totals, cart tokens, coupon validation/application, and cart JavaScript action strings. |
cw_cart_subtotal() | Use for cart state, cart totals, cart tokens, coupon validation/application, and cart JavaScript action strings. |
cw_cart_token() | Use for cart state, cart totals, cart tokens, coupon validation/application, and cart JavaScript action strings. |
cw_cart_total() | Use for cart state, cart totals, cart tokens, coupon validation/application, and cart JavaScript action strings. |
cw_clear_cart() | Use for cart state, cart totals, cart tokens, coupon validation/application, and cart JavaScript action strings. |
cw_get_cart() | Use for cart state, cart totals, cart tokens, coupon validation/application, and cart JavaScript action strings. |
cw_get_cart_applied_discounts() | Use for cart state, cart totals, cart tokens, coupon validation/application, and cart JavaScript action strings. |
cw_get_cart_count() | Use for cart state, cart totals, cart tokens, coupon validation/application, and cart JavaScript action strings. |
cw_get_cart_items() | Use for cart state, cart totals, cart tokens, coupon validation/application, and cart JavaScript action strings. |
cw_get_cart_shipping_total() | Use for cart state, cart totals, cart tokens, coupon validation/application, and cart JavaScript action strings. |
cw_get_cart_subtotal() | Use for cart state, cart totals, cart tokens, coupon validation/application, and cart JavaScript action strings. |
cw_get_cart_token() | Use for cart state, cart totals, cart tokens, coupon validation/application, and cart JavaScript action strings. |
cw_get_cart_total() | Use for cart state, cart totals, cart tokens, coupon validation/application, and cart JavaScript action strings. |
cw_get_discount() | Use for cart state, cart totals, cart tokens, coupon validation/application, and cart JavaScript action strings. |
cw_get_discounts() | Use for cart state, cart totals, cart tokens, coupon validation/application, and cart JavaScript action strings. |
cw_remove_cart_item() | Use for cart state, cart totals, cart tokens, coupon validation/application, and cart JavaScript action strings. |
cw_update_cart_item() | Use for cart state, cart totals, cart tokens, coupon validation/application, and cart JavaScript action strings. |
cw_validate_discount() | Use for cart state, cart totals, cart tokens, coupon validation/application, and cart JavaScript action strings. |
| Checkout and delivery | |
cw_get_checkout_breakdown() | Use for checkout totals, shipping options/prices, pickup warehouses, shipment rows, delivery estimates, and order tracking. |
cw_get_order_shipments() | Use for checkout totals, shipping options/prices, pickup warehouses, shipment rows, delivery estimates, and order tracking. |
cw_get_order_tracking() | Use for checkout totals, shipping options/prices, pickup warehouses, shipment rows, delivery estimates, and order tracking. |
cw_get_product_delivery_estimate() | Use for checkout totals, shipping options/prices, pickup warehouses, shipment rows, delivery estimates, and order tracking. |
cw_get_shipping_options() | Read-only helper for showing available delivery choices. Checkout forms should use @widget('core.checkout-delivery-options') so the full delivery choice is submitted. |
cw_get_shipping_price() | Read-only helper for displaying a delivery amount. CoreWave checks the accepted delivery amount again before payment. |
cw_get_shipping_zones() | Use for checkout totals, shipping options/prices, pickup warehouses, shipment rows, delivery estimates, and order tracking. |
cw_get_tracking_code() | Use for checkout totals, shipping options/prices, pickup warehouses, shipment rows, delivery estimates, and order tracking. |
cw_get_warehouses() | Use for checkout totals, shipping options/prices, pickup warehouses, shipment rows, delivery estimates, and order tracking. |
| Customer account | |
cw_add_to_customer_wishlist() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_add_to_wishlist() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_auth_redirect() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_cancel_return_request() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_create_customer_return() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_current_order_id() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_current_return_id() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_customer() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_get_current_order() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_get_current_return() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_get_customer_account_settings() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_get_customer_addresses() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_get_customer_dashboard() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_get_customer_documents() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_get_customer_invoices() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_get_customer_order() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_get_customer_orders() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_get_customer_product_review() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_get_customer_profile() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_get_customer_receipts() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_get_customer_return() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_get_customer_returns() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_get_customer_reviews() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_get_customer_summary() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_get_customer_wishlist() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_get_order_details() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_get_order_note() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_get_return_details() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_get_wishlist() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_login_user() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_logout_user() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_register_user() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_remove_from_customer_wishlist() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_remove_from_wishlist() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_submit_return_request() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_update_customer_address() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_update_customer_profile() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_verify_login() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
cw_verify_register() | Use for logged-in customer profile, addresses, orders, invoices, receipts, returns, reviews, wishlist, login, registration, and account updates. |
| Routes and navigation | |
cw_base_paths() | Use for current route context, permalink-aware URLs, breadcrumbs, menus, and navigation by key or location. |
cw_breadcrumbs() | Use for current route context, permalink-aware URLs, breadcrumbs, menus, and navigation by key or location. |
cw_current_account_item_id() | Use for current route context, permalink-aware URLs, breadcrumbs, menus, and navigation by key or location. |
cw_current_account_item_lookup() | Use for current route context, permalink-aware URLs, breadcrumbs, menus, and navigation by key or location. |
cw_current_account_item_slug() | Use for current route context, permalink-aware URLs, breadcrumbs, menus, and navigation by key or location. |
cw_current_account_section() | Use for current route context, permalink-aware URLs, breadcrumbs, menus, and navigation by key or location. |
cw_current_route() | Use for current route context, permalink-aware URLs, breadcrumbs, menus, and navigation by key or location. |
cw_get_current_currency() | Use for current route context, permalink-aware URLs, breadcrumbs, menus, and navigation by key or location. |
cw_get_navigation() | Use for current route context, permalink-aware URLs, breadcrumbs, menus, and navigation by key or location. |
cw_get_navigation_by_key() | Use for current route context, permalink-aware URLs, breadcrumbs, menus, and navigation by key or location. |
cw_get_navigation_by_location() | Use for current route context, permalink-aware URLs, breadcrumbs, menus, and navigation by key or location. |
cw_get_navigations() | Use for current route context, permalink-aware URLs, breadcrumbs, menus, and navigation by key or location. |
cw_public_api_base_url() | Use for current route context, permalink-aware URLs, breadcrumbs, menus, and navigation by key or location. |
cw_route() | Use for current route context, permalink-aware URLs, breadcrumbs, menus, and navigation by key or location. |
cw_share_url() | Use for current route context, permalink-aware URLs, breadcrumbs, menus, and navigation by key or location. |
| Store and theme settings | |
cw_custom_css() | Use for store metadata, appearance settings, custom CSS/HTML snippets, header/footer presets, fonts, and tracking code. |
cw_footer() | Use for store metadata, appearance settings, custom CSS/HTML snippets, header/footer presets, fonts, and tracking code. |
cw_footer_html() | Use for store metadata, appearance settings, custom CSS/HTML snippets, header/footer presets, fonts, and tracking code. |
cw_get_appearance() | Use for store metadata, appearance settings, custom CSS/HTML snippets, header/footer presets, fonts, and tracking code. |
cw_get_custom_css() | Use for store metadata, appearance settings, custom CSS/HTML snippets, header/footer presets, fonts, and tracking code. |
cw_get_custom_fonts() | Use for store metadata, appearance settings, custom CSS/HTML snippets, header/footer presets, fonts, and tracking code. |
cw_get_footer_html() | Use for store metadata, appearance settings, custom CSS/HTML snippets, header/footer presets, fonts, and tracking code. |
cw_get_header_html() | Use for store metadata, appearance settings, custom CSS/HTML snippets, header/footer presets, fonts, and tracking code. |
cw_get_store() | Use for store metadata, appearance settings, custom CSS/HTML snippets, header/footer presets, fonts, and tracking code. |
cw_get_store_currency_code() | Use for store metadata, appearance settings, custom CSS/HTML snippets, header/footer presets, fonts, and tracking code. |
cw_get_store_info() | Use for store metadata, appearance settings, custom CSS/HTML snippets, header/footer presets, fonts, and tracking code. |
cw_get_store_name() | Use for store metadata, appearance settings, custom CSS/HTML snippets, header/footer presets, fonts, and tracking code. |
cw_get_theme_settings() | Use for store metadata, appearance settings, custom CSS/HTML snippets, header/footer presets, fonts, and tracking code. |
cw_header() | Use for store metadata, appearance settings, custom CSS/HTML snippets, header/footer presets, fonts, and tracking code. |
cw_header_html() | Use for store metadata, appearance settings, custom CSS/HTML snippets, header/footer presets, fonts, and tracking code. |
cw_storefront_host() | Use for store metadata, appearance settings, custom CSS/HTML snippets, header/footer presets, fonts, and tracking code. |
cw_storefront_institution_id() | Use for store metadata, appearance settings, custom CSS/HTML snippets, header/footer presets, fonts, and tracking code. |
| External data | |
cw_fetch() | Use for safe public API fetches and dynamic select/dropdown option sources. |
cw_fetch_json() | Use for safe public API fetches and dynamic select/dropdown option sources. |
cw_get_select_options() | Use for safe public API fetches and dynamic select/dropdown option sources. |
cw_http_json() | Use for safe public API fetches and dynamic select/dropdown option sources. |
cw_http_request() | Use for safe public API fetches and dynamic select/dropdown option sources. |
| Formatting and utilities | |
cw_collect() | Use for money/date/time formatting, counting, string checks, truncation, defaults, JSON decoding, CSRF tokens, and simple output. |
cw_contains() | Use for money/date/time formatting, counting, string checks, truncation, defaults, JSON decoding, CSRF tokens, and simple output. |
cw_count() | Use for money/date/time formatting, counting, string checks, truncation, defaults, JSON decoding, CSRF tokens, and simple output. |
cw_csrf_token() | Use for money/date/time formatting, counting, string checks, truncation, defaults, JSON decoding, CSRF tokens, and simple output. |
cw_default() | Use for money/date/time formatting, counting, string checks, truncation, defaults, JSON decoding, CSRF tokens, and simple output. |
cw_echo() | Use for money/date/time formatting, counting, string checks, truncation, defaults, JSON decoding, CSRF tokens, and simple output. |
cw_excerpt() | Use for money/date/time formatting, counting, string checks, truncation, defaults, JSON decoding, CSRF tokens, and simple output. |
cw_format_date() | Use for money/date/time formatting, counting, string checks, truncation, defaults, JSON decoding, CSRF tokens, and simple output. |
cw_format_diff_for_humans() | Use for money/date/time formatting, counting, string checks, truncation, defaults, JSON decoding, CSRF tokens, and simple output. |
cw_format_money() | Use for money/date/time formatting, counting, string checks, truncation, defaults, JSON decoding, CSRF tokens, and simple output. |
cw_format_price() | Use for money/date/time formatting, counting, string checks, truncation, defaults, JSON decoding, CSRF tokens, and simple output. |
cw_format_time() | Use for money/date/time formatting, counting, string checks, truncation, defaults, JSON decoding, CSRF tokens, and simple output. |
cw_json_decode() | Use for money/date/time formatting, counting, string checks, truncation, defaults, JSON decoding, CSRF tokens, and simple output. |
cw_money() | Use for money/date/time formatting, counting, string checks, truncation, defaults, JSON decoding, CSRF tokens, and simple output. |
cw_slugify() | Use for money/date/time formatting, counting, string checks, truncation, defaults, JSON decoding, CSRF tokens, and simple output. |
cw_truncate() | Use for money/date/time formatting, counting, string checks, truncation, defaults, JSON decoding, CSRF tokens, and simple output. |
| Other helpers | |
cw_convert_price() | Supported helper. Check its name and nearby examples to choose the right arguments. |
cw_get_all_categories() | Supported helper. Check its name and nearby examples to choose the right arguments. |
cw_get_available_currencies() | Supported helper. Check its name and nearby examples to choose the right arguments. |
cw_get_categories() | Supported helper. Check its name and nearby examples to choose the right arguments. |
cw_get_categories_by_ids() | Supported helper. Check its name and nearby examples to choose the right arguments. |
cw_get_newsletter_subscribers() | Supported helper. Check its name and nearby examples to choose the right arguments. |
cw_get_page_title() | Supported helper. Check its name and nearby examples to choose the right arguments. |
cw_get_recently_viewed() | Supported helper. Check its name and nearby examples to choose the right arguments. |
cw_gift_card_balance() | Supported helper. Check its name and nearby examples to choose the right arguments. |
cw_has_image() | Supported helper. Check its name and nearby examples to choose the right arguments. |
cw_redeem_gift_card() | Supported helper. Check its name and nearby examples to choose the right arguments. |
cw_resend_verification_email() | Supported helper. Check its name and nearby examples to choose the right arguments. |
cw_set_currency() | Supported helper. Check its name and nearby examples to choose the right arguments. |
cw_str_limit() | Supported helper. Check its name and nearby examples to choose the right arguments. |
cw_submit_contact() | Supported helper. Check its name and nearby examples to choose the right arguments. |
cw_subscribe_newsletter() | Supported helper. Check its name and nearby examples to choose the right arguments. |
cw_unsubscribe_newsletter() | Supported helper. Check its name and nearby examples to choose the right arguments. |
cw_user() | Supported helper. Check its name and nearby examples to choose the right arguments. |
cw_user_logged_in() | Supported helper. Check its name and nearby examples to choose the right arguments. |
Old Names Not Supported as Helpers
Some older snippets mention helper names that are not part of the current renderer. Use the current replacements below.
| Do not use | Use instead |
|---|---|
cw_get_onsale_products() | @query(__products, ['type' => 'products', 'on_sale' => true]) |
cw_get_products_by_category() | cw_get_products(category: 'slug', limit: 12) or @query with category |
cw_get_collection(), cw_get_collection_products() | cw_get_collections() and product queries filtered by collection where available |
cw_get_page(), cw_get_pages() | Use CMS page route variables and assigned page templates. These helpers are not exposed. |
cw_image_url() | Use image fields on the object, cw_get_product_images(), cw_get_post_images(), or @image(...). |
cw_is_home(), cw_is_product(), etc. | Use block directives such as @is_home ... @endis and @is_product ... @endis. |
cw_has_next_page(), cw_page_url(), cw_total_pages() | Use pagination data returned by the paginated helper/query result for that feature. |
cw_empty_cart() | cw_clear_cart() |
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 | home.default / 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.default |
/{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();
__account_section = cw_current_account_section();
__account_item = cw_current_account_item_lookup();
__order_id = cw_current_order_id();
__return_id = cw_current_return_id();
@endcode
@if(__route.kind == 'product')
<p>Rendering product {{ __product_slug }}</p>
@elseif(__route.kind == 'account')
<p>Rendering account {{ __account_section }} item {{ __account_item }}</p>
@endif
| Function | Returns | Description |
|---|---|---|
cw_current_route() | object | Returns kind, productSlug, categorySlug, blogSlug, postSlug, accountSection, accountItemLookup, 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. |
cw_current_account_section() | string | Current account section, such as dashboard, orders, order-detail, returns, invoices, or documents. |
cw_current_account_item_lookup() | string | The third account URL segment. For /{accountBase}/order-detail/CW-1001, this returns CW-1001. It may be an ID, reference, SKU-like value, or slugified name/reference. |
cw_current_account_item_id() | string | The numeric account item ID when the URL lookup is numeric. |
cw_current_account_item_slug() | string | Alias-style helper for the URL item lookup when themes prefer slug terminology. |
cw_current_order_id() | string | Current numeric order ID on an account order detail URL. For reference or slug URLs, use cw_current_account_item_lookup() or call cw_get_customer_order() with no argument. |
cw_current_return_id() | string | Current numeric return ID on an account return detail URL. For reference or slug URLs, use cw_current_account_item_lookup() or call cw_get_customer_return() with no argument. |
cw_storefront_host() | string | Returns the storefront's host/domain (from session data). Useful for constructing API URLs in JavaScript when the template needs to post multipart form data (e.g., file uploads). Returns empty string when not available. |
cw_storefront_institution_id() | string | Returns the numeric institution ID for the current storefront. Useful for API calls that require institution context (e.g., file uploads via X-Institution-Id header). |
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 explicit defaults declared in starterContent, 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. |
collection, collection_id | Matches collection ID, exact title, or slugified title. |
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. |
attribute_{slug}, attr_{slug} | Dynamic product attribute filters such as attribute_color=black or attribute_size=m. Values match slugified option labels. |
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. |
cw_category_url(), cw_brand_url(), and cw_collection_url() so the active storefront handle and configured product base are preserved. Keep range, sort, tag, and attribute state in the query string with min_price, max_price, orderby, order, tag, and attribute_{slug}. Do not link shop filters with raw category_id URLs.
<a href="{ cw_category_url(__category) }" class="shop-filter-link">{ __category.name }</a>
<a href="{ cw_brand_url(__brand) }" class="shop-filter-link">{ __brand.name }</a>
<a href="{ cw_collection_url(__collection) }" class="shop-filter-link">{ __collection.title }</a>
<select name="orderby">
<option value="created_at|desc">Newest</option>
<option value="price|asc">Price: Low to High</option>
<option value="price|desc">Price: High to Low</option>
<option value="title|asc">Name: A to Z</option>
</select>
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, blog_ids | Filters posts by blog handle/name or blog ID(s). Use blog_ids with a comma-separated list (e.g. '1,2,3') for multi-blog posts. |
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>
<button
type="button"
class="js-cw-add-to-cart"
data-cw-product-id="{ __product.id }"
data-cw-product-name="{ __product.title }"
data-cw-product-price="{ __product.price }"
data-cw-product-image="{ __product.image }"
data-cw-product-url="{ cw_product_url(__product) }"
>Add to cart</button>
<button
type="button"
class="js-cw-add-to-wishlist"
data-cw-product-id="{ __product.id }"
data-cw-product-name="{ __product.title }"
data-cw-product-price="{ __product.price }"
data-cw-product-image="{ __product.image }"
data-cw-product-url="{ cw_product_url(__product) }"
>Add to wishlist</button>
</div>
</div>
js-cw-add-to-cart / js-cw-add-to-wishlist and data-cw-product-* attributes. This avoids inline JavaScript quote escaping issues, lets the React storefront cart hydrate from rendered theme markup, and keeps the header cart count in sync. Theme scripts may either call window.cw_add_to_cart(productId, quantity) / window.cw_add_to_wishlist(productId) or dispatch cw:add-to-cart / cw:add-to-wishlist browser events with a detail.productId payload.
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>
{{-- Product attributes / options --}}
@if(cw_count(var: __product.attributes) > 0)
@foreach(__product.attributes as __attribute)
@if(cw_contains(value: __attribute.name, search: 'color') || cw_contains(value: __attribute.name, search: 'colour'))
<div class="product-option product-option--color">
<span>@code echo __attribute.name; @endcode:</span>
<ul>
@foreach(__attribute.options as __option)
<li title="@code echo __option.value; @endcode" style="background-color: @code echo __option.value; @endcode"></li>
@endforeach
</ul>
</div>
@else
<div class="product-option">
<span>@code echo __attribute.name; @endcode:</span>
<ul>
@foreach(__attribute.options as __option)
<li>@code echo __option.value; @endcode</li>
@endforeach
</ul>
</div>
@endif
@endforeach
@endif
<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')
Checkout Page Template Example
A complete checkout template should collect customer details, render the shared delivery widget, and let CoreWave submit the checkout details in the expected format.
@extends('layouts/default')
@section('content')
@code
__cart = cw_get_cart();
__breakdown = cw_get_checkout_breakdown(subtotal: __cart.subtotal);
__note = cw_get_order_note();
__customer = cw_customer();
@endcode
Checkout
@if(__cart && __cart.item_count > 0)
@query(type: 'products', ids: cw_collect(__cart, 'product_id'), limit: 50)
{ item.title }
Qty: { item.quantity } x { cw_money(amount: item.price) }
@if(item.variant_key)
Variant: { item.variant_key }
@endif
@endquery
Subtotal: { cw_money(amount: __cart.subtotal) }
Shipping: { cw_money(amount: __cart.shipping_total) }
Tax: { cw_money(amount: __breakdown.tax_total) }
Total: { cw_money(amount: __cart.grand_total) }
@if(__cart.coupon_code)
Coupon applied: { __cart.coupon_code }
@endif
@if(__note && __note.order_note)
Order Note: { __note.order_note }
@endif
@else
Your cart is empty.
@endif
@endsection
shipping_option_id or a hand-written shipping amount is incomplete. Use the delivery widget so pickup, saved rates, free shipping rules, live courier choices, and order tracking work the same way in every theme.
Variant Picker Styling Guide
Product variants are exposed via __product.attributes. Each attribute has a name and options array with value, unit_price, stock_quantity, and image_url.
Basic Variant Selector
@foreach(__product.attributes as __attribute)@endforeach
Color Swatches
Use cw_contains to detect color attributes and render swatches:
@foreach(__product.attributes as __attribute)
@code
__is_color = cw_contains(value: __attribute.name, search: 'color');
__is_size = cw_contains(value: __attribute.name, search: 'size');
@endcode
@if(__is_color == 'true')
@foreach(__attribute.options as __option)
@endforeach
@else
<-- render standard dropdown -->
@endif
@endforeach
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.theme.page.render | Runs while a storefront page is being rendered. |
corewave.theme.section.render | Runs while a theme section is being rendered. |
corewave.theme.block.render | Runs while a visual-editor block is being rendered. |
corewave.theme.template.resolve | Runs when the storefront resolves which template should handle a request. |
corewave.checkout.before_submit | Runs before checkout is submitted. |
corewave.checkout.after_submit | Runs after checkout submission succeeds. |
corewave.checkout.validate | Runs during checkout validation. |
corewave.checkout.payment.method.list | Runs while payment methods are being listed. |
corewave.checkout.shipping.quote | Runs while shipping quotes are being resolved. |
corewave.checkout.discount.apply | Runs when a discount is applied. |
corewave.checkout.discount.validate | Runs while a discount is validated. |
corewave.order.created | Runs after an order is created. |
corewave.order.paid | Runs after an order payment is confirmed. |
corewave.order.fulfillment.updated | Runs after fulfillment state changes. |
corewave.order.refund.created | Runs after a refund is created. |
corewave.order.cancelled | Runs after an order is cancelled. |
corewave.inventory.product.created | Runs after an inventory product is created. |
corewave.inventory.product.updated | Runs after an inventory product is updated. |
corewave.inventory.stock.low | Runs when stock reaches the low-stock threshold. |
corewave.inventory.stock.changed | Runs after product stock changes. |
corewave.catalogue.product.card.extend | Extends product-card rendering. |
corewave.catalogue.product.detail.extend | Extends product-detail rendering. |
corewave.navigation.menu.extend | Extends a navigation menu before render. |
corewave.navigation.menu.resolve | Runs while a navigation menu is being resolved. |
corewave.blog.post.render | Runs while a blog post is being rendered. |
corewave.blog.comment.before_create | Runs before a blog comment is created. |
corewave.blog.comment.after_create | Runs after a blog comment is created. |
corewave.customer.before_signup | Runs before customer registration completes. |
corewave.customer.after_signup | Runs after customer registration completes. |
corewave.customer.login.success | Runs after customer login succeeds. |
corewave.customer.profile.updated | Runs after customer profile changes. |
corewave.customer.account.menu.extend | Extends customer-account navigation. |
corewave.customer.address.before_save | Runs before a customer address is saved. |
corewave.customer.address.after_save | Runs after a customer address is saved. |
corewave.customer.add_to_cart | Runs after a product is added to cart. |
corewave.customer.wishlist_toggle | Runs after a wishlist item is toggled. |
corewave.notifications.dispatch | Runs when a storefront notification is dispatched. |
corewave.notifications.template.resolve | Runs while resolving a notification template. |
corewave.notifications.channel.resolve | Runs while resolving the notification channel. |
corewave.search.index.before | Runs before search indexing. |
corewave.search.index.after | Runs after search indexing. |
corewave.search.query.transform | Transforms storefront search queries. |
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": "<div class=\"wa-chat\" data-phone=\"{{phone}}\">Chat with us</div>"
}
}
]
}
}
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
âââ 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')
Standalone Plugins (Not Bundled)
To create a standalone plugin (uploaded separately from a theme):
- Create a ZIP with
manifest.json,hooks/(for pseudo-code), andassets/ - Register pseudo-code hooks in
manifest.jsonunderpseudoCodeHooks - Upload via Storefront â Plugins â Upload
- Plugins can be activated/deactivated independently of themes
Plugin Marketplace Submission
To submit a plugin to the CoreWave Marketplace:
- Package as a ZIP following the plugin structure
- Include screenshots, description, and version in
manifest.json - Submit via the admin panel under Storefront â Marketplace â Submit Plugin
- Plugins are reviewed for security compliance before approval
Default Visual Editor Widgets V2 #
CoreWave360 ships default visual-editor widgets and matching built-in .cw system widgets. These defaults are platform widgets, not theme JSON templates. Theme widgets are loaded separately from the active theme runtime and appear under a runtime group named {themeName} Widgets.
"", null, false, 0, or an empty array. Store owners can clear a field back to empty. Empty values are ignored when attributes or inline CSS are generated.
Editor Interface
The visual editor uses a top toolbar, desktop/tablet/mobile preview toggles, a left panel with Widgets and Navigator tabs, a center canvas, and a right inspector with Content, Style, and Advanced tabs.
| Panel | Purpose | Saved Data |
|---|---|---|
Widgets | Drag default platform widgets and runtime theme widgets into rows/columns. | Block entries inside the page builder rows. |
Navigator | Select rows, columns, and widgets from a structure tree. | No extra data; it controls editor selection only. |
Content | Edit widget-specific fields. Theme widgets render fields from manifest.json. | Built-ins save fields on the block. Theme widgets save values in settings. |
Style | Edit colors, spacing, sizing, borders, radius, shadow, and opacity. | visualConfig. |
Advanced | Edit CSS class, anchor ID, and responsive visibility. | visualConfig.cssClass, visualConfig.anchorId, responsiveConfig. |
Widget Groups
| Group | Default Widgets | Notes |
|---|---|---|
Layout | container, inner-section | Wrappers for sections and nested layouts. |
Atomic Elements | e-div-block, e-flexbox, e-tabs, e-tabs-menu, e-tab, e-tabs-content-area, e-tab-content, e-heading, e-paragraph, e-image, e-svg, e-button, e-youtube, e-divider, e-self-hosted-video, custom-element | Low-level elements for theme authors who want smaller building blocks. custom-element renders any HTML5 element with optional child widgets, styling, and inline CSS. |
Basic | heading, image, text-editor, video, button, divider, spacer, google_maps, icon | Common content widgets. |
General | sf-icon-box, sf-testimonial, sf-progress-bar, sf-pricing-table, sf-accordion, sf-tabs, sf-gallery, sf-carousel, sf-countdown, icon-list, social-icons, alert, html, sf-loop, sf-custom-html | Reusable content and interaction widgets. sf-custom-html is the unrestricted Custom HTML & CSS widget. |
Site | sf-page-header, sf-blogs-list, sf-blog-posts-list | Storefront page and blog widgets. |
Single | sf-blog-post | Single post/content route widget. |
Commerce | sf-product-grid, sf-cart-page, sf-checkout-page, checkout-delivery-options, sf-account-panel, sf-account-dashboard, sf-account-orders, sf-account-order-detail, sf-account-wishlist, sf-account-returns, sf-account-profile, sf-account-addresses, sf-account-invoices, sf-account-documents | Storefront product, checkout, delivery, and customer account widgets. |
CoreWave360 Storefront | sf-heading, sf-checkout, sf-category-tabs, sf-subcategory-tabs, sf-pagination, sf-search | Core storefront controls used by the built-in storefront builder. |
{themeName} Widgets | Widgets declared by the active theme runtime. | This group is populated from the installed theme's manifest.json. CoreWave360 does not hardcode theme widget names. |
Common Objects
Every visual-editor widget can use the following objects. Empty values are safe and are skipped during rendering.
| Object | Allowed Keys | Behavior |
|---|---|---|
visualConfig | marginTop, marginRight, marginBottom, marginLeft, paddingTop, paddingRight, paddingBottom, paddingLeft, backgroundColor, backgroundImage, backgroundSize, backgroundPosition, backgroundRepeat, color, textAlign, borderColor, borderStyle, borderWidth, borderRadius, boxShadow, minHeight, width, maxWidth, opacity, cssClass, anchorId, deviceOverrides | Style and advanced inspector data. Empty values are not emitted as inline CSS. |
responsiveConfig | hideDesktop, hideTablet, hideMobile | Controls responsive visibility in preview and render output. |
settings | Theme-widget field keys from manifest.json.widgets.{widgetKey}.fields. | Used only by runtime theme widgets. Saved values are exposed to .cw as widget.settings and __widget.settings. |
fieldSchema | Theme widget field schema object or array. | Editor-only metadata used to render the Content tab for theme widgets. |
items[] | label, title, text, body, src, alt, caption, href, icon, value | Reusable list object used by tabs, accordion, gallery, carousel, icon list, social icons, and loop preview data. |
Default Widget Reference
| Widget Key | Group | Allowed Content Keys / Objects | .cw Alias |
|---|---|---|---|
container | Layout | direction, gap, emptyText, children[], common objects. | @widget('core.container') |
inner-section | Layout | direction, gap, emptyText, children[], common objects. | @widget('core.container') |
flexbox | Layout | children[], class, html, content, common objects. | @widget('core.container') |
e-div-block | Atomic Elements | tag, direction, gap, emptyText, children[], common objects. | @widget('e-div-block') |
e-flexbox | Atomic Elements | direction, gap, alignItems, justifyContent, emptyText, children[], common objects. | @widget('e-flexbox') |
e-tabs | Atomic Elements | items[], activeIndex, common objects. Each item may include label and body. | @widget('core.tabs') |
e-tabs-menu | Atomic Elements | items[] with label, target, href, common objects. | Theme/render helper only. |
e-tab | Atomic Elements | label, target, common objects. | Theme/render helper only. |
e-tabs-content-area | Atomic Elements | items[] with label, body, common objects. | Theme/render helper only. |
e-tab-content | Atomic Elements | label, body, common objects. | Theme/render helper only. |
e-heading | Atomic Elements | title, subtitle, tag, common objects. | @widget('e-heading') |
e-paragraph | Atomic Elements | text, html, emptyText, common objects. | @widget('e-paragraph') |
e-image | Atomic Elements | src, alt, href, caption, imageFit, imageHeight, openInNewTab, common objects. | @widget('e-image') |
e-svg | Atomic Elements | svg, src, alt, common objects. | @widget('e-svg') |
e-button | Atomic Elements | label, href, variant, size, align, fullWidth, openInNewTab, common objects. | @widget('e-button') |
e-youtube | Atomic Elements | src, title, poster, autoplay, controls, common objects. | @widget('e-youtube') |
e-divider | Atomic Elements | thickness, color, width, align, common objects. | @widget('e-divider') |
e-self-hosted-video | Atomic Elements | src, title, poster, autoplay, controls, common objects. | @widget('e-self-hosted-video') |
custom-element | Atomic Elements | tag, children[], elementId, class, backgroundImage, width, maxWidth, minHeight, height, color, backgroundColor, textAlign, objectFit, display, direction, alignItems, justifyContent, gap, margin, padding, border, borderRadius, boxShadow, opacity, common objects. | @widget('core.custom-element') |
heading | Basic | title, subtitle, tag, common objects. | @widget('core.heading') |
image | Basic | src, alt, href, caption, imageFit, imageHeight, openInNewTab, common objects. | @widget('core.image') |
text-editor | Basic | text, html, emptyText, common objects. | @widget('core.text-editor') |
video | Basic | src, title, poster, autoplay, controls, common objects. | @widget('core.video') |
button | Basic | label, href, variant, size, align, fullWidth, openInNewTab, common objects. | @widget('core.button') |
divider | Basic | thickness, color, width, align, common objects. | @widget('core.divider') |
spacer | Basic | height, common objects. | @widget('core.spacer') |
google_maps | Basic | embedUrl, title, height, common objects. | @widget('core.map') |
icon | Basic | icon, label, href, common objects. | @widget('core.icon') |
sf-icon-box | General | icon, title, body, variant, common objects. | Visual editor block. |
sf-testimonial | General | quote, author, role, common objects. | Visual editor block. |
sf-progress-bar | General | label, value, max, tone, common objects. | Visual editor block. |
sf-pricing-table | General | title, price, billingPeriod, features[], ctaLabel, ctaHref, highlighted, common objects. | Visual editor block. |
sf-accordion | General | items[], allowMultiple, common objects. Each item supports title and body. | @widget('core.accordion') |
sf-tabs | General | items[], activeIndex, common objects. Each item supports label and body. | @widget('core.tabs') |
sf-gallery | General | items[], columns, imageHeight, common objects. Each item supports src, alt, caption, href. | Visual editor block. |
sf-carousel | General | items[], autoRotate, intervalMs, common objects. Each item supports src, title, caption, href. | Visual editor block. |
sf-countdown | General | label, targetAt, completedText, common objects. | Visual editor block. |
link-list | General | items[], common objects. Each item supports label, href, optional icon, and optional class. Use it for editable footer/help/account link lists. | @widget('core.link-list') |
icon-list | General | items[], common objects. Each item supports icon, label, text, href, and optional class. | @widget('core.icon-list') |
social-icons | General | items[], common objects. Each item supports icon, label, href. | @widget('core.social-icons') |
alert | General | title, message, tone, common objects. | @widget('core.alert') |
html | General | html, css, htmlClass, allowThemeScripts, common objects. | @widget('core.html') |
sf-loop | General | queryKey, titleField, metaField, imageField, priceField, ctaLabel, maxItems, emptyText, previewItems[], common objects. | Visual editor block. |
sf-page-header | Site | showBackPrimary, common objects. | Visual editor block. |
sf-blogs-list | Site | sourceMode, blogHandles[], enablePagination, itemsPerPage, emptyText, common objects. | Visual editor block. |
sf-blog-posts-list | Site | sourceMode, blogHandles[], postHandles[], enablePagination, itemsPerPage, emptyText, common objects. | Visual editor block. |
sf-blog-post | Single | showTags, emptyText, common objects. | Visual editor block. |
sf-product-grid | Commerce | sourceMode, categoryNames[], productIds[], brandNames[], inStockOnly, maxItems, sortBy, enablePagination, itemsPerPage, showCategoryArchiveLinks, columnsDesktop, columnsMobile, gap, cardRadius, imageFit, common objects. | Visual editor block. |
sf-cart-page | Commerce | title, emptyText, showCheckoutButton, checkoutLabel, common objects. | Visual editor block. |
sf-checkout-page | Commerce | title, submitLabel, common objects. | Visual editor block. |
checkout-delivery-options | Commerce | title, loadingText, emptyText, class, titleClass, common objects. Renders pickup, static shipping rates, live courier rates, hidden checkout delivery fields, delivery status hooks, and delivery list hooks. | @widget('core.checkout-delivery-options') or @widget('core.checkout-delivery') |
checkout-payment-options | Commerce | title, loadingText, emptyText, class, titleClass, common objects. Renders active checkout payment methods and writes the hidden payment fields expected by CoreWave checkout. | @widget('core.checkout-payment-options') or @widget('core.checkout-payment') |
sf-account-panel | Commerce | title, showTabs, emptyText, common objects. | Visual editor block. |
sf-account-dashboard | Commerce | title, common objects. | Visual editor block. |
sf-account-orders | Commerce | title, emptyText, common objects. | Visual editor block. |
sf-account-order-detail | Commerce | title, common objects. | Visual editor block. |
sf-account-wishlist | Commerce | title, emptyText, common objects. | Visual editor block. |
sf-account-returns | Commerce | title, emptyText, common objects. | Visual editor block. |
sf-account-profile | Commerce | title, common objects. | Visual editor block. |
sf-account-addresses | Commerce | title, common objects. | Visual editor block. |
sf-account-invoices | Commerce | title, emptyText, common objects. | Visual editor block. |
sf-account-documents | Commerce | title, emptyText, common objects. | Visual editor block. |
sf-heading | CoreWave360 Storefront | title, subtitle, common objects. | Visual editor block. |
sf-checkout | CoreWave360 Storefront | label, common objects. | Visual editor block. |
sf-category-tabs | CoreWave360 Storefront | linkToArchives, common objects. | Visual editor block. |
sf-subcategory-tabs | CoreWave360 Storefront | Common objects only. | Visual editor block. |
sf-pagination | CoreWave360 Storefront | showSummary, common objects. | Visual editor block. |
sf-search | CoreWave360 Storefront | placeholder, common objects. | Visual editor block. |
sf-custom-html | General | html, css, htmlClass, allowThemeScripts, common objects. | @widget('core.html') |
theme-widget:{widgetKey} | {themeName} Widgets | widgetKey, settings, fieldSchema, common objects. settings keys come from the theme manifest field schema. | @widget('{widgetKey}') |
Custom HTML & CSS Widget
Use sf-custom-html when the store owner or designer needs complete control over markup and styling from the visual editor. The widget exposes separate HTML and CSS textareas. Empty values stay empty: if html is blank, no wrapper markup is rendered; if css is blank, no <style> tag is emitted.
| Field | Type | Behavior |
|---|---|---|
html | html | Raw HTML fragment rendered in place. Relative asset URLs are resolved through the active theme asset resolver where available. |
css | css | Raw CSS emitted as a page-local <style data-corewave-custom-css> tag. Leave empty to emit nothing. |
htmlClass | text | Optional class added to the rendered widget wrapper. |
allowThemeScripts | boolean | Allows trusted theme HTML/forms through the visual editor renderer. Prefer theme asset scripts and platform widgets for JavaScript-heavy behavior. |
{
"id": "sf-custom-html",
"html": "<section class=\"promo-strip\"><h2>Free delivery today</h2><a href=\"/shop\">Shop now</a></section>",
"css": ".promo-strip { padding: 32px; background: #111; color: #fff; text-align: center; } .promo-strip a { color: #f8c15c; }",
"htmlClass": "my-custom-widget",
"allowThemeScripts": true
}
Theme developers can also render it from .cw as a default/fallback widget. Visual-editor values saved by the store owner still take priority when the widget is edited on a page.
@widget('core.html', [
'html' => '<div class="designer-note">Editable fallback HTML</div>',
'css' => '.designer-note { padding: 20px; border: 1px dashed currentColor; }',
'htmlClass' => 'theme-custom-html'
])
Using Built-In Widgets in .cw
Built-in widgets can be called with their saved widget key or an explicit core. alias where one exists. Values passed to @widget() override empty defaults for that render only.
@widget('core.heading', [
'tag' => 'h1',
'title' => cw_get_page_title(),
'className' => 'theme-page-title'
])
@widget('e-image', [
'src' => __product.imageUrl,
'alt' => __product.name,
'width' => '100%',
'objectFit' => 'cover'
])
@widget('core.button', [
'label' => 'View product',
'href' => cw_product_url(__product),
'className' => 'theme-btn theme-btn-primary'
])
Nested Widgets (Embedded Children)
Container-style platform widgets — container, flexbox, and custom-element — support nested child widgets. This allows store owners to embed any platform or theme widget inside a parent container directly from the visual editor, enabling complex hierarchical layouts without custom coding.
container, flexbox, custom-element. These render a children[] field in the visual editor's Content tab where the store owner can add, reorder, edit, and remove child widgets.
Visual Editor Interface
When editing a parent widget that supports nested children, the Content tab displays a Child Widgets section with:
- A list of added child widgets, each showing its label, key, and position number
- Move Up / Move Down buttons to reorder children
- An Edit button to select a child and edit its fields inline
- A Remove button to delete a child widget
- An Add Widget button that opens a mini palette showing all available system and theme widgets
When editing a child, a back-arrow returns to the children list. Child settings are saved inline within the parent widget's settings JSON and persist through the standard save flow.
Settings JSON Structure
Child widgets are stored as an array of objects inside the parent widget's children field. Each child object contains:
| Key | Type | Description |
|---|---|---|
key | string | The widget key (e.g. heading, button, core.image). System widget keys are stored without the core. prefix. |
instanceId | string | Unique instance identifier used for media picker targeting and internal references. |
settings | object | Key-value map of the child widget's field values. Keys correspond to the widget's field definitions. |
{
"children": [
{
"key": "heading",
"instanceId": "heading-abc123",
"settings": {
"text": "Welcome",
"level": "h2"
}
},
{
"key": "button",
"instanceId": "button-def456",
"settings": {
"label": "Shop Now",
"href": "/products"
}
}
]
}
Defining Children in .cw Templates
Theme developers can define embedded children directly in .cw template files using the children argument to the @widget() directive. When a container-style widget is rendered from a template with a children array, each child is rendered automatically, exactly the same way as children added through the visual editor.
This allows theme developers to ship pre-populated container layouts that store owners can later edit, reorder, or extend through the visual editor's Child Widgets interface. Children defined in template source are parsed by the visual editor and displayed as editable children when the parent widget is selected.
Basic Syntax
Pass a children array to any container-style widget call. Each entry is a [...] array with key (the widget key), instanceId (unique identifier), and settings (a key-value map of field values):
@widget('core.container', [
'direction' => 'column',
'gap' => '16px',
'children' => [
['key' => 'heading', 'instanceId' => 'child-heading-1', 'settings' => [
'title' => 'Welcome to Our Store',
'tag' => 'h2',
]],
['key' => 'text-editor', 'instanceId' => 'child-text-1', 'settings' => [
'html' => '<p>Browse our latest collection.</p>',
]],
['key' => 'button', 'instanceId' => 'child-btn-1', 'settings' => [
'label' => 'Shop Now',
'href' => '/products',
]],
],
])
Using Dynamic Values
Child widget settings can reference template variables and pseudo-code expressions, just like any other @widget() argument. The example below uses @code to compute dynamic values and passes them into child settings:
{{-- Container with dynamic children --}}
@code
__banner_heading = 'Summer Sale -- ' .. cw_get_store_name();
__banner_subtitle = 'Up to ' .. cw_get_discount('summer2024')?.discountPercentage .. '% off';
@endcode
@widget('core.container', [
'class' => 'promo-banner',
'children' => [
['key' => 'heading', 'instanceId' => 'promo-heading', 'settings' => [
'title' => __banner_heading,
'tag' => 'h1',
]],
['key' => 'text-editor', 'instanceId' => 'promo-text', 'settings' => [
'html' => '<p>' .. __banner_subtitle .. '</p>',
]],
['key' => 'button', 'instanceId' => 'promo-btn', 'settings' => [
'label' => 'Shop Sale',
'href' => cw_product_url(__product),
'className' => 'btn-accent',
]],
],
])
Nesting Containers
You can nest containers arbitrarily â each nested container can carry its own children array:
@widget('core.container', [
'direction' => 'row',
'gap' => '24px',
'children' => [
['key' => 'container', 'instanceId' => 'left-col', 'settings' => [
'direction' => 'column',
'gap' => '12px',
'children' => [
['key' => 'heading', 'instanceId' => 'left-heading', 'settings' => [
'title' => 'Left Column',
]],
['key' => 'text-editor', 'instanceId' => 'left-text', 'settings' => [
'html' => '<p>Content for the left side.</p>',
]],
],
]],
['key' => 'container', 'instanceId' => 'right-col', 'settings' => [
'direction' => 'column',
'gap' => '12px',
'children' => [
['key' => 'image', 'instanceId' => 'right-image', 'settings' => [
'src' => __product.imageUrl,
'alt' => __product.name,
'objectFit' => 'cover',
]],
],
]],
],
])
Visual Editor Integration
When a template source containing a container-style widget with embedded children is loaded in the visual editor, the children array is parsed from the inline @widget() arguments. These children appear in the Child Widgets section of the parent widget's settings panel, where the store owner can:
- View all children with their labels, keys, and positions
- Edit each child's fields inline
- Reorder children with move up/down
- Add new children from the widget palette
- Remove children entirely
Saved changes are persisted to the theme settings JSON, which takes priority over the template-source defaults. This means theme developers can define a sensible starting layout in the .cw file, and store owners can customize it without touching template code.
To keep large pages manageable, the visual editor starts widget-palette groups collapsed and starts every widget-map node with children collapsed. Expand only the group or parent widget you are editing. This is especially important for themes that build headers, hero rows, product grids, and footers from deeply nested core.custom-element widgets.
Universal Widget Style Overrides
Every widget, including custom theme widgets, has a collapsed Style Overrides group in the visual editor. Store owners can use it to add a wrapper element, CSS class, background image, spacing, sizing, border, shadow, opacity, and flex/grid alignment without editing the theme CSS file.
For default system widgets, CoreWave360 applies these fields directly to the rendered widget element when possible. For custom theme widgets, CoreWave360 wraps the widget output only when an override is set, using the selected overrideWrapperElement or a safe div fallback. Override fields are saved with an override* prefix so they never collide with a theme widget's own fields such as wrapperElement, wrapperClass, backgroundColor, or padding. Empty, 0, null, and unset values are ignored so the theme's CSS remains untouched unless the owner explicitly sets an override.
{{-- A designer can still provide sane defaults in the template. --}}
@widget('anton.hero-slideshow', [
'instanceId' => 'home-hero',
'items' => [
['image' => 'assets/images/Home_03/Banner1_Home3.png', 'title' => 'New arrivals']
]
])
{{--
Visual editor style overrides are saved separately, for example:
{
"widgetInstances": {
"home-hero": {
"key": "anton.hero-slideshow",
"settings": {
"overrideWrapperElement": "section",
"overrideWrapperClass": "merchant-home-hero",
"overrideBackgroundImage": "https://files.corewave360.com/...",
"overridePadding": "40px 0",
"overrideMargin": "0 auto"
}
}
}
}
--}}
@widget() inline arguments > theme manifest defaults. Children added or modified through the visual editor are saved in the settings JSON and merge with / override the template-source children on subsequent renders.
Nesting Children
Container widgets support nesting â a child widget that is itself a container can hold its own children. This allows complex layouts like a container inside a custom-element inside another container, each level fully editable through the visual editor's nested interface.
Custom Element Widget
The custom-element widget is a flexible, low-level platform widget that renders any valid HTML5 element. It combines the nested-children support of container with comprehensive styling options, making it ideal for theme developers and store owners who need precise control over markup structure without writing raw HTML.
Field Reference
| Field | Type | Description |
|---|---|---|
children | nested-widgets | Nested child widgets embedded inside this custom element. Supports the same add/reorder/edit interface as container and flexbox. |
tag | select | The HTML5 element to render. Choose from 60+ valid tags: div, span, section, article, header, footer, nav, main, aside, figure, details, summary, blockquote, pre, p, address, fieldset, legend, label, strong, em, b, i, u, small, mark, del, ins, sub, sup, code, kbd, samp, var, cite, abbr, time, data, dfn, ul, ol, li, dl, dt, dd, table, caption, colgroup, col, thead, tbody, tfoot, tr, td, th, h1–h6. Defaults to div when unset. |
elementId | text | Optional id attribute for the HTML element. Useful for anchor links and JavaScript targeting. |
class | text | Optional CSS class name(s) applied to the element. Supports multiple space-separated classes. |
backgroundImage | image | Background image URL, selected from the Media Library or entered manually. Rendered as background-image:url(...) in the element's inline style. |
width | text | CSS width (e.g. 100%, 500px, auto). |
maxWidth | text | CSS max-width (e.g. 1200px, 100%). |
minHeight | text | CSS min-height (e.g. 400px). |
height | text | CSS height (e.g. 300px, auto). |
color | text | CSS text color (e.g. #333, red). |
backgroundColor | text | CSS background color (e.g. #fff, transparent). |
textAlign | select | Text alignment: left, center, right, justify. |
objectFit | select | How content fits within the element: contain, cover, fill, none, scale-down. |
display | select | CSS display: block, inline, inline-block, flex, grid, none. |
direction | select | Flex direction (when display: flex): row, column, row-reverse, column-reverse. |
alignItems | select | CSS align-items: flex-start, center, flex-end, stretch, baseline. |
justifyContent | select | CSS justify-content: flex-start, center, flex-end, space-between, space-around, space-evenly. |
gap | text | Gap between flex/grid children (e.g. 16px, 1rem). |
margin | text | CSS margin shorthand (e.g. 10px 20px). |
padding | text | CSS padding shorthand (e.g. 15px). |
border | text | CSS border shorthand (e.g. 1px solid #ccc). |
borderRadius | text | CSS border-radius (e.g. 8px, 50%). |
boxShadow | text | CSS box-shadow (e.g. 0 2px 4px rgba(0,0,0,0.1)). |
opacity | text | CSS opacity (0 to 1, e.g. 0.8). |
Using Custom Element in .cw
The custom-element widget can be rendered from .cw templates using @widget('core.custom-element'). All field values can be passed as directive arguments:
{{-- Render a styled section element with nested content --}}
@widget('core.custom-element', [
'tag' => 'section',
'class' => 'hero-section',
'elementId' => 'home-hero',
'backgroundImage' => '@asset('assets/images/hero-bg.jpg')',
'width' => '100%',
'minHeight' => '450px',
'color' => '#fff',
'textAlign' => 'center',
'display' => 'flex',
'alignItems' => 'center',
'justifyContent' => 'center',
'padding' => '40px 20px'
])
When rendered, this produces markup like:
<section id="home-hero" class="hero-section"
style="width:100%;min-height:450px;color:#fff;background-image:url(/assets/images/hero-bg.jpg);text-align:center;display:flex;align-items:center;justify-content:center;padding:40px 20px">
{{-- Nested child widgets render here if any --}}
</section>
Widget Wrapper Configuration
Every @widget() call accepts two special parameters — wrapperElement and wrapperClass — that control the outer HTML element wrapping the widget output.
Allowed tags:
div, span, p, section, article, aside, header, footer, nav, main, figure, figcaption, details, summary, blockquote, pre, address, fieldset, legend, label, strong, em, b, i, u, small, mark, del, ins, sub, sup, code, kbd, samp, var, cite, abbr, time, data, dfn, ul, ol, li, dl, dt, dd, table, thead, tbody, tfoot, tr, td, th, caption, colgroup, col, h1, h2, h3, h4, h5, h6.
Usage notes: Void elements (e.g.,
![]()
,
,
, ) are not allowed because wrapping would produce invalid HTML. The tag name is lowercased and trimmed automatically.
className / cssClass / class. When used together with wrapperElement, the class is applied to the wrapper element. When wrapperElement is omitted, the class is applied directly to the widget's root element the same way className would.
{{-- Wrap a social icons widget in a <nav> with a CSS class --}}
@widget('core.social-icons', [
'wrapperElement' => 'nav',
'wrapperClass' => 'iconft'
])
{{-- Wrap a button in a <p> --}}
@widget('core.button', [
'label' => 'Learn More',
'href' => '/about',
'wrapperElement' => 'p',
'wrapperClass' => 'text-center'
])
{{-- wrapperClass without wrapperElement applies the class to the widget root --}}
@widget('core.heading', [
'tag' => 'h2',
'title' => 'Featured Products',
'wrapperClass' => 'section-title'
])
Theme Widget Field Schema
Runtime theme widgets are declared in the theme manifest. The visual editor reads each widget's fields schema, renders matching controls in the Content tab, and stores values in the widget block's settings object.
{
"widgets": {
"theme.navigation": {
"label": "Theme Navigation",
"template": "widgets/navigation.cw",
"fields": {
"menu": { "type": "select", "label": "Menu", "source": "navigations", "default": "" },
"logo": { "type": "image", "label": "Logo", "default": "" },
"logoWidth": { "type": "number", "label": "Logo width", "default": 0 }
}
}
}
}
{{-- widgets/navigation.cw --}}
@code
__menu_id = widget.settings.menu;
__items = cw_get_navigation(__menu_id);
@endcode
@if(widget.settings.logo)
<img src="{ widget.settings.logo }" alt="" />
@endif
Navigation Widgets and Mega Menu Images
A navigation widget should normally ask the editor for the menu to render, not for every menu item. Store owners manage menu items in Storefront > Navigation. Each menu item can have an optional Mega menu image, selected from the Media Library or pasted as a URL. That value is returned by cw_get_navigation as item.imageUrl and item.image.
{
"widgets": {
"anton-navigation-one": {
"label": "Navigation Menu One",
"description": "Select navigation menu to display",
"template": "widgets/anton-navigation-one.cw",
"fields": {
"menuId": {
"label": "Menu",
"type": "select",
"default": "",
"source": "cw_get_navigations",
"valueField": "id",
"labelField": "name"
}
}
}
}
}
{{-- widgets/anton-navigation-one.cw --}}
@code
__menu = cw_get_navigation(id: widget.settings.menuId);
@endcode
@if(__menu)
<ul class="main-menu">
@foreach(__menu.items as item)
<li class="level11">
<a href="{ item.url ?? '#' }">{ item.label }</a>
@if(item.children)
<div class="hover-menu-home">
@foreach(item.children as child)
<div class="item-menu-home">
<a href="{ child.url ?? '#' }">{ child.label }</a>
</div>
@endforeach
@if(item.imageUrl)
<div class="item-menu-home">
<img src="{ item.imageUrl }" alt="{ item.label }">
</div>
@endif
</div>
@endif
</li>
@endforeach
</ul>
@endif
Starter menu items in manifest.json may also include imageUrl, image, mediaUrl, or thumbnail. During Theme Demo Import, CoreWave360 copies the value into the created menu item only when the merchant has not already set an image.
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:
fields is the visual-editor schema. Store owners edit these fields in Storefront > v2 Storefront Editor > Widget settings. Saved values are stored per installed theme instance and are exposed inside the widget template as __widget.settings and widget.settings. Theme defaults are used first, values passed directly to @widget(...) are used as page/template-level fallback defaults, and saved visual-editor values override both.
"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" },
"menus": { "label": "Navigation Menus", "type": "multi-select", "default": [], "source": "cw_get_navigations" }
}
}
}
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 âââ templates/ âââ widgets/ // Custom widget .cw templates â âââ featured-products.cw â âââ newsletter-signup.cw â âââ product-carousel.cw âââ assets/ âââ bundled-plugins/
Using Widgets in Templates
Once registered, widgets are rendered in any .cw template using the @widget directive. You can pass fallback defaults directly. These defaults apply to that page/template render, but they do not lock the widget: when the store owner edits the widget in the visual editor, the saved visual-editor value takes priority.
{-- Render widget with default settings --}
@widget('featured-products')
{{-- Render widget with page/template fallback defaults --}}
@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.
Embedding Widgets Inside Widget Templates V2
Widget .cw template files can themselves contain @widget() directives, embedding other widgets inside a widget's own template. For example, a widget registered in manifest.json with template: "widgets/promo-banner.cw" can call @widget('core.heading'), @widget('core.button'), or @widget('another-theme-widget') from within its template. The embedded widget's template can itself contain further @widget() calls, and the system handles this automatically at render time.
Example: Widget Template with Embedded Widgets
"widgets": {
"promo-banner": {
"label": "Promo Banner",
"description": "A promotional banner with heading, text, and button",
"template": "widgets/promo-banner.cw",
"fields": {
"title": { "label": "Title", "type": "text", "default": "Special Offer" },
"body": { "label": "Body", "type": "textarea", "default": "" },
"bgColor": { "label": "Background Color", "type": "color", "default": "#1a1a2e" }
}
}
}
{{-- widgets/promo-banner.cw --}}
{{-- This widget template embeds platform widgets using @widget() --}}
@code
__title = __widget.settings.title ?? 'Special Offer';
__body = __widget.settings.body ?? '';
__bg = __widget.settings.bgColor ?? '#1a1a2e';
@endcode
Visual Editor Support
When a theme widget is selected in the visual editor's Widget Editor panel, the editor automatically detects any @widget() directives inside the widget's own .cw template and displays them in a Widget Template section. You can see how many embedded directives exist, which widget keys they reference, and edit the template source directly. Changes are saved with the theme and persist on the live storefront.
âââââââââââââââââââââââââââââââââââââââââââââââââââ â Widget Editor â â Editing: promo-banner â â â â Widget: [promo-banner âŧ] â â â â ââ Widget Template âââââââââââââââââââââââââââ â â â widgets/promo-banner.cw [3 @widget()] â â â This widget's .cw template contains â â â â @widget() directives that are rendered â â â â recursively at runtime. â â â â â â â â âĸ core.heading â â â â âĸ core.button â â â â âĸ trust-badges â â â â â â â â ââââââââââââââââââââââââââââââââââââââââââ â â â â â
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 | "" |
html / richtext | HTML-capable content field for trusted store-owner editable copy | "" |
code / css | Code editor field. Use css for custom CSS snippets and code for generic source-like text. | "" |
number | Numeric input | 0 |
range / slider | Numeric slider control for bounded values such as opacity, gap, width, or speed | 0 |
boolean / checkbox / toggle | Yes/No toggle | false |
select / dropdown | Dropdown selector. Use options (array of {value, label} objects) for static choices or source (string) for dynamic options from products, categories, collections, navigations, blogs, posts, tags, discounts, countries, regions, cities | First option or "" |
multi-select / multiselect / list | Multiple selection control. Uses the same options array or dynamic source as select. The stored value is an array, so templates can iterate it directly with @foreach(widget.settings.fieldName as value). | [] |
repeater / repeatable / array | Repeatable row editor with Add Item, Remove, Duplicate, Move Up, Move Down, and nested controls from the repeater's own fields object. The stored value is an array of objects, so templates can iterate it with @foreach(__widget.settings.items as __item). | [] |
color / colour | Color picker | #000000 |
image / media | Media library image picker. The editor shows a picker button and saves the selected asset URL/value. | "" |
link | URL/link picker | "" |
Repeater Field Example
"anton.social-icons": {
"label": "Anton Social Icons",
"template": "widgets/anton-social-icons.cw",
"fields": {
"items": {
"label": "Social Icons",
"type": "repeater",
"default": [],
"fields": {
"label": { "label": "Label", "type": "text", "default": "" },
"href": { "label": "Link URL", "type": "text", "default": "" },
"icon": { "label": "Font Awesome Class", "type": "text", "default": "" },
"class": { "label": "Additional CSS Class", "type": "text", "default": "" }
}
}
}
}
@code
__items = __widget.settings.items ?? [];
@endcode
@foreach(__items as __item)
@if(__item.href && __item.icon)
<a href="{ __item.href }" class="{ __item.class }" aria-label="{ __item.label }">
<i class="{ __item.icon }" aria-hidden="true"></i>
</a>
@endif
@endforeach
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"). The visual editor fetches these options when the store owner edits the widget. Templates can also fetch the same options at render time with 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 an array. Iterate it directly, or use membership checks against that array:
{-- Check if a specific value is selected --}
@if(in_array('red', widget.settings.colors))
<div class="color-swatch color-swatch--red">Red is active</div>
@endif
{-- Iterate through all selected values --}
<ul class="selected-filters">
@foreach(widget.settings.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 visual-editor options are populated dynamically from current store data. Newly added products, categories, blogs, posts, tags, locations, discounts, and navigations are fetched live when the editor opens; they are not frozen into the theme package. Use the cw_get_select_options() template function inside a @foreach block when the storefront page itself also needs to render those options:
{
"fields": {
"category": {
"label": "Category Filter",
"type": "select",
"default": "",
"source": "categories"
},
"products": {
"label": "Featured Products",
"type": "multi-select",
"default": [],
"source": "products"
},
"menuIds": {
"label": "Menus to Render",
"type": "multi-select",
"default": [],
"source": "cw_get_navigations",
"valueField": "id",
"labelField": "name"
}
}
}
{-- 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']);
__menu_opts = cw_get_select_options(['source' => 'navigations', 'valueField' => 'id', 'labelField' => 'name']);
@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 --}
<div class="checkbox-group">
@foreach(__category_opts as __opt)
<label>
<input type="checkbox"
value="{ __opt.value }"
{{ in_array(__opt.value, widget.settings.categories) ? '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 |
navigations, navigation, menus, cw_get_navigations | Navigation/menu options (id / name by default) | limit, valueField, labelField |
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 when choices depend on dynamic store data. The visual editor loads source options for the store owner; templates can also call cw_get_select_options(). For multi-select, the saved setting is an array and can be used directly in @foreach.
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 */ },
"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": {
"home.default": { /* inlined .cw content */ },
"page.default": "https://storage.googleapis.com/.../page.default.cw"
},
"templateFormats": {
"home.default": "cw",
"cart.default": "cw"
}
}
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, CoreWave360 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 demo-import defaults, starter CMS pages, starter blogs, and named menus. Template defaults are explicit flags on your own template keys; no special home key is required.
"anton.home.one": { "defaultHome": true } do not create CMS pages. They only assign default templates for routing. Actual storefront pages are created only from starterContent.pages. This means a theme can mark anton.product.details.one as the default product detail template without creating a fake product page record.
Appearance Defaults From Manifest
A theme can provide Storefront â Appearance defaults in manifest.json. These values are applied when the merchant runs Theme Demo Import. They update the merchant's Appearance settings, but they still do not force styling unless the theme reads them with cw_get_appearance() or cw_get_store().appearance.
{
"appearanceDefaults": {
"theme": {
"primary": "#ea5e03",
"background": "#f4f4f4",
"surface": "#ffffff",
"text": "#111111",
"muted": "#888888",
"border": "#e5e5e5"
},
"typography": {
"body": "Poppins",
"header": "Poppins",
"navigation": "Poppins",
"title": "Poppins",
"h1": "Poppins",
"h2": "Poppins",
"h3": "Poppins",
"h4": "Poppins",
"h5": "Poppins",
"h6": "Poppins",
"button": "Poppins",
"price": "Poppins"
},
"layout": {
"header": {
"sticky": true
}
}
},
"cmsSettingsDefaults": {
"permalink": {
"productSearchBase": "product-search",
"blogSearchBase": "blog-search",
"searchBase": "search"
}
}
}
| Manifest Key | Writes To | Notes |
|---|---|---|
appearanceDefaults.theme | appearanceJson.theme | Primary, background, surface, text, muted, and border color defaults. |
appearanceDefaults.typography | appearanceJson.typography | Google Font family defaults for body, header, navigation, titles, H1-H6, buttons, and prices. |
appearanceDefaults.layout.header.sticky | appearanceJson.layout.header.sticky | Default sticky-header preference. Logo position is ignored because theme partials own logo layout. |
cmsSettingsDefaults.permalink.productSearchBase | cmsSettingsJson.permalink.productSearchBase | Exposed to templates as cw_base_paths().product_search. |
cmsSettingsDefaults.permalink.blogSearchBase | cmsSettingsJson.permalink.blogSearchBase | Exposed to templates as cw_base_paths().blog_search. |
cmsSettingsDefaults.permalink.searchBase | cmsSettingsJson.permalink.searchBase | Exposed to templates as cw_base_paths().search. |
The importer also accepts aliases such as defaultAppearance, storefrontAppearanceDefaults, defaultCmsSettings, and basePaths, but new themes should use appearanceDefaults and cmsSettingsDefaults.
"starterContent": {
"anton.home.one": { "defaultHome": true },
"anton.product.details.one": { "defaultProductDetails": true },
"anton.blogs.one": { "defaultBlogPage": true },
"anton.blog": { "defaultBlogPost": true },
"anton.not-found": { "defaultNotFound": true },
"maintenance.anton": { "defaultMaintenance": true }
}
| Field | Type | Description |
|---|---|---|
defaultHome | Boolean | Marks this template key as the storefront home template. It does not create a CMS page. |
defaultProductDetails | Boolean | Marks this template key as the default single product template. Merchants can later change it in Storefront â Overview. |
defaultBlogPage | Boolean | Marks this template key as the default blog archive/listing template. |
defaultBlogPost | Boolean | Marks this template key as the default single blog post template. |
defaultNotFound | Boolean | Marks this template key as the default 404/not-found template. Aliases: defaultNotFoundPage, default404, default404Page. |
defaultMaintenance | Boolean | Marks this template key as the default maintenance/coming-soon template. Aliases: defaultMaintenancePage, defaultComingSoon. |
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 using the pages array inside starterContent. Each entry becomes a StorefrontPage record when the merchant runs Theme Demo Import. A page can optionally set pageHeaderKey and pageFooterKey to choose a preferred manifest header/footer preset for the imported page. Store owners can later change those values in Storefront â Pages. If these keys are omitted, @cw_header() and @cw_footer() use the manifest defaults. Set showHeader: false or showFooter: false when the imported page should intentionally render without header/footer.
Demo content is imported from Storefront â Appearance â Theme Demo Import. Theme installation only installs the package; it does not add starter media to the merchant's media library. Media assets declared by the theme are indexed for the storefront owner only when they import demo content. Re-running demo import is idempotent: existing pages, menu items, blogs, posts, and media assets are skipped or only filled where the merchant has not set a value.
"starterContent": {
"home.default": { "defaultHome": true },
"pages": [
{
"title": "About Us",
"handle": "about-us",
"templateKey": "page.default",
"pageHeaderKey": "header-dark",
"pageFooterKey": "footer-minimal",
"previewImageUrl": "https://files.corewave360.com/platform/storefront-marketplace/media/05-2026/about-page-preview.png",
"sortOrder": 2,
"publish": true
},
{
"title": "Contact",
"handle": "contact",
"templateKey": "page.default",
"previewImageUrl": "https://files.corewave360.com/platform/storefront-marketplace/media/05-2026/contact-page-preview.png",
"sortOrder": 3,
"publish": true
},
{
"title": "Coming Soon",
"handle": "coming-soon",
"templateKey": "maintenance.default",
"showHeader": false,
"showFooter": false,
"pageHeaderKey": "__none",
"pageFooterKey": "__none",
"previewImageUrl": "https://files.corewave360.com/platform/storefront-marketplace/media/05-2026/coming-soon-preview.png",
"sortOrder": 4,
"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") |
pageHeaderKey | String? | Optional header preset key from manifest.headerPresets[].key. This is the recommended v2 field. When omitted, @cw_header() uses defaultHeaderPresetKey. Legacy imported packages may still be normalized from headerPresetKey, headerPreset, or headerKey, but new themes should not use those aliases. |
pageFooterKey | String? | Optional footer preset key from manifest.footerPresets[].key. This is the recommended v2 field. When omitted, @cw_footer() uses defaultFooterPresetKey. Legacy imported packages may still be normalized from footerPresetKey, footerPreset, or footerKey, but new themes should not use those aliases. |
showHeader | Boolean? | Set to false to import the page with no header. CoreWave360 stores this as the no-header sentinel __none, and @cw_header() renders nothing for the page. Aliases accepted: includeHeader, renderHeader, hasHeader. |
showFooter | Boolean? | Set to false to import the page with no footer. CoreWave360 stores this as the no-footer sentinel __none, and @cw_footer() renders nothing for the page. Aliases accepted: includeFooter, renderFooter, hasFooter. |
__none | String sentinel | Optional explicit value for pageHeaderKey or pageFooterKey when you want no header/footer. Accepted no-chrome aliases are none, off, disabled, no-header, and no-footer. |
previewImageUrl | String? | Optional external http/https preview image shown in Theme Demo Import. Use a public bucket URL such as https://files.corewave360.com/platform/storefront-marketplace/media/05-2026/page-preview.png. Aliases accepted: previewImage, thumbnailUrl, thumbnail, imageUrl, image. |
contentHtml | String? | Optional raw HTML content body for the page (seeded as ContentJson) |
defaultHome | Boolean? | Optional shortcut to also assign this page's templateKey as the default home template. |
defaultBlogPage | Boolean? | Optional shortcut to also assign this page's templateKey as the default blog archive template. |
sortOrder | Int | Display order for navigation sorting (lower = first) |
publish | Boolean | Whether the page is published immediately (true) or created as draft (false) |
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": {
"home.default": { "defaultHome": true },
"product.detail": { "defaultProductDetails": 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 }
]
}
}
| 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"). Install-time tokens are supported: cw_base_paths().search resolves to the configured base path, and cw_route('account.default') resolves to the full configured route such as /account/dashboard. |
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 storefront price after sale discount, when applicable. |
__product.regular_price | decimal | Undiscounted admin unit price. |
__product.original_price | decimal | Alias of regular_price for themes that render original / was pricing. |
__product.sale_price | decimal? | Discounted sale price when on_sale and discount_percent apply. |
__product.compare_price | decimal? | Compare-at / original price to show with strikethrough. Falls back to regular_price when a discount creates a lower sale 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}. The primary thumbnail is included first when available. |
__product.categories | array | Array of {id, name, slug} objects |
__product.tags | array | Array of {id, name, slug} objects |
__product.attributes | array | Array of product attribute objects {name, options}. Options expose {value, unit_price, cost_price, stock_quantity}. |
__product.created_at | datetime | Creation timestamp |
__product.updated_at | datetime | Last update timestamp |
Product Badges
The product DTO includes computed badge data. Badges are available at __product.badges as an array of objects with type, label, and class.
| Badge Type | Condition | Example |
|---|---|---|
sale | Product has a discount or compare-at price higher than unit price | Sale |
new | IsNewArrival = true and NewArrivalEndDate has not passed (if set) | New |
out-of-stock | StockQuantity <= 0 | Out of Stock |
featured | Product is in a "featured" collection | Featured |
collection | Product is in any other collection | Collection Name |
Use cw_get_stock() for additional low-stock detection:
@code __stock = cw_get_stock(productId: __product.id); @endcode @if(__stock.is_low_stock) Only { __stock.total_stock } left! @endif @foreach(__product.badges as __badge) { __badge.label } @endforeach
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.image | string? | Alias for cover_image |
__post.author | string? | Post author name (may be null) |
__post.published_at | datetime | Publish timestamp |
__post.status | string | Post status (Published/Draft/etc) |
__post.blog_id | int | Primary blog ID (first assigned blog) |
__post.blog_ids | array<int> | Array of all assigned blog IDs (supports multi-blog posts) |
__post.blog_slug | string | Primary blog slug/handle for URL construction |
__post.url | string | Relative URL (blog_slug/post_slug). Prefix with __paths.blog for full URL. |
__post.blogs | array | Array of {id, name, slug} objects for all blogs the post belongs to |
__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(). See the canonical Store Object table above for sample values and full access examples.
| 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 from appearance.branding.siteTagline |
__store.logo | string? | Default logo URL from appearance.branding.logoDefault |
__store.logo_url | string? | Default logo URL alias |
__store.logo_default / __store.logoDefault | string? | Default logo URL from appearance.branding.logoDefault |
__store.logo_light / __store.logoLight | string? | Logo intended for light backgrounds from appearance.branding.logoLight |
__store.logo_dark / __store.logoDark | string? | Logo intended for dark backgrounds from appearance.branding.logoDark |
__store.favicon | string? | Favicon URL from appearance.branding.favicon |
__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.current_currency_code | string | Shopper's preferred currency code, falls back to base currency |
__store.current_currency_symbol | string | Shopper's preferred currency symbol |
__store.current_currency_name | string | Shopper's preferred currency name |
__store.current_currency_is_base | bool | Whether the shopper's preferred currency is the store's base 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.appearance | object | Full Appearance object. Same shape as cw_get_appearance(). |
__store.cmsSettings / __store.cms_settings | object | Full CMS settings object. |
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 |
Customer Object (cw_customer())
Returned by cw_customer(). All fields are at the top level â no nested .customer or .summary access needed.
Store in a variable: __customer = cw_customer();.
Access properties directly: __customer.email, __customer.first_name.
if (__customer && __customer.error) { __customer = null; }
or use null-coalescing: { __customer.first_name ?? '-' }.
| Property | Type | Description |
|---|---|---|
__customer.id | int | Customer ID |
__customer.name | string | Business / display name |
__customer.first_name | string | Customer first name |
__customer.last_name | string | Customer last name |
__customer.other_names | string | Other / middle names |
__customer.email | string | Email address |
__customer.phone | string | Phone number |
__customer.billing_address | string | Billing address (free text) |
__customer.shipping_address | string | Shipping address (free text) |
__customer.zip_code | string | ZIP / postal code |
__customer.profile_picture_url | string? | Profile picture URL (null if not set) |
__customer.is_verified | bool | Whether email is verified |
__customer.email_verified_at | datetime? | Verification timestamp (null if unverified) |
__customer.country_id | int? | Country ID |
__customer.country_name | string? | Country name (resolved server-side) |
__customer.region_id | int? | State / Region ID |
__customer.state_name | string? | State / Region name (resolved server-side) |
__customer.city_id | int? | City / LGA ID |
__customer.city_name | string? | City / LGA name (resolved server-side) |
__customer.total_orders | int | Summary: Total number of orders |
__customer.orders | int | Alias of total_orders |
__customer.paid_orders | int | Number of paid orders |
__customer.total_spend | decimal | Total lifetime spend |
__customer.currency | string | Currency code (e.g. "USD", "NGN") |
__customer.latest_order_at | datetime? | Most recent order timestamp |
__customer.addresses | array | Array of address objects: {id, type, address, country_id, country_name, region_id, region_name, city_id, city_name, zip_code, is_default} |
__customer.default_billing_address | string | Default billing address from Customer entity (BillingAddress column) |
__customer.default_shipping_address | string? | Default shipping address from Customer entity (ShippingAddress column, nullable) |
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 | Any template marked with defaultHome: true, or a key containing home | home.default, theme.home |
| 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"
}
}
Using Appearance Logos in .cw Templates
cw_get_store() exposes the same branding logos directly for convenience. Use store.logo or store.logoDefault for the default logo, store.logoLight for light backgrounds, and store.logoDark for dark backgrounds. The snake-case aliases store.logo_default, store.logo_light, and store.logo_dark are also available.
@code
__store = cw_get_store();
__appearance = cw_get_appearance();
@endcode
<header class="site-header site-header--light">
@if(__store.logoLight)
<img src="{ __store.logoLight }" alt="{ __store.name }">
@elseif(__store.logo)
<img src="{ __store.logo }" alt="{ __store.name }">
@else
<img src="@asset('assets/images/logo.png')" alt="{ __store.name }">
@endif
</header>
<footer class="site-footer site-footer--dark">
@if(__store.logoDark)
<img src="{ __store.logoDark }" alt="{ __store.name }">
@elseif(__store.logo)
<img src="{ __store.logo }" alt="{ __store.name }">
@else
<img src="@asset('assets/images/logo-white.png')" alt="{ __store.name }">
@endif
</footer>
Theme Colors, Typography, and Header Layout
Theme colors, font choices, and the sticky-header preference are saved in appearanceJson and exposed as data only. They do not automatically repaint, relayout, or override an installed v2 .cw theme. A theme uses these values only when its template, partial, section, CSS, or script explicitly reads them through cw_get_appearance() or cw_get_store().appearance. Logo placement is not an Appearance setting; header partials own logo layout.
Theme authors can seed these values with appearanceDefaults in manifest.json. Those defaults are written into the merchant's Appearance settings during Theme Demo Import, then the theme may read them like any other Appearance value.
__appearance.theme.primary, changing Primary in the dashboard will not change that theme. This is intentional so theme authors keep full control of their design.
| UI Group | JSON Path | Available Keys | Template Access |
|---|---|---|---|
| Theme Colors | appearanceJson.theme | primary, background, surface, text, muted, border | __appearance.theme.primary |
| Typography / Google Fonts | appearanceJson.typography | body, header, navigation, title, h1âĻh6, button, price | __appearance.typography.body |
| Header Layout | appearanceJson.layout.header | sticky | __appearance.layout.header.sticky |
@code
__appearance = cw_get_appearance();
__store = cw_get_store();
@endcode
<style>
:root {
--cw-color-primary: {{ __appearance.theme.primary }};
--cw-color-background: {{ __appearance.theme.background }};
--cw-color-text: {{ __appearance.theme.text }};
--cw-color-border: {{ __appearance.theme.border }};
--cw-font-body: {{ __appearance.typography.body }};
--cw-font-heading: {{ __appearance.typography.header }};
}
body {
background: var(--cw-color-background);
color: var(--cw-color-text);
font-family: var(--cw-font-body), sans-serif;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--cw-font-heading), sans-serif;
}
</style>
<header class="@if(__appearance.layout.header.sticky) cw-header--sticky @endif">
@include('partials/header-light')
</header>
cw_get_store() also returns the same object as store.appearance, so templates can use whichever shape is more convenient. These values are storefront-specific; two customers using the same theme can have different Appearance settings, and the theme decides how much of those settings to honor.
Preferred Places to Use Appearance Values
- Layout templates: read
cw_get_appearance()once near the top oftemplates/home.default.cw,templates/product.detail.cw, or another route template, then pass the variables naturally through included sections/partials. - Header and footer partials: use
__appearance.layout.header.stickyfor sticky-header classes and__store.appearance.branding.logoDefaultfor merchant-provided brand assets. - Theme CSS variables: emit a small
<style>block mapping Appearance values to CSS custom properties, then let normal theme CSS consume those variables. - Component sections: use color/font values only where the section intentionally supports merchant styling. If a section has a fixed art-directed design, ignore Appearance values there.
When a converted static theme expects sticky behavior on a specific wrapper, compute the full class string before passing it into a widget setting. Do not pass literal interpolation text such as { __sticky } inside a quoted widget setting; pass the computed variable instead.
@code
__appearance = cw_get_appearance();
__header_row_class = __appearance.layout.header.sticky
? 'row header-bt-h3 clearfix header-sticky'
: 'row header-bt-h3 clearfix';
@endcode
@widget('core.custom-element', [
'tag' => 'div',
'class' => __header_row_class,
'children' => [
{{-- header child widgets --}}
],
])
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 structured settings to JSON columns and optional snippet fields on the StorefrontSettings record:
| Column | Contents | API Field |
|---|---|---|
AppearanceJson | branding, theme, layout, typography | appearanceJson |
CmsSettingsJson | reading, discussion, avatars, media, permalink, privacy | cmsSettingsJson |
CustomCss | Merchant CSS snippet. Not auto-injected; fetched with cw_get_custom_css() or @cw_custom_css. | customCss |
HeaderHtml | Merchant header HTML snippet. Not auto-injected; fetched with cw_get_header_html() or @cw_header_html. | headerHtml |
FooterHtml | Merchant footer HTML snippet. Not auto-injected; fetched with cw_get_footer_html() or @cw_footer_html. | footerHtml |
The JSON columns are serialized JSON objects. The frontend loads all fields 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, system page selections, and fallback SEO metadata as storefront runtime settings. These settings are storefront-specific, so one customer's choices do 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.maintenance | string | Default active .cw template key for maintenance mode when no specific maintenance CMS page/template is selected. |
defaultTemplateKeys.notFound | string | Default active .cw template key for unresolved storefront routes when no specific 404 CMS page/template is selected. |
defaultPageIds.notFound | number | Optional CMS page ID to render as the storefront 404 page. If omitted, CoreWave360 uses the theme's not-found or page.not-found template. |
seoDefaults.titleFormat | string | Fallback document title format selected in Storefront â Overview. Supported tokens are {{ page_title }} and {{ shop_name }}. |
seoDefaults.defaultMetaDescription | string | Fallback meta description for storefront routes that do not have page, blog post, or item-specific SEO descriptions. |
{
"defaultTemplateKeys": {
"productDetail": "product.detail",
"blogArchive": "blog.magazine",
"blogPost": "post.editorial",
"maintenance": "maintenance.anton",
"notFound": "anton.not-found"
},
"defaultPageIds": {
"notFound": 42
},
"seoDefaults": {
"titleFormat": "{{ page_title }} - {{ shop_name }}",
"defaultMetaDescription": "Shop quality products from our online store."
}
}
Title Format Tokens
| Token | Description | Example Value |
|---|---|---|
{{ page_title }} | The current page, product, blog, post, account section, or system page title. | Wireless Headphones |
{{ shop_name }} | The storefront name configured in Storefront â Overview. | Demo Store |
{{ page_title }} - {{ shop_name }}
{{ page_title }} | {{ shop_name }}
{{ page_title }}
{{ shop_name }} - {{ page_title }}
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. |
Product search page path | product-search | Path exposed as cw_base_paths().product_search. Use for a product search page, product results page, or any theme-defined search design. |
Blog search page path | blog-search | Path exposed as cw_base_paths().blog_search. Use for blog/post search designs. |
Unified search page path | search | Path exposed as cw_base_paths().search and cw_base_paths().unified_search. Use when one page searches products, blogs, categories, or custom data together. |
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 Search page paths (Product search = "find-products", Blog search = "find-posts", Unified search = "search"): /find-products?q=shoes Product search page chosen by the theme /find-posts?q=launch Blog search page chosen by the theme /search?q=invoice Unified search page chosen by the theme
cw_base_paths() and starter-content token resolution, but the theme decides what to render there and how to interpret query strings such as ?q=shoes.
Theme Template Usage
Theme templates can use cw_base_paths(), cw_route(), cw_product_url(product), cw_category_url(category), cw_brand_url(brand), and cw_collection_url(collection) to generate links that respect the configured permalink bases and platform-host storefront handles. Template authors should not hardcode /products, /blogs, /shop, or /category in reusable themes.
@code
__paths = cw_base_paths();
@endcode
<a href="{{ __paths.blog }}">Blog</a>
<a href="{{ __paths.category }}/clothing">Clothing</a>
<form action="{{ __paths.product_search }}" method="GET">
<input type="search" name="q" placeholder="Search products">
</form>
<a href="{{ __product.url }}">{{ __product.title }}</a>
Using Base-Path Tokens in Starter Content
Theme manifests can use cw_base_paths().product_search, cw_base_paths().blog_search, or cw_base_paths().search as a page handle or menu pageHandle. During theme install, CoreWave360 resolves the token to the store owner's configured path and creates/links the matching page. If the merchant later changes the path, future theme installs use the new value; existing pages are not silently renamed.
Menu url fields can also use install-time route tokens. Use cw_base_paths().search when you want the base path itself, and use cw_route('account.default') when you need a complete route that includes its section, such as /{accountBase}/dashboard.
{
"starterContent": {
"pages": [
{
"title": "Product Search",
"handle": "cw_base_paths().product_search",
"templateKey": "anton.search.products",
"sortOrder": 21,
"publish": true
},
{
"title": "Search",
"handle": "cw_base_paths().search",
"templateKey": "anton.search",
"sortOrder": 22,
"publish": true
}
],
"menus": {
"main": [
{
"label": "Search Results",
"pageHandle": "cw_base_paths().product_search",
"sortOrder": 6
},
{
"label": "Account",
"url": "cw_route('account.default')",
"sortOrder": 7
},
{
"label": "Unified Search",
"url": "cw_base_paths().search",
"sortOrder": 8
}
]
}
}
}
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.post') }">
@hook('login.form.before')
<input type="hidden" name="csrf_token" value="{ cw_csrf_token() }">
<input type="hidden" name="redirect_url" value="{ cw_route('account.default') }">
<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('forgot-password') }">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.post') }">
@hook('register.form.before')
<input type="hidden" name="csrf_token" value="{ cw_csrf_token() }">
<input type="hidden" name="redirect_url" value="{ cw_route('account.default') }">
<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="register-country">Country</label>
<select name="country_id" id="register-country" data-region-target="register-region" data-city-target="register-city" required class="form-control">
<option value="">Select country</option>
@foreach(__countries as __country)
<option value="{ __country.id }">{ __country.name }</option>
@endforeach
</select>
</div>
<div class="form-group">
<label for="register-region">State / Region</label>
<select name="region_id" id="register-region" data-city-target="register-city" required class="form-control">
<option value="">Select state</option>
</select>
</div>
<div class="form-group">
<label for="register-city">City / LGA</label>
<select name="city_id" id="register-city" required class="form-control">
<option value="">Select city</option>
</select>
</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 6 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
Forgot and Reset Password Forms
Forgot-password and reset-password forms are native POST actions. They must include csrf_token. The forgot-password action sends a signed reset link to the customer if the email exists, without exposing whether the account exists.
{-- templates/password-reset.cw --}
@extends('layouts/main')
@section('content')
@if(__query.token)
<form method="POST" action="{ cw_route('password-reset.post') }">
<input type="hidden" name="csrf_token" value="{ cw_csrf_token() }">
<input type="hidden" name="token" value="{ __query.token }">
<input type="hidden" name="email" value="{ __query.email }">
<label for="password">New password</label>
<input id="password" type="password" name="password" required>
<label for="password_confirm">Confirm password</label>
<input id="password_confirm" type="password" name="password_confirm" required>
<button type="submit">Reset password</button>
</form>
@else
<form method="POST" action="{ cw_route('forgot-password.post') }">
<input type="hidden" name="csrf_token" value="{ cw_csrf_token() }">
<label for="email">Email address</label>
<input id="email" type="email" name="email" required>
<button type="submit">Send reset link</button>
</form>
@endif
@endsection
Login-Aware Menu and Logout
Use @is_logged_in to switch menu content for authenticated customers. Logout is a POST action, so render it as a small form or button rather than a plain anchor.
<ul class="submenu submenu_user">
@is_logged_in
<li>
<a href="{ cw_route('account.default') }" title="My Account">My Account</a>
</li>
<li>
<form method="POST" action="{ cw_route('logout.post') }" class="logout-form">
<input type="hidden" name="csrf_token" value="{ cw_csrf_token() }">
<input type="hidden" name="redirect_url" value="{ cw_route('login') }">
<button type="submit" title="Logout">Logout</button>
</form>
</li>
@else
<li><a href="{ cw_route('login') }" title="Login">Login</a></li>
<li><a href="{ cw_route('register') }" title="Register">Register</a></li>
@endis_logged_in
</ul>
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>
404 Page (not-found / page.not-found)
The 404 page is shown when a storefront route cannot resolve its CMS page, product, blog, or blog post. Store owners can choose a CMS page for this in Storefront â Overview â Maintenance Mode â 404 page. If no CMS page is selected, CoreWave360 falls back to a theme template named not-found or page.not-found.
{-- templates/page.not-found.cw or templates/not-found.cw --}
@code
__store = cw_get_store();
__route = cw_current_route();
@endcode
<main class="not-found-page">
<h1>Page not found</h1>
<p>We couldn't find that page on {{ __store.name }}.</p>
<a href="/">Return home</a>
</main>
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
__customer = cw_customer();
if (__customer && __customer.error) { __customer = null; }
__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
<div class="account-page">
<h1>Welcome, { __customer.first_name ?? '-' }</h1>
<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.) |
Single Account Item Pages
Account templates can read a single item lookup from the URL. The base path is the configured accountBase from Storefront â Appearance â Permalinks, not a hardcoded /account. For example, if accountBase is my-account, the order detail URL is /my-account/order-detail/{id|reference|slugified-reference}.
{{-- templates/account/order-detail.cw --}}
@code
__order_lookup = cw_current_account_item_lookup();
__order = cw_get_customer_order();
@endcode
@if(__order.error)
<p>Order not found.</p>
@else
<h1>Order {{ __order.reference }}</h1>
<p>Status: {{ __order.status }}</p>
@endif
{{-- templates/account/returns.cw, for /{accountBase}/returns/{id|order-reference|slugified-reference} --}}
@code
__return_lookup = cw_current_account_item_lookup();
__return = cw_get_customer_return();
@endcode
@if(__return.error)
<p>Return request not found.</p>
@else
<h1>Return {{ __return.id }}</h1>
<p>Order: {{ __return.orderReference }}</p>
@endif
Auth-Related Data Functions
These functions are available in account and auth templates:
| Function | Returns | Description |
|---|---|---|
cw_customer() / cw_user() | object | Primary customer accessor. Returns a flat object with all profile fields at the top level: .id, .first_name, .last_name, .email, .phone, .billing_address, .shipping_address, .zip_code, .country_id, .country_name, .region_id, .state_name, .city_id, .city_name, .default_billing_address, .default_shipping_address, .total_orders, .total_spend, .addresses, etc. Returns {error} when not authenticated. Use __customer = cw_customer(); then access properties directly: __customer.email, __customer.first_name. |
cw_get_customer_profile() | object | Returns nested structure {customer: {...}, summary: {...}} for the active session. Prefer cw_customer() for flat property access. Returns {error} when not authenticated. |
cw_get_customer_summary() | object | Returns {totalOrders, paidOrders, totalSpend, currency, latestOrderAt} for the active session. |
cw_get_customer_addresses() | array | Returns saved address rows with legacy billing/shipping fallback entries when the customer has no structured rows. |
cw_resend_verification_email(email) | object | Resends the email verification email. Accepts optional on_success and on_failure callbacks. |
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?) / cw_get_current_order() / cw_get_order_details() | object or null | Returns a single order by ID, reference, payment link ID/reference, invoice ID/reference, or slugified reference. Omitting the argument reads the current account URL item lookup. |
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_get_customer_return(return_id?) / cw_get_current_return() / cw_get_return_details() | object or null | Returns a single return by return ID, order ID, order reference, product ID, or slugified reference. Omitting the argument reads the current account URL item lookup. |
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(name?, business_name?, first_name?, last_name?, other_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. name is the optional business/display name; the first/last/other name fields are for the contact person. Supports location fields (country_id, region_id, city_id) |
cw_login_user(email, password, redirect_url?, send_login_otp?, otp_required?, on_success?, on_failure?) | object | Authenticates with email and password. OTP challenge is enforced automatically when Storefront Settings â Customer Accounts â Require OTP for customer login is enabled. Returns session token when OTP is not required, or otp_sent: true with otp_token when OTP challenge is needed. |
cw_get_customer_account_settings() | object | Returns the customer account configuration from storefront settings: requireEmailVerification, verificationRequiresOtp, sendWelcomeMail, requireLoginOtp. Use to conditionally show the correct messages after registration or login. |
cw_verify_login(challenge_token, code, redirect_url?, on_success?, on_failure?) | object | Verifies the OTP code and establishes a customer session. Returns redirectUrl on success. |
cw_logout_user() | void | Clears the current customer session |
cw_register_user(first_name, last_name, email, password, name?, business_name?, phone?, country_id?, region_id?, city_id?, address?, zip_code?, redirect_url?, on_success?, on_failure?) | object | Registers a new customer account. Returns either email_verification_required or session token depending on storefront verification settings. Use redirect_url for post-registration redirect. |
cw_verify_register(token, on_success?, on_failure?) | object | Verifies a registration verification token. Returns verified: true on success. |
cw_count(var) | int | Returns count of collection items, or the numeric value for integers/decimals. Returns 0 for null. |
cw_format_money(amount, currency) | string | Formats a monetary value with currency symbol (e.g. $19.99) |
cw_route(name, params) | string | Generates a route URL by name. Page aliases include login, register, forgot-password, password-reset, and account.default. Native POST aliases include login.post, register.post, logout.post, forgot-password.post, password-reset.post, contact.post, account.profile.post, and account.addresses.post. |
@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.
Accessing Customer Data with cw_customer()
cw_customer() is the primary function for accessing authenticated customer data in account templates.
It returns a flat object with all profile and summary fields at the top level â no nested .customer or .summary access required.
Recommended Pattern
@code
__customer = cw_customer();
if (__customer && __customer.error) { __customer = null; }
__customer_name = __customer.first_name;
__customer_email = __customer.email;
__customer_phone = __customer.phone;
__orders = __customer.orders;
@endcode
{{-- Use with null-coalescing for safe output --}}
<p>Hello, <strong>{ __customer_name ?? '-' }</strong></p>
<p>Email: { __customer_email ?? 'Not set' }</p>
<p>Total Orders: { __customer.total_orders ?? 0 }</p>
{{-- Conditional display --}}
@if(__customer.phone)
<p>Phone: { __customer.phone }</p>
@endif
{{-- Addresses iteration --}}
@if(cw_count(var: __customer.addresses) > 0)
@foreach(__customer.addresses as __addr)
<p>{ __addr.type }: { __addr.address }</p>
@endforeach
@endif
Available Flat Properties
All properties returned by cw_customer() are at the top level:
| Category | Properties |
|---|---|
| Identity | id, name, first_name, last_name, other_names |
| Contact | email, phone |
| Address | billing_address, shipping_address, zip_code, default_billing_address, default_shipping_address, addresses[] |
| Verification | is_verified, email_verified_at |
| Location | country_id, country_name, region_id, state_name, city_id, city_name |
| Summary | total_orders, orders, paid_orders, total_spend, currency, latest_order_at |
| Media | profile_picture_url |
cw_get_customer_profile() returns a nested structure
{customer: {...}, summary: {...}} requiring __customer = __profile.customer.
This is retained for backward compatibility but not recommended for new templates.
Always prefer the flat cw_customer() for simpler, more readable template code.
Route, Title, CSRF, and Price Helpers
These helpers are available globally and are commonly used in layouts, forms, account pages, product cards, and SEO metadata. They respect the storefront's configured base paths where applicable.
@section('head')
<title>{ cw_get_page_title(fallback: 'Storefront') } - { cw_get_store().name }</title>
@endsection
{{-- cw_route('login') is the login page URL.
Native forms post to cw_route('login.post') and must include csrf_token. --}}
<form method="POST" action="{ cw_route('login.post') }">
<input type="hidden" name="csrf_token" value="{ cw_csrf_token() }">
<input type="hidden" name="redirect_url" value="{ cw_route('account.default') }">
<input type="email" name="email" required>
<input type="password" name="password" required>
<button type="submit">Sign in</button>
</form>
{{-- JSON/fetch posts may send the same token as csrfToken, X-CSRF-Token, or X-CoreWave-CSRF. --}}
@code
__products = cw_get_products(limit: 8, order: 'desc');
@endcode
@has_products(__products)
@foreach(__products as __product)
<a href="{ cw_product_url(__product) }">
{ __product.name }
<span>{ cw_format_price(__product.price) }</span>
</a>
@endforeach
@endis
Account Template Directory Structure
templates/ âââ home.cw âââ login.cw // Customer sign-in âââ register.cw // Customer registration âââ verify-email.cw // Email verification landing page âââ 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.
Email Verification Template
When email verification is enabled in Storefront â Appearance â CMS Settings â Customer Accounts, new customers receive a verification email containing a link like:
https://yourstore.com/storefront/verify-email?token=abc123def456...
Clicking this link renders the verify-email.default template.
Create this template in your theme package as templates/verify-email.cw
(or assign a custom template key via Storefront â Pages â Template Assignments).
Template Key Resolution
| Priority | Source | Example |
|---|---|---|
| 1 | Assigned template (Storefront â Pages) | Admin sets verifyEmail assignment to my-verification |
| 2 | Fallback default | verify-email.default |
Available Template Variables
| Variable | Type | Description |
|---|---|---|
__request.token | string | The verification token from the URL query string |
__request | object | All query string parameters as a key-value map |
Template Example
{{-- templates/verify-email.cw â Email verification landing page --}}
@extends('layouts/main')
@section('head')
<title>Email Verification - {{ __store.name }}</title>
@endsection
@section('content')
<div class="verify-email-page">
@code
__token = get(__request, 'token', '');
__result = new();
if (__token != '')
__result = cw_verify_register(token: __token, redirect_url: cw_route('login'));
endif
@endcode
@if(__result and __result.verified)
<div class="alert alert-success">
<h4>Email Verified Successfully!</h4>
<p>Your email address has been verified. You can now log in to your account.</p>
<a href="{{ cw_route('login') }}" class="btn btn-primary">Go to Login</a>
</div>
@elseif(__result and __result.error)
<div class="alert alert-danger">
<h4>Verification Failed</h4>
<p>@code echo __result.error; @endcode</p>
<p>The verification link may have expired or already been used. Try registering again or contact support.</p>
</div>
@else
<div class="alert alert-info">
<h4>Verification Required</h4>
<p>Please use the verification link sent to your email address.</p>
<p>Didn't receive the email? <a href="#" onclick="cw_resend_verification_email('{{ __request.email }}'); return false;">Resend verification email</a>.</p>
</div>
@endif
</div>
@endsection
How It Works
- Customer registers â system checks Storefront Settings to determine if verification is required
- If required, a verification email is sent with a link to
/verify-email?token=<token> - Customer clicks the link â storefront resolves route kind
"verify-email" - Storefront loads
verify-email.defaulttemplate (or admin-assigned template) - Template reads
__request.tokenand callscw_verify_register(token: __token) - On success: customer is logged in and redirected
- On failure: template shows the error message
- If customer tries to login before verifying, the system auto-resends the verification email
cw_verify_register function accepts optional on_success and on_failure callback parameters for AJAX-based templates. For server-rendered templates, use @if blocks to check the result as shown above.
Form Error Handling #
CoreWave360 provides multiple mechanisms for handling form submission errors in your theme templates. Understanding these patterns is essential for building robust login, registration, and contact forms.
Auth Form Error Query Parameters
When a user submits a login, registration, forgot-password, or reset-password form via standard HTML <form action="..." method="POST">, the backend redirects back with error/success flags in the URL query string:
| Scenario | Redirect URL | How to Display |
|---|---|---|
| Login failed | /login?auth_error=Invalid email or password | @if(__session.login_error)<div class="alert">{{ __session.login_error }}</div>@endif |
| Registration failed | /register?auth_error=Email already exists | @if(__session.register_error)<div class="alert">{{ __session.register_error }}</div>@endif |
| Forgot password success | /password-reset?reset_sent=1 | @if(__session.reset_sent)<div class="alert alert-success">Check your email for reset instructions.</div>@endif |
| Password reset success | /login?reset_success=1 | @if(__session.reset_success)<div class="alert alert-success">Password reset successfully. Please login.</div>@endif |
Storefront Contact Forms
Contact forms should post to the native CoreWave contact route. This saves the shopper's message in the store owner's contact inbox and also queues email when the store has a business email configured.
<form method="POST" action="{ cw_route('contact.post') }">
@csrf
<input type="hidden" name="redirect_url" value="{ __page.url ?? '/contact' }">
<input type="hidden" name="page_url" value="{ __page.url ?? '/contact' }">
<input name="name" placeholder="Your name" required>
<input name="email" type="email" placeholder="Your email">
<input name="phone" placeholder="Your phone">
<input name="subject" placeholder="Subject">
<textarea name="message" placeholder="Message" required></textarea>
<button type="submit">Send message</button>
</form>
Theme authors may add custom fields such as order_number, company_name, or inquiry_type. CoreWave stores those values with the message so the store owner can review them under Storefront → Contact Messages.
| Field | Required | What it means |
|---|---|---|
name | Yes | The shopper's name. |
message | Yes | The message body. |
email or phone | One required | How the store can reply. |
redirect_url | No | Where the shopper returns after submit. The backend adds contact_success or contact_error. |
Session Flash Variables Reference
| Session Variable | Type | Description |
|---|---|---|
__session.login_error | string|null | Error message set by failed login (HTML form POST) |
__session.register_error | string|null | Error message set by failed registration (HTML form POST) |
__session.reset_sent | bool|null | True after successful forgot-password request |
__session.reset_success | bool|null | True after successful password reset |
__session.customer_email | string|null | Email of the currently logged-in customer |
__session.customer_id | string|null | ID of the currently logged-in customer |
__session.customer_token | string|null | JWT session token for the current customer |
__session.auth_redirect_url | string|null | Post-auth redirect URL set during login/register |
AJAX / Client-Side Callbacks
All cw_* auth functions (cw_login_user, cw_register_user, cw_verify_login, cw_verify_register, cw_subscribe_newsletter, cw_submit_contact) accept optional on_success and on_failure parameters. These are JavaScript callback function names that the response includes so your theme's client-side JS can invoke them:
// Theme JavaScript â define your callbacks
function onLoginSuccess(data) {
if (data.redirectUrl) window.location.href = data.redirectUrl;
else window.location.reload();
}
function onLoginFailure(data) {
document.getElementById('login-error').textContent = data.error;
document.getElementById('login-error').style.display = 'block';
}
// Template usage â pass callback names when calling cw functions
<script>
async function handleLogin() {
const result = await fetch('/...', {
method: 'POST',
body: new URLSearchParams({email: '...', password: '...'})
});
const json = await result.json();
// Check for callback hints in the response
if (json.on_success_call && typeof window[json.on_success_call] === 'function')
window[json.on_success_call](json);
if (json.on_failure_call && typeof window[json.on_failure_call] === 'function')
window[json.on_failure_call](json);
}
</script>
When using cw_login_user and cw_register_user inside @code blocks, read the success flag and error field from the returned value:
@code
var result = cw_login_user(email, password, redirectUrl, false, false, "onLoginSuccess", "onLoginFailure");
if (result.success)
// Login succeeded â session is now active
echo "<script>window.location.href='" + result.RedirectUrl + "'</script>";
else
echo "<div class='alert alert-danger'>Login failed: " + result.error + "</div>";
@endcode
Customer Addresses #
Each customer record supports a structured list of billing and shipping addresses. Each address carries its own geographic context (country, region/state, city) independent of the customer record.
Address Flow from Storefront to Institution CRM
- Theme registration / checkout: When a customer signs up or checks out on your storefront, their
firstName,lastName,email, andphoneautomatically populate the contact person fields on the customer record. - Company name: The
namefield from registration forms maps to the customer's business/display name. - Institution CRM: Staff can manage all customer addresses from the Customers section â add, edit, delete, or set defaults.
Default Billing & Shipping Address
Each customer can designate one billing address and one shipping address as their defaults. The default billing address is what appears on invoices, receipts, quotations, and other sales documents. Staff can select default addresses in the institution CRM or via API.
Template Variables for Sales Documents
When a sales document (invoice, quotation, receipt) references a specific billing or shipping address, the following template variables are populated:
| Placeholder | Description |
|---|---|
[customerBillingAddress] | The billing address line |
[customerBillingCity] | The billing address city name |
[customerBillingState] | The billing address region/state name |
[customerBillingCountry] | The billing address country name |
[customerBillingZipCode] | The billing address ZIP/postal code |
[customerShippingAddress] | The shipping address line |
[customerShippingCity] | The shipping address city name |
[customerShippingState] | The shipping address region/state name |
[customerShippingCountry] | The shipping address country name |
[customerShippingZipCode] | The shipping address ZIP/postal code |
[customerAddress] | Convenience alias â resolves to the default billing address |
Geo Per-Address Model
Unlike the legacy approach (single country/region/city on the customer record), each address now carries its own:
CountryIDâ reference to the GeoCountry tableRegionIDâ reference to the GeoRegion tableCityIDâ reference to the GeoCity tableZipCodeâ postal codeIsDefaultâ whether this is the default address of its type
This allows a customer to have a billing address in one country and a shipping address in another â fully independent.
@is_logged_in / @else Bug Fix (v2.x) #
@is_logged_in ... @else ... @endis_logged_in would render both the logged-in and logged-out content blocks when a user was authenticated. This is now fixed â the directive correctly shows only the matching branch using GetBeforeElseBranch(), the same logic used by @if. The same fix applies to all route conditionals: @is_home, @is_page, @is_product, @is_category, @is_blog, @is_single, @is_search, @is_account, @is_cart, @is_checkout, @has_products.
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. |
Full Working Theme Checklist
A production-ready CoreWave360 theme should pass the full checklist below before marketplace submission or customer installation.
| Area | Developer Requirement |
|---|---|
| Package | ZIP contains manifest.json, templates/, partials/, sections/, widgets/, and assets/. No design.json, legacy JSON route templates, or generated build trash are included. |
| Manifest | formatVersion is 3; every template key points to a real .cw file; headerPresets, footerPresets, defaultHeaderPresetKey, and defaultFooterPresetKey are valid. |
| Required Pages | Theme includes templates for home, product listing, product detail, category/tag archives, product search, blog search, unified search, blog archive, blog post, cart, checkout, login, register, forgot password, reset password, account dashboard, orders, order detail, wishlist, returns, reviews, profile, addresses, documents, invoices, maintenance, 404, and generic content pages. |
| Starter Content | starterContent seeds only real pages and menus. Default route assignments use flags such as defaultHome, defaultProductDetails, defaultBlogPage, defaultBlogPost, defaultNotFound, and defaultMaintenance. Menu URLs use route tokens or real URLs, not hardcoded development file names. |
| Headers & Footers | Layouts call @cw_header() and @cw_footer(). Pages that should not show chrome use showHeader: false / showFooter: false or the __none preset sentinel. |
| Editable Widgets | Each independently editable visual block is a widget or embedded child widget. Avoid wrapping a removable widget in static parent HTML that would remain behind after deletion. Nested rows, columns, slides, cards, icon links, and menu panels should be editable as child widgets or repeater items. |
| Widget Fields | Fields use documented types and aliases only. Empty values are allowed and should not emit empty inline CSS, empty attributes, or broken classes. Image/media fields open the media picker and repeaters support add, remove, duplicate, reorder, and nested controls. |
| Data | Products, categories, blogs, posts, cart, checkout, account, wishlist, reviews, and returns are fetched with @query or cw_*() functions. Static demo data is acceptable only as fallback text/images before the merchant imports demo content. |
| Appearance | Theme appearance defaults may be declared in appearanceDefaults, but storefront Appearance values affect public design only when the theme explicitly reads them with cw_get_appearance(), cw_get_theme_settings(), or cw_get_store().appearance. |
| Assets | Theme CSS, JS, fonts, and runtime images are referenced with @asset(). Demo media preview URLs are public bucket URLs and are imported into the merchant media library only during Theme Demo Import. |
| JavaScript | Theme scripts initialize sliders, menus, currency switchers, carts, and interactive controls after the rendered HTML is injected. Scripts should tolerate repeated preview renders and should not duplicate event handlers. |
| Forms | Native POST forms use cw_route('*.post') aliases and include cw_csrf_token() or @csrf. Logout is a POST form/button, not a GET link. |
| Responsive QA | Desktop, tablet, and mobile previews match the source design with no horizontal overflow, clipped text, broken sliders, hidden menus, or overlapping cart/search/account panels. |
| Validation | Run the CoreWave360VS Code extension validation, render major templates in preview, upload package to platform admin, install into a test storefront, import demo content, repair assets if needed, and test the public storefront route through SSR. |
VS Code Theme Extension V2 #
CoreWave360 provides a VS Code extension that adds syntax highlighting, completions, and live preview for .cw theme files. Write and test your themes locally, then package and publish them to the CoreWave360 marketplace.
Installation
- Download the extension:
package.vsix - Open VS Code, go to Extensions (Ctrl+Shift+X or Cmd+Shift+X)
- Click the ... menu â Install from VSIX...
- Select the downloaded
package.vsixfile - Restart VS Code when prompted
Setup
After installing, open your .cw theme folder in VS Code. The extension automatically activates for *.cw files and provides:
- Syntax highlighting for CoreWave360 template directives, expressions, and Blade-style blocks
- Code completions for
cw_*data-access functions, template directives, and route helpers - Live preview â render your local templates against a live storefront for instant feedback
- Package validation â checks your theme structure before publishing to the marketplace
Customer Command Recognition
Version 0.1.5 recognizes the newly supported customer helpers in
completions, hover help, and unknown-function diagnostics. This means valid uses of
cw_user() and
cw_get_customer_summary() no longer receive false warning
diagnostics. The existing cw_customer() and
cw_get_customer_addresses() helpers remain recognized.
Customer profile helpers now share one normalized payload contract: root profile fields plus
nested .customer and .summary.
Version 0.1.5 also recognizes the documented compatibility
helper catalog and the @includeonce directive. Compatibility
helper hover text identifies functions that require storefront runtime support before publishing.
| Function | Editor Support | Runtime Purpose |
|---|---|---|
cw_customer() | Completion, hover, diagnostics | Recommended. Returns flat profile + summary at root level. Access via __customer.email, __customer.first_name, etc. |
cw_user() | Completion, hover, diagnostics | Alias of cw_customer() for theme compatibility. |
cw_get_customer_profile() | Completion, hover, diagnostics | Legacy nested accessor: {customer: {...}, summary: {...}}. Use cw_customer() for new templates. |
cw_get_customer_summary() | Completion, hover, diagnostics | Authenticated order and spend summary. |
cw_get_customer_addresses() | Completion, hover, diagnostics | Authenticated saved-address collection with legacy fallback. |
Type cw-customer in a .cw
file to insert a complete guarded example that loads profile, summary, and address data.
Connecting to a Storefront
To enable live preview, connect the extension to your storefront:
- In the CoreWave360 admin panel, go to Storefront â Appearance â Theme Developer App Access
- Generate a developer API key (it starts with
cwdk_) - In VS Code, open Settings (Ctrl+, or Cmd+,) and set:
corewave.storefrontNameâ your storefront handle or namecorewave.apiKeyâ the API key you generatedcorewave.storefrontBaseUrlâhttps://storefront.corewave360.com(or your custom domain)
The extension uses these settings to fetch store data (products, categories, blog posts) and render your local templates against your live storefront environment.
File Icon Theme
The extension ships a CoreWave360 Icons file icon theme. After installing, open VS Code's File Icon Theme picker and choose CoreWave360 Icons to show the CoreWave360 logo beside .cw files in Explorer.
Packaging for Distribution
Once your theme is complete, use the extension's Package Theme command (Ctrl+Shift+P â "CoreWave: Package Theme") to produce a distributable .zip file ready for the CoreWave360 marketplace.