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.

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

Architecture Overview

The CoreWave360 storefront uses a backend-rendered template pipeline:

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

Key Features

  • Directive-based templates — 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 — CoreWave360cw_get_products(), cw_get_categories(), cw_get_cart(), etc.
  • Hook and filter system — Extend templates with @hook and @filter directives
  • Plugin integration — Bundle companion plugins via bundled-plugins/
  • v2-only visual editing — Theme editing focuses on runtime .cw templates, sections, partials, settings, and route assignments

Rendering Pipeline

TEXT
┌─────────────────────────────────────────────────┐
│ 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 .cw template files and .json configuration
  • 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:

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

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:

  1. theme.css (base styles)
  2. responsive.css (breakpoint overrides — loaded AFTER base)
  3. custom.css (theme-specific overrides)
  4. @cw_custom_css (storefront admin custom CSS)

Responsive Breakpoints

Theme authors should target these common breakpoints:

BreakpointTargetsExample
max-width: 480pxSmall phonesSingle-column layouts, stacked elements
max-width: 768pxTablets and large phonesTwo-column → single-column, collapsed navigation
max-width: 1024pxSmall desktops/landscape tabletsReduced sidebar widths, adjusted grid gaps
min-width: 1025pxDesktopFull multi-column layouts

Widget Responsive Settings

Widgets support responsive visibility controls via the responsiveConfig object:

