Single-page application (SPA) architecture for Varsity Market storefront themes. Each theme is a single interface file (no extension) that renders as a full multi-page e-commerce website powered by hash-based routing.
A theme is one HTML file deployed to /themes/{theme-name}/interface. When a customer visits a store, the system loads that file through skel/interface.php, which:
- Reads the
interfacefile from the active theme directory - Extracts
e(__CONSTANT__)placeholders and generates aconfig.phpwithdefine()calls - Replaces placeholders with PHP echo statements in a cached
encode.php - Serves the processed file to the browser
The theme file must be completely self-contained — all HTML, CSS, JavaScript, templates, and mock data live in a single file.
| Constant | Source | Example |
|---|---|---|
__SITE_TITLE__ |
Store name from admin | "OASIS Studios" |
__SYSTEM_CURRENCY__ |
Currency sign from settings | "R", "$", "\u20AC" |
__SYSTEM_API__ |
API endpoint path | "api.php" |
__SYSTEM_JS_API__ |
JS API client path | "vm.api.js" |
__SYSTEM_JS_THEME__ |
Theme engine path | "vm.theme.js" |
__SYSTEM_JS_CONNECT__ |
Connect script path | "vm.connect.js" |
__SITE_LOGO__ |
Store logo URL | "https://..." |
__SHOP_INTRO__ |
Hero headline text | "Premium Goods" |
__SHOP_PARAGRAPH__ |
Hero subtext | "Quality delivered." |
__SHOP_DESCRIPTION__ |
Store meta description | "..." |
__DESIGN_COLOR_PRIMARY__ |
Brand accent color | "#7c3aed" |
__DESIGN_COLOR_BACKGROUND__ |
Background color | "#ffffff" |
__DESIGN_COLOR_TEXT__ |
Text color | "#111111" |
__DESIGN_COLOR_SURFACE__ |
Card/surface color | "#f9fafb" |
Custom constants can be added via autofill.json in the theme directory.
Base URL: api.php (relative to the deployed site)
| Endpoint | Params | Returns |
|---|---|---|
?state=products |
— | [{id, name, price, image, description, stock, category_id}] |
?state=product |
id |
{id, name, price, image, description, stock, category_id} |
?state=categories |
— | [{id, name}] |
?state=products_by_category |
category_id |
[{id, name, price, image, ...}] |
?state=search |
q |
[{id, name, price, image, ...}] |
?state=discounts |
— | [{id, code, percentage, active}] |
?state=site |
— | {name, currency, domain} |
?state=orders |
email |
[{id, customer_name, total_amount, items, status, created_at}] |
| Endpoint | Body | Returns |
|---|---|---|
?state=order |
{name, email, total, items} |
{success, message} |
API keys passed via X-API-Key header, Authorization: Bearer {key}, or ?api_key={key} query parameter. The local api.php within the deployed site does not require authentication.
Every theme must implement these views as <section> elements with id="view-{name}" and class view-section. The router shows/hides sections based on the URL hash.
#home -> Home / Landing Page
#shop -> All Products (catalog grid)
#product?id=x -> Product Detail Page
#collections -> Collections / Categories
#sale -> Sale / Discounted Items
#cart -> Shopping Cart
#checkout -> Multi-step Checkout
#search -> Search Results
#about -> About Us / Our Story
#contact -> Contact Us
#faq -> FAQ / Help Center
#track -> Order Tracking
#terms -> Terms & Conditions
#orders -> Order History Lookup
https://store.example.com/#home
https://store.example.com/#shop
https://store.example.com/#product?id=42
https://store.example.com/#collections
https://store.example.com/#sale
https://store.example.com/#cart
https://store.example.com/#checkout
https://store.example.com/#search?q=sneakers
https://store.example.com/#about
https://store.example.com/#contact
https://store.example.com/#faq
https://store.example.com/#track
https://store.example.com/#terms
https://store.example.com/#orders?email=user@mail.com
The landing page. First impression — must be visually striking and conversion-focused.
Required Sections:
- Announcement bar — top strip with promo text (e.g. "Free shipping on orders over R500")
- Hero banner — large image or gradient with headline
e(__SHOP_INTRO__), subtexte(__SHOP_PARAGRAPH__), and CTA button linking to#shop - Featured products — 4-8 product cards from
?state=products(first page) or mock data - Collection highlights — 2-3 category cards from
?state=categorieslinking to#collections - Social proof / trust badges — shipping, returns, secure payment icons
- Newsletter signup — email input (stores locally or posts to contact endpoint)
Data Source: ?state=products (limited to 8), ?state=categories, ?state=site
Full product catalog with filtering, sorting, and pagination.
Required Elements:
- Page title with product count (e.g. "All Products (24)")
- Category sidebar/filter — populated from
?state=categories, clickable to filter via?state=products_by_category - Sort dropdown — Featured, Price Low-High, Price High-Low, Newest
- Product grid — responsive (1 col mobile, 2 col tablet, 3-4 col desktop)
- Product cards — image, name, price (formatted with currency), category label
- Empty state — "No products found" message when catalog is empty
Data Source: ?state=products, ?state=categories, ?state=products_by_category
Single product page for purchase decisions.
Required Elements:
- Breadcrumb — Home > Shop > {Product Name}
- Product image gallery — main image with zoom capability
- Product info — name, price (with currency
e(__SYSTEM_CURRENCY__)), description - Stock indicator — "In Stock" / "Low Stock" / "Out of Stock" based on
stockfield - Quantity selector — +/- controls, minimum 1, maximum = stock
- Add to Cart button — calls
cart.addItem(), shows toast confirmation - Related products — 4 products from the same category via
?state=products_by_category - Back to shop link
Data Source: ?state=product&id={id}, ?state=products_by_category&category_id={category_id}
Browse by category.
Required Elements:
- Category grid — each card shows category name and product count
- Category image — use first product image from that category or a placeholder
- Clicking a category navigates to
#shopwith filter applied, or a#collection?id=xsub-view
Data Source: ?state=categories, ?state=products_by_category
Discounted products and active promotions.
Required Elements:
- Active discount codes — from
?state=discounts, displayed as badges or banner cards - Discount percentage display — e.g. "UP TO 30% OFF"
- Sale product grid — products associated with active sales (or full catalog with discount badges)
- Discount code copy button — click-to-copy the code with visual feedback
Data Source: ?state=discounts, ?state=products
Shopping cart review before checkout.
Required Elements:
- Cart item list — product image, name, unit price, quantity controls (+/-), line total, remove button
- Cart summary — subtotal, estimated shipping, discount applied, total
- Discount code input — text field to apply a promo code from
?state=discounts - Empty cart state — illustration + "Your cart is empty" + CTA to shop
- Checkout button — navigates to
#checkout - Continue shopping link — navigates to
#shop
Data Source: localStorage cart, ?state=discounts (for code validation)
Multi-step checkout flow.
Step 1 — Information:
- Email address
- Full name
- Shipping address (street, city, province/state, postal code)
Step 2 — Shipping:
- Shipping method selection (populated from delivery rates if available)
- Delivery estimate display
Step 3 — Payment:
- Payment method selection (COD indicator, card form placeholder)
- Order review summary
Step 4 — Confirmation:
- Order success message
- Order number display
- "Continue Shopping" button
- Cart cleared on success
Required Elements:
- Step indicator — breadcrumb showing Information > Shipping > Payment
- Order summary sidebar — item list, subtotal, shipping, total
- Back/Continue navigation per step
- Form validation — required fields highlighted on submit
- Place Order button on final step — calls
POST ?state=order
Data Source: localStorage cart, POST ?state=order
Search results page.
Required Elements:
- Search input — pre-filled with query, supports live typing with debounce (300ms)
- Result count — "X results for '{query}'"
- Product grid — same card layout as shop
- No results state — "No products match '{query}'" with suggestion to browse shop
- Search overlay (optional) — modal/overlay triggered from header search icon
Data Source: ?state=search&q={query}
Store brand story.
Required Elements:
- Hero section — brand headline and mission statement
- Story section — text + image grid about the brand
- Values / features — 3-4 value props (quality, sustainability, etc.)
- Team or founder section (optional)
Data Source: Static content, e(__SHOP_DESCRIPTION__) for store description
Customer support and inquiries.
Required Elements:
- Contact form — name, email, order ID (optional), message, submit button
- Contact info — email address, physical address, phone
- Business hours (optional)
- Form submit handler — shows success toast, resets form
Data Source: Static content, form submission stored locally or POSTed
Common questions.
Required Elements:
- Accordion list — minimum 6 questions with expand/collapse
- Categories: Shipping, Returns, Payments, Orders, General
- Search filter (optional) — filter FAQ items by keyword
Suggested Questions:
- What is your shipping policy?
- How do I return or exchange an item?
- What payment methods do you accept?
- How do I track my order?
- Do you ship internationally?
- How do I apply a discount code?
- What is your refund policy?
- How do I contact customer support?
Order status lookup.
Required Elements:
- Order ID input — text field for order reference
- Track button — simulates or queries order status
- Status display — order found/not found, status steps (Confirmed > Processing > Shipped > Delivered)
- Timeline visualization — step dots or progress bar
Data Source: ?state=orders&email={email} or simulated tracking
Legal pages.
Required Elements:
- Terms of Service text
- Privacy Policy section
- Refund/Return Policy section
- Clean typography, readable layout
Customer order lookup.
Required Elements:
- Email input — to look up past orders
- Order list — table or cards showing order ID, date, total, status
- Order detail expand — item breakdown per order
- Empty state — "No orders found for this email"
Data Source: ?state=orders&email={email}
Every theme must include a fallback mock dataset that renders when the API is unreachable. This ensures the theme displays correctly in preview mode, during development, and when deployed without API connectivity.
const MOCK = {
site: {
name: "THEME_NAME Store",
currency: "$",
domain: window.location.hostname
},
categories: [
{ id: 1, name: "Apparel" },
{ id: 2, name: "Accessories" },
{ id: 3, name: "Footwear" },
{ id: 4, name: "Home & Living" }
],
products: [
{
id: 1,
name: "Essential Cotton Tee",
price: 45.00,
image: "https://images.unsplash.com/photo-1521572267360-ee0c2909d518?w=600",
description: "Premium heavyweight cotton with a relaxed fit.",
stock: 24,
category_id: 1
},
{
id: 2,
name: "Classic Denim Jacket",
price: 185.00,
image: "https://images.unsplash.com/photo-1576995853123-5a10305d93c0?w=600",
description: "Rigid selvedge denim with a structured silhouette.",
stock: 8,
category_id: 1
},
{
id: 3,
name: "Leather Crossbody Bag",
price: 120.00,
image: "https://images.unsplash.com/photo-1548036328-c9fa89d128fa?w=600",
description: "Full-grain vegetable-tanned leather with brass hardware.",
stock: 15,
category_id: 2
},
{
id: 4,
name: "Minimalist Canvas Sneakers",
price: 95.00,
image: "https://images.unsplash.com/photo-1549298916-b41d501d3772?w=600",
description: "Vulcanized rubber sole with organic cotton upper.",
stock: 32,
category_id: 3
},
{
id: 5,
name: "Wool Blend Overcoat",
price: 320.00,
image: "https://images.unsplash.com/photo-1539533018447-63fcce2678e3?w=600",
description: "Italian wool-cashmere blend with a tailored drop shoulder.",
stock: 5,
category_id: 1
},
{
id: 6,
name: "Stoneware Ceramic Vase",
price: 68.00,
image: "https://images.unsplash.com/photo-1612196808214-b8e1d6145a8c?w=600",
description: "Hand-thrown stoneware with a matte glaze finish.",
stock: 18,
category_id: 4
},
{
id: 7,
name: "Titanium Frame Sunglasses",
price: 210.00,
image: "https://images.unsplash.com/photo-1572635196237-14b3f281503f?w=600",
description: "Ultra-lightweight Japanese titanium with polarized lenses.",
stock: 12,
category_id: 2
},
{
id: 8,
name: "Linen Relaxed Trousers",
price: 110.00,
image: "https://images.unsplash.com/photo-1594938298603-c8148c4dae35?w=600",
description: "Belgian linen with an elastic waistband and tapered leg.",
stock: 20,
category_id: 1
}
],
discounts: [
{ id: 1, code: "WELCOME10", percentage: 10, active: 1 },
{ id: 2, code: "SUMMER20", percentage: 20, active: 1 }
]
};Every theme must use this pattern to fetch data with mock fallback:
async function fetchStore(state, params = {}) {
const url = new URL("api.php", window.location.href);
url.searchParams.set("state", state);
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
try {
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
if (!res.ok) throw new Error(res.status);
return await res.json();
} catch (e) {
console.warn(`API offline, using mock data for: ${state}`);
return getMockData(state, params);
}
}
function getMockData(state, params) {
switch (state) {
case "site": return MOCK.site;
case "products": return MOCK.products;
case "product": return MOCK.products.find(p => p.id == params.id) || null;
case "categories": return MOCK.categories;
case "products_by_category":
return MOCK.products.filter(p => p.category_id == params.category_id);
case "search":
const q = (params.q || "").toLowerCase();
return MOCK.products.filter(p =>
p.name.toLowerCase().includes(q) || p.description.toLowerCase().includes(q)
);
case "discounts": return MOCK.discounts;
case "orders": return [];
default: return [];
}
}All prices must be formatted using the store's configured currency symbol. The currency is loaded from:
e(__SYSTEM_CURRENCY__)— PHP constant injected at build time?state=siteAPI response —{currency: "R"}- Fallback:
"$"
function formatPrice(amount, currency = null) {
const sym = currency || window.StoreConfig?.currency || "$";
return `${sym}${parseFloat(amount).toFixed(2)}`;
}Usage in templates: formatPrice(product.price) outputs R185.00 or $185.00.
The router manages view switching via URL hash. All navigation uses #page or #page?key=value format.
const router = {
init() {
window.addEventListener("hashchange", () => this.resolve());
window.addEventListener("load", () => this.resolve());
},
navigate(view, params = {}) {
let hash = `#${view}`;
const qs = new URLSearchParams(params).toString();
if (qs) hash += `?${qs}`;
window.location.hash = hash;
},
resolve() {
const raw = window.location.hash.slice(1) || "home";
const [view, qs] = raw.split("?");
const params = Object.fromEntries(new URLSearchParams(qs || ""));
// Hide all views, show target
document.querySelectorAll(".view-section").forEach(s => s.classList.remove("active"));
const target = document.getElementById(`view-${view}`);
if (target) {
target.classList.add("active");
} else {
document.getElementById("view-home")?.classList.add("active");
}
// Trigger page-specific rendering
this.onRoute(view, params);
window.scrollTo({ top: 0, behavior: "instant" });
},
onRoute(view, params) {
// Override per theme — load data and render dynamic content
}
};Since this is a hash-based SPA, traditional crawling is limited. Apply these techniques:
<title>tag — updatedocument.titleon every route change to reflect the current page<meta name="description">— update dynamically per page- Open Graph tags — set default OG image, title, description in the
<head>usinge()constants - Semantic HTML — use proper
<header>,<main>,<footer>,<nav>,<article>,<section>tags - Structured data — inject
<script type="application/ld+json">for Product and Organization schemas - Accessible navigation — use
<a href="#shop">links (not just onclick) so hash URLs are crawlable - Sitemap hint — include a
<noscript>fallback that lists all page links as plain HTML
<head>
<title>e(__SITE_TITLE__)</title>
<meta name="description" content="e(__SHOP_DESCRIPTION__)">
<meta property="og:title" content="e(__SITE_TITLE__)">
<meta property="og:description" content="e(__SHOP_DESCRIPTION__)">
<meta property="og:image" content="e(__SITE_LOGO__)">
<link rel="icon" href="e(__SITE_LOGO__)">
</head>Uses localStorage for persistence. Cart state is shared across all views.
const cart = {
items: JSON.parse(localStorage.getItem("vm_cart") || "[]"),
save() {
localStorage.setItem("vm_cart", JSON.stringify(this.items));
this.updateBadge();
window.dispatchEvent(new CustomEvent("cart-updated"));
},
add(product, qty = 1) {
const existing = this.items.find(i => i.id === product.id);
if (existing) existing.qty += qty;
else this.items.push({ ...product, qty });
this.save();
},
update(productId, qty) {
const item = this.items.find(i => i.id === productId);
if (!item) return;
item.qty = Math.max(0, qty);
if (item.qty === 0) this.remove(productId);
else this.save();
},
remove(productId) {
this.items = this.items.filter(i => i.id !== productId);
this.save();
},
clear() { this.items = []; this.save(); },
subtotal() { return this.items.reduce((s, i) => s + i.price * i.qty, 0); },
count() { return this.items.reduce((s, i) => s + i.qty, 0); },
applyDiscount(code, discounts) {
const d = discounts.find(d => d.code === code && d.active);
if (!d) return null;
return { code: d.code, percentage: d.percentage, amount: this.subtotal() * (d.percentage / 100) };
},
updateBadge() {
document.querySelectorAll("[data-cart-count]").forEach(el => {
const c = this.count();
el.textContent = c;
el.style.display = c > 0 ? "" : "none";
});
}
};Every theme directory contains:
themes/{theme-name}/
interface # The single-page HTML file (required)
poster.png # Theme preview thumbnail 800x600 (required)
autofill.json # Default constant values (optional)
.version_hash # Auto-generated version tracking (system)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>e(__SITE_TITLE__)</title>
<meta name="description" content="e(__SHOP_DESCRIPTION__)">
<meta property="og:title" content="e(__SITE_TITLE__)">
<meta property="og:description" content="e(__SHOP_DESCRIPTION__)">
<meta property="og:image" content="e(__SITE_LOGO__)">
<link rel="icon" href="e(__SITE_LOGO__)">
<!-- Tailwind CSS (CDN) -->
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<!-- Icons (Lucide, Bootstrap Icons, or Heroicons) -->
<style>
/* Theme-specific styles */
body { font-family: 'Inter', sans-serif; }
.view-section { display: none; }
.view-section.active { display: block; }
</style>
</head>
<body>
<!-- ====== ANNOUNCEMENT BAR ====== -->
<div id="announcement-bar">...</div>
<!-- ====== HEADER / NAVIGATION ====== -->
<header>
<nav>
<a href="#home">e(__SITE_TITLE__)</a>
<a href="#shop">Shop</a>
<a href="#collections">Collections</a>
<a href="#sale">Sale</a>
<a href="#about">About</a>
<a href="#contact">Contact</a>
<button id="search-toggle">Search</button>
<a href="#cart">Cart (<span data-cart-count>0</span>)</a>
</nav>
</header>
<!-- ====== MAIN CONTENT ====== -->
<main>
<section id="view-home" class="view-section active"><!-- Home --></section>
<section id="view-shop" class="view-section"><!-- Shop --></section>
<section id="view-product" class="view-section"><!-- Product Detail --></section>
<section id="view-collections" class="view-section"><!-- Collections --></section>
<section id="view-sale" class="view-section"><!-- Sale --></section>
<section id="view-cart" class="view-section"><!-- Cart --></section>
<section id="view-checkout" class="view-section"><!-- Checkout --></section>
<section id="view-search" class="view-section"><!-- Search Results --></section>
<section id="view-about" class="view-section"><!-- About Us --></section>
<section id="view-contact" class="view-section"><!-- Contact --></section>
<section id="view-faq" class="view-section"><!-- FAQ --></section>
<section id="view-track" class="view-section"><!-- Order Tracking --></section>
<section id="view-terms" class="view-section"><!-- Terms --></section>
<section id="view-orders" class="view-section"><!-- Order History --></section>
</main>
<!-- ====== CART DRAWER (slide-out) ====== -->
<div id="cart-drawer">...</div>
<!-- ====== SEARCH OVERLAY ====== -->
<div id="search-overlay">...</div>
<!-- ====== TOAST NOTIFICATIONS ====== -->
<div id="toast">...</div>
<!-- ====== FOOTER ====== -->
<footer>
<div><!-- Shop links --></div>
<div><!-- Company links --></div>
<div><!-- Support links --></div>
<div><!-- Newsletter signup --></div>
<p>© 2026 e(__SITE_TITLE__). All rights reserved.</p>
</footer>
<script>
// === MOCK DATA ===
const MOCK = { /* ... full mock dataset ... */ };
// === API WRAPPER ===
async function fetchStore(state, params) { /* ... with mock fallback ... */ }
function getMockData(state, params) { /* ... */ }
// === CURRENCY ===
function formatPrice(amount) { /* ... */ }
// === CART ===
const cart = { /* ... localStorage cart system ... */ };
// === ROUTER ===
const router = { /* ... hash-based SPA router ... */ };
// === PAGE RENDERERS ===
// Each view has a render function called by router.onRoute()
// === INIT ===
window.addEventListener("DOMContentLoaded", () => {
router.init();
cart.updateBadge();
});
</script>
</body>
</html>These components must be present in every theme, persistent across all views:
- Logo linking to
#home - Nav links: Shop, Collections, Sale, About, Contact
- Search icon (toggles search overlay)
- Cart icon with item count badge
- Mobile hamburger menu
- Slide-out panel from right side
- Shows cart items with quantity controls
- Subtotal display
- "Checkout" button navigating to
#checkout - Overlay backdrop that closes drawer on click
- Full-screen or large modal overlay
- Auto-focus text input
- Live search results with debounce (300ms)
- Product result cards linking to
#product?id=x - Close button / ESC key handler
- Fixed position (bottom-right or bottom-center)
- Auto-dismiss after 3 seconds
- Types: success (green), error (red), info (neutral)
- Used for: add to cart, order placed, form submitted, errors
- 4-column grid: Shop, Company, Support, Newsletter
- Link to all pages: Shop, About, Contact, FAQ, Terms, Track Order
- Copyright with
e(__SITE_TITLE__) - Social media icons (optional)
Themes should follow Shopify-level quality standards:
- Use Google Fonts:
Inter,Plus Jakarta Sans,DM Sansfor body;Playfair Display,Frauncesfor headings - Body text: 14-16px, line-height 1.6
- Headings: clear hierarchy (h1 > h2 > h3)
- Section padding:
py-16topy-24 - Grid gaps:
gap-4togap-8 - Content max-width:
max-w-7xl(1280px)
- Use
e(__DESIGN_COLOR_*)constants for brand colors - Maintain sufficient contrast (WCAG AA minimum)
- Neutral palette for surfaces and text
- Mobile: 0-640px (1 column grid, stacked layout)
- Tablet: 641-1024px (2 column grid)
- Desktop: 1025px+ (3-4 column grid, sidebar layouts)
- Hover states on all clickable elements
- Smooth transitions (200-300ms)
- Loading skeletons for async content
- Focus states for accessibility
Before publishing a theme:
- All 14 view sections implemented and navigable
- Mock data renders correctly when API is offline
- API data loads and replaces mock data when online
- Cart add/remove/update works across all views
- Checkout flow completes and places order via API
- Currency displays correctly using store configuration
- All
e(__CONSTANT__)placeholders are used where applicable - Mobile responsive at all breakpoints
- Search returns results and handles empty state
- Discount codes can be applied in cart
-
poster.pngpreview image is included (800x600) -
autofill.jsonhas sensible defaults for all custom constants - Page title updates on every route change
- No console errors in production
- Toast notifications work for all user actions