SettingDescription
hideDesktopHide widget on screens wider than 1024px
hideTabletHide widget on screens between 481px and 1024px
hideMobileHide widget on screens narrower than 480px
columnsDesktop / columnsMobileGrid 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.

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

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

  "templates": {
    "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

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

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

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

Basic Template Structure

HTML
{{--
  Template Name: Home Page
  Template Key: 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:

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

Template Inheritance

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

Layout file: templates/layouts/main.cw

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

Child template: templates/page.default.cw

HTML
@extends('layouts/main')

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

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

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

Data Setup with @code

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

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

Sections & Partials V2 #

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

Text
theme.zip
├── templates/
│   ├── 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.

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

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

Use a Section

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

HTML
@extends('layouts/main')

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

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

Template Directive Reference #

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

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

Conditionals

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

Code Execution

DirectivePurposeExample
@code ... @endcodeInline C# code block@code __title = cw_get_store_name(); @endcode
@echo(__value)Output a value@echo(__product.title)
@var(__key, __value)Set template variable@var(__title, 'My Page')
@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')
@csrfRender a hidden storefront CSRF input@csrf
@debug(__value)Render debug output for local/theme development@debug(__route)
@php ...Unsupported migration placeholderUse @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

DirectivePurposeExample
@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') ... @endsectionDefine a content section@section('content') ... @endsection
@yield('name')Render a section from parent layout@yield('content')

Widgets & Hooks

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

Assets & URLs

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

DirectivePurposeExample
@is_home() ... @endisRender only on the storefront home/catalogue root.@is_home() <h1>Welcome</h1> @endis
@is_page('slug') ... @endisRender only on a CMS page. Passing a slug restricts the block to that page handle.@is_page('about-us') ... @endis
@is_product() ... @endisRender only on a single product detail route.@is_product() @include('partials/product-breadcrumb') @endis
@is_category('slug') ... @endisRender only on a category archive. Passing a slug restricts the block to that category.@is_category('clothing') ... @endis
@is_blog() ... @endisRender on the blog index or a blog archive route.@is_blog() <aside>Blog filters</aside> @endis
@is_single() ... @endisRender only on a single blog post route.@is_single() @include('partials/post-share') @endis
@is_search() ... @endisRender only on a search results route.@is_search() <p>Search results</p> @endis
@is_account() ... @endisRender only inside the customer account area.@is_account() @include('account/sidebar') @endis
@is_cart() ... @endisRender only on the cart route.@is_cart() @include('cart/summary') @endis
@is_checkout() ... @endisRender only on the checkout route.@is_checkout() @include('checkout/steps') @endis
@has_products() ... @endisRender 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) ... @endisRender only when an entity exposes an image, thumbnail, or URL image field.@has_image(__product) <img src="{ __product.image }"> @endis
@user_logged_in() ... @endisRender only when a customer session is active.@user_logged_in() <a href="/account">Account</a> @endis
@is_logged_in ... @endis_logged_inAlias for @user_logged_in(); useful for simpler auth-gated blocks.@is_logged_in Welcome back @endis_logged_in
CW
@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

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

Implemented v2 Runtime Compatibility Notes

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

@include('header')
@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)
SyntaxSupported?Details
'value' / "value"YesString literals are unquoted before use.
key=valueYesClassic named argument syntax.
key: valueYesRecommended for readable single-line calls.
['key' => 'value']YesAccepted as an alternate array argument syntax.
__object.propertyYesWorks for C# objects, dictionaries, JSON objects, arrays with numeric indexes, and function return objects.
cw_get_store().nameYesFunction results can be accessed with dot-property syntax after JSON parsing.

Inline Expression Syntax

Three inline expression forms are supported:

SyntaxDescriptionExample
{{ 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:

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

@query — The Core Data Directive

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

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

Supported Query Parameters

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

@is_logged_in / @user_logged_in — Login-Aware Block

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

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

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

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

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

Data Access Functions #

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.

Use this rule: Use @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.

CW
@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
NeedUseExample
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 categorycategory filter@query(__products, ['type' => 'products', 'category' => 'shirts'])
Products by tagtag 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.

CW
@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.

CW
@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.

PartFieldsMeaning
deliveryPreferencepickup, ship, or digitalHow the order will be fulfilled. Digital-only carts do not need pickup or shipping.
PickuppickupLocationId, warehouseId, pickupName, pickupAddress, pickupPhoneThe selected store, warehouse, or pickup point. Pickup is selected by default when available.
ShippingproviderCode, courierId, courierName, courierImageUrl, serviceCode, rateRequestToken, shippingRateId, shippingProfileId, shippingZoneIdThe selected flat/free/weight/price rate or live courier rate.
Quoteamount, currencyCode, estimatedDeliveryText, estimatedDeliveryDate, quoteExpiresAtThe customer-facing delivery cost and ETA. Expired quotes are rejected and must be refreshed.
DestinationcountryCode, countryName, regionName, cityName, address, postalCode, customerName, customerEmail, customerPhoneThe delivery address used to quote and validate the order.
Hidden fieldscheckout[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.
What CoreWave protects: CoreWave360 checks the delivery price again, confirms pickup locations are active, checks warehouse stock for pickup orders, rejects expired shipping quotes, and stores the complete delivery record on the checkout and order.

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.

ChoiceHow it worksWhat shoppers see
Pickup onlyThe store accepts pickup from saved pickup locations, storefront warehouses, or the store address.Pickup options first, with the address and contact details.
CoreWave delivery accountCoreWave 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 accountThe 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 onlyThe 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 choiceBest useWhat to configure
CoreWave-managed paymentThe 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 accountThe 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 paymentThe 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.

CW
@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.

CW
@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.

NeedUse
Theme image, CSS, or JS file@asset('assets/images/logo.png'), @css('assets/css/style.css'), @js('assets/js/main.js')
Named storefront routecw_route('cart'), cw_route('checkout'), cw_route('account.default')
Product URLcw_product_url(__product)
Category, brand, collection URLscw_category_url(__category), cw_brand_url(__brand), cw_collection_url(__collection)
Permalink basescw_base_paths()
Store details and logoscw_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.

CW
@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
External fetch limits: Only absolute 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.

HelperUse
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 useUse 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.

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

Current Route Functions

HTML
@code
  __route = cw_current_route();
  __product_id = cw_current_product_id();
  __product_slug = cw_current_product_slug();
  __category_slug = cw_current_category_slug();
  __blog_id = cw_current_blog_id();
  __blog_slug = cw_current_blog_slug();
  __post_id = cw_current_post_id();
  __post_slug = cw_current_post_slug();
  __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
FunctionReturnsDescription
cw_current_route()objectReturns kind, productSlug, categorySlug, blogSlug, postSlug, accountSection, accountItemLookup, and related route fields.
cw_current_product_id()stringCurrent product ID when the product was resolved.
cw_current_product_slug()stringThe product URL segment. Product URLs accept numeric ID, SKU, or slugified product name.
cw_current_category_slug()stringCurrent category route segment.
cw_current_blog_id()stringCurrent blog ID when available.
cw_current_blog_slug() / cw_current_blog_handle()stringCurrent blog handle from the URL.
cw_current_post_id()stringCurrent blog post ID when available.
cw_current_post_slug() / cw_current_post_handle()stringCurrent post handle from the URL.
cw_current_account_section()stringCurrent account section, such as dashboard, orders, order-detail, returns, invoices, or documents.
cw_current_account_item_lookup()stringThe 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()stringThe numeric account item ID when the URL lookup is numeric.
cw_current_account_item_slug()stringAlias-style helper for the URL item lookup when themes prefer slug terminology.
cw_current_order_id()stringCurrent 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()stringCurrent 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()stringReturns 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()stringReturns 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.

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

Single Product Page

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

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

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

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

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

Single Blog Archive

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

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

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

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

Single Blog Post

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

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

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

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

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

Single Lookup Functions

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

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

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

Products

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

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

Blogs & Posts

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

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

Template Examples #

Product Card Partial

File: sections/product-card.cw

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

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

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

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

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

    <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>
Product action buttons: For SSR-rendered cards and detail pages, prefer delegated buttons with 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

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

@include('header')

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

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

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

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

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

@include('footer')

Product Detail Page

File: templates/product.detail.cw

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

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

@include('header')

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

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

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

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

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

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

      {{-- 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 }

{ 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
@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.' ])
@else

Your cart is empty.

@endif
@endsection
Use the shared delivery widget: a checkout that only sends 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

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

Referencing Assets in Templates

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

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

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

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

CSS Loading Priority

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

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

Asset URL Resolution

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

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

Theme Surfaces #

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

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

Pseudo-Code Hooks System #

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

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

Declaring Hooks in Manifest

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

Use Cases

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

Available Hooks

Public developer scope: this guide only documents hooks intended for theme/plugin developers. Platform-only hooks for admin UI, internal API interception, webhooks, media storage, schedulers, cron jobs, AI pipelines, and privileged backend workflows are visible only to platform admins inside Platform Admin → Widget Hooks → System Hook Catalog. Public theme packages should rely only on hooks listed here and the published developer contract for the package type.
HookDescription
corewave.theme.page.renderRuns while a storefront page is being rendered.
corewave.theme.section.renderRuns while a theme section is being rendered.
corewave.theme.block.renderRuns while a visual-editor block is being rendered.
corewave.theme.template.resolveRuns when the storefront resolves which template should handle a request.
corewave.checkout.before_submitRuns before checkout is submitted.
corewave.checkout.after_submitRuns after checkout submission succeeds.
corewave.checkout.validateRuns during checkout validation.
corewave.checkout.payment.method.listRuns while payment methods are being listed.
corewave.checkout.shipping.quoteRuns while shipping quotes are being resolved.
corewave.checkout.discount.applyRuns when a discount is applied.
corewave.checkout.discount.validateRuns while a discount is validated.
corewave.order.createdRuns after an order is created.
corewave.order.paidRuns after an order payment is confirmed.
corewave.order.fulfillment.updatedRuns after fulfillment state changes.
corewave.order.refund.createdRuns after a refund is created.
corewave.order.cancelledRuns after an order is cancelled.
corewave.inventory.product.createdRuns after an inventory product is created.
corewave.inventory.product.updatedRuns after an inventory product is updated.
corewave.inventory.stock.lowRuns when stock reaches the low-stock threshold.
corewave.inventory.stock.changedRuns after product stock changes.
corewave.catalogue.product.card.extendExtends product-card rendering.
corewave.catalogue.product.detail.extendExtends product-detail rendering.
corewave.navigation.menu.extendExtends a navigation menu before render.
corewave.navigation.menu.resolveRuns while a navigation menu is being resolved.
corewave.blog.post.renderRuns while a blog post is being rendered.
corewave.blog.comment.before_createRuns before a blog comment is created.
corewave.blog.comment.after_createRuns after a blog comment is created.
corewave.customer.before_signupRuns before customer registration completes.
corewave.customer.after_signupRuns after customer registration completes.
corewave.customer.login.successRuns after customer login succeeds.
corewave.customer.profile.updatedRuns after customer profile changes.
corewave.customer.account.menu.extendExtends customer-account navigation.
corewave.customer.address.before_saveRuns before a customer address is saved.
corewave.customer.address.after_saveRuns after a customer address is saved.
corewave.customer.add_to_cartRuns after a product is added to cart.
corewave.customer.wishlist_toggleRuns after a wishlist item is toggled.
corewave.notifications.dispatchRuns when a storefront notification is dispatched.
corewave.notifications.template.resolveRuns while resolving a notification template.
corewave.notifications.channel.resolveRuns while resolving the notification channel.
corewave.search.index.beforeRuns before search indexing.
corewave.search.index.afterRuns after search indexing.
corewave.search.query.transformTransforms storefront search queries.

Available Actions

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

Block Fields Reference

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

Plugin Integration #

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

Plugin Manifest

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

  "corewavePseudoCode": {
    "engine": "corewave-pseudo/1.0",
    "blocks": [
      {
        "id":     "whatsapp-button",
        "hook":    "corewave.theme.page.render",
        "action": "inject_html",
        "args": {
          "position": "body_end",
          "html":    "<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:

Text
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:

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

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

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

Standalone Plugins (Not Bundled)

To create a standalone plugin (uploaded separately from a theme):

  1. Create a ZIP with manifest.json, hooks/ (for pseudo-code), and assets/
  2. Register pseudo-code hooks in manifest.json under pseudoCodeHooks
  3. Upload via Storefront → Plugins → Upload
  4. Plugins can be activated/deactivated independently of themes

Plugin Marketplace Submission

To submit a plugin to the CoreWave Marketplace:

  1. Package as a ZIP following the plugin structure
  2. Include screenshots, description, and version in manifest.json
  3. Submit via the admin panel under Storefront → Marketplace → Submit Plugin
  4. 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.

Empty defaults: every widget option starts as "", 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.

PanelPurposeSaved Data
WidgetsDrag default platform widgets and runtime theme widgets into rows/columns.Block entries inside the page builder rows.
NavigatorSelect rows, columns, and widgets from a structure tree.No extra data; it controls editor selection only.
ContentEdit widget-specific fields. Theme widgets render fields from manifest.json.Built-ins save fields on the block. Theme widgets save values in settings.
StyleEdit colors, spacing, sizing, borders, radius, shadow, and opacity.visualConfig.
AdvancedEdit CSS class, anchor ID, and responsive visibility.visualConfig.cssClass, visualConfig.anchorId, responsiveConfig.

Widget Groups

GroupDefault WidgetsNotes
Layoutcontainer, inner-sectionWrappers for sections and nested layouts.
Atomic Elementse-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-elementLow-level elements for theme authors who want smaller building blocks. custom-element renders any HTML5 element with optional child widgets, styling, and inline CSS.
Basicheading, image, text-editor, video, button, divider, spacer, google_maps, iconCommon content widgets.
Generalsf-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-htmlReusable content and interaction widgets. sf-custom-html is the unrestricted Custom HTML & CSS widget.
Sitesf-page-header, sf-blogs-list, sf-blog-posts-listStorefront page and blog widgets.
Singlesf-blog-postSingle post/content route widget.
Commercesf-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-documentsStorefront product, checkout, delivery, and customer account widgets.
CoreWave360 Storefrontsf-heading, sf-checkout, sf-category-tabs, sf-subcategory-tabs, sf-pagination, sf-searchCore storefront controls used by the built-in storefront builder.
{themeName} WidgetsWidgets 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.

ObjectAllowed KeysBehavior
visualConfigmarginTop, 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, deviceOverridesStyle and advanced inspector data. Empty values are not emitted as inline CSS.
responsiveConfighideDesktop, hideTablet, hideMobileControls responsive visibility in preview and render output.
settingsTheme-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.
fieldSchemaTheme 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, valueReusable list object used by tabs, accordion, gallery, carousel, icon list, social icons, and loop preview data.

Default Widget Reference

Widget KeyGroupAllowed Content Keys / Objects.cw Alias
containerLayoutdirection, gap, emptyText, children[], common objects.@widget('core.container')
inner-sectionLayoutdirection, gap, emptyText, children[], common objects.@widget('core.container')
flexboxLayoutchildren[], class, html, content, common objects.@widget('core.container')
e-div-blockAtomic Elementstag, direction, gap, emptyText, children[], common objects.@widget('e-div-block')
e-flexboxAtomic Elementsdirection, gap, alignItems, justifyContent, emptyText, children[], common objects.@widget('e-flexbox')
e-tabsAtomic Elementsitems[], activeIndex, common objects. Each item may include label and body.@widget('core.tabs')
e-tabs-menuAtomic Elementsitems[] with label, target, href, common objects.Theme/render helper only.
e-tabAtomic Elementslabel, target, common objects.Theme/render helper only.
e-tabs-content-areaAtomic Elementsitems[] with label, body, common objects.Theme/render helper only.
e-tab-contentAtomic Elementslabel, body, common objects.Theme/render helper only.
e-headingAtomic Elementstitle, subtitle, tag, common objects.@widget('e-heading')
e-paragraphAtomic Elementstext, html, emptyText, common objects.@widget('e-paragraph')
e-imageAtomic Elementssrc, alt, href, caption, imageFit, imageHeight, openInNewTab, common objects.@widget('e-image')
e-svgAtomic Elementssvg, src, alt, common objects.@widget('e-svg')
e-buttonAtomic Elementslabel, href, variant, size, align, fullWidth, openInNewTab, common objects.@widget('e-button')
e-youtubeAtomic Elementssrc, title, poster, autoplay, controls, common objects.@widget('e-youtube')
e-dividerAtomic Elementsthickness, color, width, align, common objects.@widget('e-divider')
e-self-hosted-videoAtomic Elementssrc, title, poster, autoplay, controls, common objects.@widget('e-self-hosted-video')
custom-elementAtomic Elementstag, 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')
headingBasictitle, subtitle, tag, common objects.@widget('core.heading')
imageBasicsrc, alt, href, caption, imageFit, imageHeight, openInNewTab, common objects.@widget('core.image')
text-editorBasictext, html, emptyText, common objects.@widget('core.text-editor')
videoBasicsrc, title, poster, autoplay, controls, common objects.@widget('core.video')
buttonBasiclabel, href, variant, size, align, fullWidth, openInNewTab, common objects.@widget('core.button')
dividerBasicthickness, color, width, align, common objects.@widget('core.divider')
spacerBasicheight, common objects.@widget('core.spacer')
google_mapsBasicembedUrl, title, height, common objects.@widget('core.map')
iconBasicicon, label, href, common objects.@widget('core.icon')
sf-icon-boxGeneralicon, title, body, variant, common objects.Visual editor block.
sf-testimonialGeneralquote, author, role, common objects.Visual editor block.
sf-progress-barGenerallabel, value, max, tone, common objects.Visual editor block.
sf-pricing-tableGeneraltitle, price, billingPeriod, features[], ctaLabel, ctaHref, highlighted, common objects.Visual editor block.
sf-accordionGeneralitems[], allowMultiple, common objects. Each item supports title and body.@widget('core.accordion')
sf-tabsGeneralitems[], activeIndex, common objects. Each item supports label and body.@widget('core.tabs')
sf-galleryGeneralitems[], columns, imageHeight, common objects. Each item supports src, alt, caption, href.Visual editor block.
sf-carouselGeneralitems[], autoRotate, intervalMs, common objects. Each item supports src, title, caption, href.Visual editor block.
sf-countdownGenerallabel, targetAt, completedText, common objects.Visual editor block.
link-listGeneralitems[], 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-listGeneralitems[], common objects. Each item supports icon, label, text, href, and optional class.@widget('core.icon-list')
social-iconsGeneralitems[], common objects. Each item supports icon, label, href.@widget('core.social-icons')
alertGeneraltitle, message, tone, common objects.@widget('core.alert')
htmlGeneralhtml, css, htmlClass, allowThemeScripts, common objects.@widget('core.html')
sf-loopGeneralqueryKey, titleField, metaField, imageField, priceField, ctaLabel, maxItems, emptyText, previewItems[], common objects.Visual editor block.
sf-page-headerSiteshowBackPrimary, common objects.Visual editor block.
sf-blogs-listSitesourceMode, blogHandles[], enablePagination, itemsPerPage, emptyText, common objects.Visual editor block.
sf-blog-posts-listSitesourceMode, blogHandles[], postHandles[], enablePagination, itemsPerPage, emptyText, common objects.Visual editor block.
sf-blog-postSingleshowTags, emptyText, common objects.Visual editor block.
sf-product-gridCommercesourceMode, categoryNames[], productIds[], brandNames[], inStockOnly, maxItems, sortBy, enablePagination, itemsPerPage, showCategoryArchiveLinks, columnsDesktop, columnsMobile, gap, cardRadius, imageFit, common objects.Visual editor block.
sf-cart-pageCommercetitle, emptyText, showCheckoutButton, checkoutLabel, common objects.Visual editor block.
sf-checkout-pageCommercetitle, submitLabel, common objects.Visual editor block.
checkout-delivery-optionsCommercetitle, 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-optionsCommercetitle, 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-panelCommercetitle, showTabs, emptyText, common objects.Visual editor block.
sf-account-dashboardCommercetitle, common objects.Visual editor block.
sf-account-ordersCommercetitle, emptyText, common objects.Visual editor block.
sf-account-order-detailCommercetitle, common objects.Visual editor block.
sf-account-wishlistCommercetitle, emptyText, common objects.Visual editor block.
sf-account-returnsCommercetitle, emptyText, common objects.Visual editor block.
sf-account-profileCommercetitle, common objects.Visual editor block.
sf-account-addressesCommercetitle, common objects.Visual editor block.
sf-account-invoicesCommercetitle, emptyText, common objects.Visual editor block.
sf-account-documentsCommercetitle, emptyText, common objects.Visual editor block.
sf-headingCoreWave360 Storefronttitle, subtitle, common objects.Visual editor block.
sf-checkoutCoreWave360 Storefrontlabel, common objects.Visual editor block.
sf-category-tabsCoreWave360 StorefrontlinkToArchives, common objects.Visual editor block.
sf-subcategory-tabsCoreWave360 StorefrontCommon objects only.Visual editor block.
sf-paginationCoreWave360 StorefrontshowSummary, common objects.Visual editor block.
sf-searchCoreWave360 Storefrontplaceholder, common objects.Visual editor block.
sf-custom-htmlGeneralhtml, css, htmlClass, allowThemeScripts, common objects.@widget('core.html')
theme-widget:{widgetKey}{themeName} WidgetswidgetKey, 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.

FieldTypeBehavior
htmlhtmlRaw HTML fragment rendered in place. Relative asset URLs are resolved through the active theme asset resolver where available.
csscssRaw CSS emitted as a page-local <style data-corewave-custom-css> tag. Leave empty to emit nothing.
htmlClasstextOptional class added to the rendered widget wrapper.
allowThemeScriptsbooleanAllows trusted theme HTML/forms through the visual editor renderer. Prefer theme asset scripts and platform widgets for JavaScript-heavy behavior.
JSON
{
  "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.

CW
@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.

CW
@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.

Supported parent widgets: 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:

KeyTypeDescription
keystringThe widget key (e.g. heading, button, core.image). System widget keys are stored without the core. prefix.
instanceIdstringUnique instance identifier used for media picker targeting and internal references.
settingsobjectKey-value map of the child widget's field values. Keys correspond to the widget's field definitions.
JSON
{
  "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):

CW
@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:

CW
{{-- 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:

CW
@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.

CW
{{-- 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"
      }
    }
  }
}
--}}
Priority order: Visual-editor saved settings > @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.

Use cases: Semantic HTML layouts (<section>, <article>, <nav>), styled wrapper elements, flex containers with background images, custom grid structures, and element-level inline CSS without editing theme stylesheets.

Field Reference

FieldTypeDescription
childrennested-widgetsNested child widgets embedded inside this custom element. Supports the same add/reorder/edit interface as container and flexbox.
tagselectThe 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, h1h6. Defaults to div when unset.
elementIdtextOptional id attribute for the HTML element. Useful for anchor links and JavaScript targeting.
classtextOptional CSS class name(s) applied to the element. Supports multiple space-separated classes.
backgroundImageimageBackground image URL, selected from the Media Library or entered manually. Rendered as background-image:url(...) in the element's inline style.
widthtextCSS width (e.g. 100%, 500px, auto).
maxWidthtextCSS max-width (e.g. 1200px, 100%).
minHeighttextCSS min-height (e.g. 400px).
heighttextCSS height (e.g. 300px, auto).
colortextCSS text color (e.g. #333, red).
backgroundColortextCSS background color (e.g. #fff, transparent).
textAlignselectText alignment: left, center, right, justify.
objectFitselectHow content fits within the element: contain, cover, fill, none, scale-down.
displayselectCSS display: block, inline, inline-block, flex, grid, none.
directionselectFlex direction (when display: flex): row, column, row-reverse, column-reverse.
alignItemsselectCSS align-items: flex-start, center, flex-end, stretch, baseline.
justifyContentselectCSS justify-content: flex-start, center, flex-end, space-between, space-around, space-evenly.
gaptextGap between flex/grid children (e.g. 16px, 1rem).
margintextCSS margin shorthand (e.g. 10px 20px).
paddingtextCSS padding shorthand (e.g. 15px).
bordertextCSS border shorthand (e.g. 1px solid #ccc).
borderRadiustextCSS border-radius (e.g. 8px, 50%).
boxShadowtextCSS box-shadow (e.g. 0 2px 4px rgba(0,0,0,0.1)).
opacitytextCSS 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:

CW
{{-- 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:

HTML
<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.

wrapperElement Wraps the entire widget output in the specified HTML tag. Only safe, common HTML tags are allowed. When omitted, no wrapper is added and the widget renders its native output directly.

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.
wrapperClass Shorthand alias for setting the CSS class on the widget's root element. It behaves identically to 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.
CW
{{-- 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.

JSON + CW
{
  "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.

JSON + CW
{
  "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:

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

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

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

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

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

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

Widget Directory Structure

Text
my-theme-v3.0.0.zip
├── manifest.json
├── 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.

HTML
{-- 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:

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

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

JSON
"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" }
    }
  }
}
CW
{{-- 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

{{-- Embed a heading widget --}} @widget('core.heading', [ 'title' => __title, 'tag' => 'h2' ]) @if(__body)

{ __body }

@endif {{-- Embed a button widget --}} @widget('core.button', [ 'label' => 'Shop Now', 'href' => '/collections/all', 'variant' => 'primary' ]) {{-- Embed a theme widget that itself has a .cw template with @widget() --}} @widget('trust-badges', [ 'layout' => 'row' ])

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.

Visual Editor — Widget Editor Panel
┌─────────────────────────────────────────────────┐
│  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('core.heading', [ │ │ │ │ │ │ 'title' => __title, │ │ │ │ │ │ 'tag' => 'h2' │ │ │ │ │ │ ]) │ │ │ │ │ │ ... │ │ │ │ │ └────────────────────────────────────────┘ │ │ │ │ [Save Widget Template] │ │ │ └─────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────┘
Override priority: Saved widget template overrides take priority over the packaged template source. To revert to the original, use the Revert Override button in the visual editor, which clears the saved template override.

Widget Configuration Schema Fields

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

Field Type Reference

TypeDescriptionDefault Value
textSingle-line text input""
textareaMulti-line text input""
html / richtextHTML-capable content field for trusted store-owner editable copy""
code / cssCode editor field. Use css for custom CSS snippets and code for generic source-like text.""
numberNumeric input0
range / sliderNumeric slider control for bounded values such as opacity, gap, width, or speed0
boolean / checkbox / toggleYes/No togglefalse
select / dropdownDropdown 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, citiesFirst option or ""
multi-select / multiselect / listMultiple 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 / arrayRepeatable 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 / colourColor picker#000000
image / mediaMedia library image picker. The editor shows a picker button and saves the selected asset URL/value.""
linkURL/link picker""
Widget style overrides: Every default widget and theme widget can expose normal content fields, and the visual editor also provides a collapsed style override group for wrapper element, wrapper class, background image, background color, text color, alignment, spacing, border, and responsive overrides. Empty override values are ignored and are not emitted into inline styles.

Repeater Field Example

JSON
"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": "" }
      }
    }
  }
}
CW
@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 @query inside widgets to fetch their own data rather than relying on parent context
  • Prefix widget template files with a descriptive name to avoid conflicts
  • Include a widgets/ directory in your theme package even if initially empty — it signals widget support
  • Test widgets with both named and area usage to ensure they render correctly in both modes

Select / Multi-Select Field Types V2

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

ModePropertyDescription
Static options Array of {value, label} objects — choices are fixed and known at theme-authoring time. See example below.
Dynamic source String referencing a platform data source (e.g. "source": "products"). 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):

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

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

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

Usage in templates (multi-select): The stored value is an array. Iterate it directly, or use membership checks against that array:

CODE
{-- 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:

JSON
{
  "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"
    }
  }
}
CODE
{-- Fetch select options from a data source --}
@code
  __category_opts = cw_get_select_options(['source' => 'categories', 'limit' => 100]);
  __product_opts  = cw_get_select_options(['source' => 'products', 'limit' => 50, 'order' => 'asc']);
  __blog_opts     = cw_get_select_options(['source' => 'blogs']);
  __country_opts  = cw_get_select_options(['source' => 'countries', 'orderBy' => 'name']);
  __tag_opts      = cw_get_select_options(['source' => 'tags', 'scope' => 'products']);
  __discount_opts = cw_get_select_options(['source' => 'discounts']);
  __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

SourceReturnsAvailable Parameters
productsProduct options (id / title)limit, orderBy, order, ids
categoriesCategory options (slug / title)limit, orderBy, order
collectionsCollection options (slug / title)limit, orderBy
navigations, navigation, menus, cw_get_navigationsNavigation/menu options (id / name by default)limit, valueField, labelField
blogsBlog options (slug / title)
postsBlog post options (id / title)limit, orderBy, order, blog, tag
tagsTag options (slug / name)scope (products, posts, or both), limit
discountsDiscount options (code / title)limit
countriesCountry options (code / name)limit, orderBy
regionsRegion options (code / name)limit, orderBy, parentId (country ID)
citiesCity options (id / name)limit, orderBy, parentId (region ID)
Select / Multi-Select summary: Use options (array of {value, label} objects) when choices are fixed and known at theme-authoring time. Use source 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.

JSON
{
  "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

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

First-Response SSR Endpoint

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

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

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

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

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

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

Starter Content #

The starterContent section in manifest.json defines 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.

Important: Template-key entries such as "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.

JSON
{
  "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 KeyWrites ToNotes
appearanceDefaults.themeappearanceJson.themePrimary, background, surface, text, muted, and border color defaults.
appearanceDefaults.typographyappearanceJson.typographyGoogle Font family defaults for body, header, navigation, titles, H1-H6, buttons, and prices.
appearanceDefaults.layout.header.stickyappearanceJson.layout.header.stickyDefault sticky-header preference. Logo position is ignored because theme partials own logo layout.
cmsSettingsDefaults.permalink.productSearchBasecmsSettingsJson.permalink.productSearchBaseExposed to templates as cw_base_paths().product_search.
cmsSettingsDefaults.permalink.blogSearchBasecmsSettingsJson.permalink.blogSearchBaseExposed to templates as cw_base_paths().blog_search.
cmsSettingsDefaults.permalink.searchBasecmsSettingsJson.permalink.searchBaseExposed 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.

JSON
"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 }
}
FieldTypeDescription
defaultHomeBooleanMarks this template key as the storefront home template. It does not create a CMS page.
defaultProductDetailsBooleanMarks this template key as the default single product template. Merchants can later change it in Storefront → Overview.
defaultBlogPageBooleanMarks this template key as the default blog archive/listing template.
defaultBlogPostBooleanMarks this template key as the default single blog post template.
defaultNotFoundBooleanMarks this template key as the default 404/not-found template. Aliases: defaultNotFoundPage, default404, default404Page.
defaultMaintenanceBooleanMarks this template key as the default maintenance/coming-soon template. Aliases: defaultMaintenancePage, defaultComingSoon.
menusObjectNamed navigation menus to seed on installation. Each key is the menu slug (e.g. main, footer, secondary-nav) and the value is an array of Menu Item objects (see Menu Seeding below)
pagesArrayArray of Page objects to seed as CMS pages on installation (see Page Seeding below)

Page Seeding

Themes can seed any number of CMS pages 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.

JSON
"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
    }
  ]
}
FieldTypeDescription
titleStringPage title displayed in the browser tab and admin UI
handleStringURL slug for the page (e.g. "about-us" → /about-us)
templateKeyString?Template key to render this page (e.g. "page.default")
pageHeaderKeyString?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.
pageFooterKeyString?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.
showHeaderBoolean?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.
showFooterBoolean?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.
__noneString sentinelOptional 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.
previewImageUrlString?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.
contentHtmlString?Optional raw HTML content body for the page (seeded as ContentJson)
defaultHomeBoolean?Optional shortcut to also assign this page's templateKey as the default home template.
defaultBlogPageBoolean?Optional shortcut to also assign this page's templateKey as the default blog archive template.
sortOrderIntDisplay order for navigation sorting (lower = first)
publishBooleanWhether the page is published immediately (true) or created as draft (false)

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

JSON
"starterContent": {
  "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 }
    ]
  }
}
FieldTypeDescription
labelStringDisplay text for the menu link
pageHandleString?Handle of a seeded page to link to (e.g. "about-us")
urlString?Custom URL override (e.g. "/shop", "https://example.com"). 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.
sortOrderIntDisplay order (lower = first)
childrenArray?Nested child items (same structure), enabling dropdown/submenu navigation. Supports arbitrary depth
Note: Menu items are only seeded if the menu does not already exist for the storefront. If a menu item with the same label already exists, its sort order and URL may be updated, but merchant edits are preserved. The children array supports multilevel nesting for dropdown menus.
Tip: Set at least one starter template to "Published" — otherwise the storefront will show empty content after theme installation.

Object Property References #

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

Product Object

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

PropertyTypeDescription
__product.idintProduct ID
__product.titlestringProduct name / title
__product.skustringStock keeping unit
__product.descriptionstringFull description (HTML)
__product.short_descriptionstringTruncated description (200 chars)
__product.barcodestringBarcode / UPC
__product.pricedecimalCurrent storefront price after sale discount, when applicable.
__product.regular_pricedecimalUndiscounted admin unit price.
__product.original_pricedecimalAlias of regular_price for themes that render original / was pricing.
__product.sale_pricedecimal?Discounted sale price when on_sale and discount_percent apply.
__product.compare_pricedecimal?Compare-at / original price to show with strikethrough. Falls back to regular_price when a discount creates a lower sale price.
__product.discount_percentdecimal?Discount percent (0-100) applied when on_sale is true
__product.cost_pricedecimalCost price (internal use)
__product.stock_quantityintCurrent stock count
__product.in_stockbooltrue when stock_quantity > 0
__product.on_saleboolSale status (currently false)
__product.brandstringBrand name
__product.manufacturerstringManufacturer name
__product.imagestringPrimary thumbnail URL
__product.thumbnailstringPrimary thumbnail URL (same as image)
__product.imagesarrayArray of Image objects {url, alt, width, height}. The primary thumbnail is included first when available.
__product.categoriesarrayArray of {id, name, slug} objects
__product.tagsarrayArray of {id, name, slug} objects
__product.attributesarrayArray of product attribute objects {name, options}. Options expose {value, unit_price, cost_price, stock_quantity}.
__product.created_atdatetimeCreation timestamp
__product.updated_atdatetimeLast 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 TypeConditionExample
saleProduct has a discount or compare-at price higher than unit priceSale
newIsNewArrival = true and NewArrivalEndDate has not passed (if set)New
out-of-stockStockQuantity <= 0Out of Stock
featuredProduct is in a "featured" collectionFeatured
collectionProduct is in any other collectionCollection 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:

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

Category Object

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

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

Blog Object

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

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

Blog Post Object

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

PropertyTypeDescription
__post.idintPost ID
__post.titlestringPost title
__post.slugstringURL slug (post handle)
__post.excerptstringPost excerpt / summary
__post.contentstringFull post content (JSON/HTML)
__post.content_jsonstringRaw content JSON
__post.cover_imagestring?Cover image URL (may be null)
__post.imagestring?Alias for cover_image
__post.authorstring?Post author name (may be null)
__post.published_atdatetimePublish timestamp
__post.statusstringPost status (Published/Draft/etc)
__post.blog_idintPrimary blog ID (first assigned blog)
__post.blog_idsarray<int>Array of all assigned blog IDs (supports multi-blog posts)
__post.blog_slugstringPrimary blog slug/handle for URL construction
__post.urlstringRelative URL (blog_slug/post_slug). Prefix with __paths.blog for full URL.
__post.blogsarrayArray of {id, name, slug} objects for all blogs the post belongs to
__post.categoriesarrayArray of {id, name, slug} objects
__post.tagsarrayArray of {id, name, slug} objects
__post.created_atdatetimeCreation timestamp
__post.updated_atdatetimeLast update timestamp

Store Object

Returned by cw_get_store(). See the canonical Store Object table above for sample values and full access examples.

PropertyTypeDescription
__store.idintStorefront ID
__store.namestringStore / business name
__store.slugstringStore URL handle
__store.taglinestring?Store tagline from appearance.branding.siteTagline
__store.logostring?Default logo URL from appearance.branding.logoDefault
__store.logo_urlstring?Default logo URL alias
__store.logo_default / __store.logoDefaultstring?Default logo URL from appearance.branding.logoDefault
__store.logo_light / __store.logoLightstring?Logo intended for light backgrounds from appearance.branding.logoLight
__store.logo_dark / __store.logoDarkstring?Logo intended for dark backgrounds from appearance.branding.logoDark
__store.faviconstring?Favicon URL from appearance.branding.favicon
__store.favicon_urlstring?Favicon URL (alias of favicon)
__store.primary_colorstring?Brand primary color (hex)
__store.secondary_colorstring?Brand secondary color (hex)
__store.currencystringCurrency code (currently "NGN")
__store.currency_codestringCurrency code (alias of currency)
__store.current_currency_codestringShopper's preferred currency code, falls back to base currency
__store.current_currency_symbolstringShopper's preferred currency symbol
__store.current_currency_namestringShopper's preferred currency name
__store.current_currency_is_baseboolWhether the shopper's preferred currency is the store's base currency
__store.languagestringLanguage code (currently "en")
__store.language_codestringLanguage code (alias of language)
__store.timezonestringTimezone (currently "Africa/Lagos")
__store.emailstring?Contact email from CMS settings
__store.phonestring?Contact phone from CMS settings
__store.addressstring?Contact address from CMS settings
__store.appearanceobjectFull Appearance object. Same shape as cw_get_appearance().
__store.cmsSettings / __store.cms_settingsobjectFull 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):

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

Menu Item Object

Returned inside __nav.items arrays:

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

Discount Code Object

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

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

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.

Pattern: Always guard against unauthenticated access with if (__customer && __customer.error) { __customer = null; } or use null-coalescing: { __customer.first_name ?? '-' }.
PropertyTypeDescription
__customer.idintCustomer ID
__customer.namestringBusiness / display name
__customer.first_namestringCustomer first name
__customer.last_namestringCustomer last name
__customer.other_namesstringOther / middle names
__customer.emailstringEmail address
__customer.phonestringPhone number
__customer.billing_addressstringBilling address (free text)
__customer.shipping_addressstringShipping address (free text)
__customer.zip_codestringZIP / postal code
__customer.profile_picture_urlstring?Profile picture URL (null if not set)
__customer.is_verifiedboolWhether email is verified
__customer.email_verified_atdatetime?Verification timestamp (null if unverified)
__customer.country_idint?Country ID
__customer.country_namestring?Country name (resolved server-side)
__customer.region_idint?State / Region ID
__customer.state_namestring?State / Region name (resolved server-side)
__customer.city_idint?City / LGA ID
__customer.city_namestring?City / LGA name (resolved server-side)
__customer.total_ordersintSummary: Total number of orders
__customer.ordersintAlias of total_orders
__customer.paid_ordersintNumber of paid orders
__customer.total_spenddecimalTotal lifetime spend
__customer.currencystringCurrency code (e.g. "USD", "NGN")
__customer.latest_order_atdatetime?Most recent order timestamp
__customer.addressesarrayArray 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_addressstringDefault billing address from Customer entity (BillingAddress column)
__customer.default_shipping_addressstring?Default shipping address from Customer entity (ShippingAddress column, nullable)

Tag Object

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

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

Template Assignments #

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

Route KindTemplate Key PatternExample
Home / LandingAny template marked with defaultHome: true, or a key containing homehome.default, theme.home
Catalog / Categorycatalogue.defaultcatalogue.default
Product Detailproduct.detailproduct.detail
Cartcart.defaultcart.default
Checkoutcheckout.defaultcheckout.default
Blog Indexblogs.defaultblogs.default
Blog Postpost.defaultpost.default
CMS Pagepage.defaultpage.default
404page.not-foundpage.not-found
Loginlogin.defaultlogin.default
Registerregister.defaultregister.default
Account Dashboardaccount.default or account.{section}account.dashboard, account.orders

Storefront Appearance Settings V2 #

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

Branding Assets

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

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

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.

CW
@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.

Important: Storefront → Appearance is a settings source, not a forced design layer. If a theme ignores __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 GroupJSON PathAvailable KeysTemplate Access
Theme ColorsappearanceJson.themeprimary, background, surface, text, muted, border__appearance.theme.primary
Typography / Google FontsappearanceJson.typographybody, header, navigation, title, h1â€Ļh6, button, price__appearance.typography.body
Header LayoutappearanceJson.layout.headersticky__appearance.layout.header.sticky
HTML
@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

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.

CW
@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 --}}
  ],
])
Recommended pattern: Fetch Appearance in the outer template or shared header partial, define CSS variables once, then use CSS classes throughout the theme. This keeps templates clean and prevents repeated calls in every card or loop item.

Reading Settings

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

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

Discussion Settings

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

Default Post Settings

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

Other Comment Settings

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

Email Me Whenever

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

Before a Comment Appears

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

Avatars

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

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

Image Sizes

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

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

Privacy

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

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

Data Storage

The Appearance page saves structured settings to JSON columns and optional snippet fields on the StorefrontSettings record:

ColumnContentsAPI Field
AppearanceJsonbranding, theme, layout, typographyappearanceJson
CmsSettingsJsonreading, discussion, avatars, media, permalink, privacycmsSettingsJson
CustomCssMerchant CSS snippet. Not auto-injected; fetched with cw_get_custom_css() or @cw_custom_css.customCss
HeaderHtmlMerchant header HTML snippet. Not auto-injected; fetched with cw_get_header_html() or @cw_header_html.headerHtml
FooterHtmlMerchant 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 PathTypeDescription
defaultTemplateKeys.productDetailstringDefault active .cw template key for product detail routes when the product has no item-level TemplateKey.
defaultTemplateKeys.blogArchivestringDefault active .cw template key for single blog archive routes when the blog has no item-level TemplateKey.
defaultTemplateKeys.blogPoststringDefault active .cw template key for blog post routes when the post has no item-level TemplateKey.
defaultTemplateKeys.maintenancestringDefault active .cw template key for maintenance mode when no specific maintenance CMS page/template is selected.
defaultTemplateKeys.notFoundstringDefault active .cw template key for unresolved storefront routes when no specific 404 CMS page/template is selected.
defaultPageIds.notFoundnumberOptional 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.titleFormatstringFallback document title format selected in Storefront → Overview. Supported tokens are {{ page_title }} and {{ shop_name }}.
seoDefaults.defaultMetaDescriptionstringFallback meta description for storefront routes that do not have page, blog post, or item-specific SEO descriptions.
JSON
{
  "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

TokenDescriptionExample 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
TEXT
{{ page_title }} - {{ shop_name }}
{{ page_title }} | {{ shop_name }}
{{ page_title }}
{{ shop_name }} - {{ page_title }}
Note: When saving, the frontend strips any homepageMode key from the reading settings to prevent stale data. If you need to set a homepage mode, configure it through the dedicated Storefront Pages panel instead.

Auth-Protected Templates & Page Link V2 #

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

is_auth_protected Flag

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

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

page_link Property

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

Managing Auth Protection & Page Link in the Admin

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

Runtime Data

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

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

Account & Auth Templates V2 #

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

Login Page (login.default)

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

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

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

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

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

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

    <form method="POST" action="{ cw_route('login.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:

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

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

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

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

    <form method="POST" action="{ cw_route('register.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.

CW
{-- 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.

CW
<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:

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

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

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

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

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

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.

CODE
{-- 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:

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

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

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

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}.

HTML
{{-- 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:

FunctionReturnsDescription
cw_customer() / cw_user()objectPrimary 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()objectReturns nested structure {customer: {...}, summary: {...}} for the active session. Prefer cw_customer() for flat property access. Returns {error} when not authenticated.
cw_get_customer_summary()objectReturns {totalOrders, paidOrders, totalSpend, currency, latestOrderAt} for the active session.
cw_get_customer_addresses()arrayReturns saved address rows with legacy billing/shipping fallback entries when the customer has no structured rows.
cw_resend_verification_email(email)objectResends the email verification email. Accepts optional on_success and on_failure callbacks.
cw_user_logged_in()booleanReturns true if a customer session is active
cw_get_customer_orders(page, limit)objectReturns paginated orders for the current customer. Accepts named params: page, limit, sort_by, sort_dir
cw_get_customer_order(order_id?) / cw_get_current_order() / cw_get_order_details()object or nullReturns 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)objectReturns paginated invoices (orders with invoice) for the current customer
cw_get_customer_receipts(page, limit)objectReturns paginated receipts (paid orders) for the current customer
cw_get_customer_documents(order_id)arrayReturns documents for a specific order, or all documents if no order_id given
cw_get_customer_wishlist()arrayReturns wishlist items for the current customer
cw_add_to_customer_wishlist(product_id)objectAdds a product to the customer's wishlist
cw_remove_from_customer_wishlist(product_id)voidRemoves a product from the customer's wishlist
cw_get_customer_returns(page, limit)objectReturns paginated return requests for the current customer
cw_get_customer_return(return_id?) / cw_get_current_return() / cw_get_return_details()object or nullReturns 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)objectCreates a return request for an order. product_id is optional
cw_get_customer_reviews(page, limit)objectReturns paginated product reviews written by the customer
cw_get_customer_dashboard()objectReturns dashboard summary with wishlist, returns, reviews, and spend chart data
cw_update_customer_profile(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?)objectUpdates 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?)objectAuthenticates 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()objectReturns 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?)objectVerifies the OTP code and establishes a customer session. Returns redirectUrl on success.
cw_logout_user()voidClears 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?)objectRegisters 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?)objectVerifies a registration verification token. Returns verified: true on success.
cw_count(var)intReturns count of collection items, or the numeric value for integers/decimals. Returns 0 for null.
cw_format_money(amount, currency)stringFormats a monetary value with currency symbol (e.g. $19.99)
cw_route(name, params)stringGenerates 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.
Authentication Check: Use @if(cw_user_logged_in()) to conditionally render account content. If the customer is not logged in, redirect them to the login page using the cw_route('login') function for the login URL or display a login prompt.

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

CW
@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:

CategoryProperties
Identityid, name, first_name, last_name, other_names
Contactemail, phone
Addressbilling_address, shipping_address, zip_code, default_billing_address, default_shipping_address, addresses[]
Verificationis_verified, email_verified_at
Locationcountry_id, country_name, region_id, state_name, city_id, city_name
Summarytotal_orders, orders, paid_orders, total_spend, currency, latest_order_at
Mediaprofile_picture_url
Legacy Note: 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.

CW
@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

Text
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
Tip: You can override any account section template by placing a .cw file with the matching template key in the templates/account/ directory. If a template is missing, the runtime falls back to the default account UI built into the storefront.

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

PrioritySourceExample
1Assigned template (Storefront → Pages)Admin sets verifyEmail assignment to my-verification
2Fallback defaultverify-email.default

Available Template Variables

VariableTypeDescription
__request.tokenstringThe verification token from the URL query string
__requestobjectAll query string parameters as a key-value map

Template Example

CW
{{-- 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

  1. Customer registers → system checks Storefront Settings to determine if verification is required
  2. If required, a verification email is sent with a link to /verify-email?token=<token>
  3. Customer clicks the link → storefront resolves route kind "verify-email"
  4. Storefront loads verify-email.default template (or admin-assigned template)
  5. Template reads __request.token and calls cw_verify_register(token: __token)
  6. On success: customer is logged in and redirected
  7. On failure: template shows the error message
  8. If customer tries to login before verifying, the system auto-resends the verification email
Note: The 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:

ScenarioRedirect URLHow 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.

FieldRequiredWhat it means
nameYesThe shopper's name.
messageYesThe message body.
email or phoneOne requiredHow the store can reply.
redirect_urlNoWhere the shopper returns after submit. The backend adds contact_success or contact_error.

Session Flash Variables Reference

Session VariableTypeDescription
__session.login_errorstring|nullError message set by failed login (HTML form POST)
__session.register_errorstring|nullError message set by failed registration (HTML form POST)
__session.reset_sentbool|nullTrue after successful forgot-password request
__session.reset_successbool|nullTrue after successful password reset
__session.customer_emailstring|nullEmail of the currently logged-in customer
__session.customer_idstring|nullID of the currently logged-in customer
__session.customer_tokenstring|nullJWT session token for the current customer
__session.auth_redirect_urlstring|nullPost-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

  1. Theme registration / checkout: When a customer signs up or checks out on your storefront, their firstName, lastName, email, and phone automatically populate the contact person fields on the customer record.
  2. Company name: The name field from registration forms maps to the customer's business/display name.
  3. 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:

PlaceholderDescription
[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:

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) #

Fixed in current version: Prior versions had a bug where @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

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

Full Working Theme Checklist

A production-ready CoreWave360 theme should pass the full checklist below before marketplace submission or customer installation.

AreaDeveloper Requirement
PackageZIP contains manifest.json, templates/, partials/, sections/, widgets/, and assets/. No design.json, legacy JSON route templates, or generated build trash are included.
ManifestformatVersion is 3; every template key points to a real .cw file; headerPresets, footerPresets, defaultHeaderPresetKey, and defaultFooterPresetKey are valid.
Required PagesTheme 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 ContentstarterContent 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 & FootersLayouts call @cw_header() and @cw_footer(). Pages that should not show chrome use showHeader: false / showFooter: false or the __none preset sentinel.
Editable WidgetsEach 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 FieldsFields 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.
DataProducts, 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.
AppearanceTheme 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.
AssetsTheme 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.
JavaScriptTheme 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.
FormsNative 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 QADesktop, tablet, and mobile previews match the source design with no horizontal overflow, clipped text, broken sliders, hidden menus, or overlapping cart/search/account panels.
ValidationRun 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

  1. Download the extension: package.vsix
  2. Open VS Code, go to Extensions (Ctrl+Shift+X or Cmd+Shift+X)
  3. Click the ... menu → Install from VSIX...
  4. Select the downloaded package.vsix file
  5. 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:

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.

FunctionEditor SupportRuntime Purpose
cw_customer()Completion, hover, diagnosticsRecommended. Returns flat profile + summary at root level. Access via __customer.email, __customer.first_name, etc.
cw_user()Completion, hover, diagnosticsAlias of cw_customer() for theme compatibility.
cw_get_customer_profile()Completion, hover, diagnosticsLegacy nested accessor: {customer: {...}, summary: {...}}. Use cw_customer() for new templates.
cw_get_customer_summary()Completion, hover, diagnosticsAuthenticated order and spend summary.
cw_get_customer_addresses()Completion, hover, diagnosticsAuthenticated 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:

  1. In the CoreWave360 admin panel, go to Storefront → Appearance → Theme Developer App Access
  2. Generate a developer API key (it starts with cwdk_)
  3. In VS Code, open Settings (Ctrl+, or Cmd+,) and set:
    • corewave.storefrontName — your storefront handle or name
    • corewave.apiKey — the API key you generated
    • corewave.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.