Commera

Commera

Open in ChatGPT
Ask ChatGPT about this page
Open in Claude
Ask Claude about this page

Step 3: Add pages

In this step, you add the Acme home page and product page. Each page is a template, plus an optional Python controller that gets the data.

What you'll build

  • A home page with a hero and the eight newest products
  • A product card macro that the home page uses
  • A product page with sizes and an Add to cart button

How a file becomes a URL

The Themed Routes table in Shop Theme Settings maps each URL to a file in pages/. Commera fills it when you install it:

URL File
/en, /ar pages/index.html
/en/products pages/products/list.html
/en/products/<route> pages/products/details.html
/en/cart pages/cart/cart.html

The full table is in Routes and pages. Acme has only two pages. For every other URL, Commera looks in Shop Base Theme, which has no pages, and then serves its core page.

1. Add the home page controller

Create acme_theme/themes/acme/pages/index.py:

from commera.utils import get_product_list
from commera.www import index


def get_context(context):
	index.get_context(context)
	context.acme_tagline = "Everyday goods, honestly made."
	context.latest_products = get_product_list(page_length=8, sort_by="new_arrival")
	return context

The controller first calls Commera's own home page controller, commera.www.index.get_context. That call sets the SEO data and the home page settings. Then it adds the two values that the Acme template uses. sort_by="new_arrival" puts the newest products first. Without it, the list is in name order.

commera.www.index, commera.www.products.details and commera.utils.get_product_list are Commera's own page code, not a fixed API. Check your controllers after each Commera update.

A controller has these rules:

  • Its name is the page's name with .py, in the same folder as the page.
  • It defines get_context(context). It can change context, or return a dict that Commera adds to the context.
  • It uses absolute imports only, such as from commera.www import index.

2. Add a product card macro

Create acme_theme/themes/acme/components/macros/product_card.html:

{% macro product_card(product) %}
<a class="acme-card" href="/{{ frappe.lang or 'en' }}/products/{{ product.route }}">
	<img src="{{ product.image or '/assets/commera/themes/shop_base_theme/images/placeholder.svg' }}" alt="{{ product.item_name }}" loading="lazy">
	<span class="acme-card-title">{{ product.item_name }}</span>
	<span class="acme-price">{{ get_currency_symbol() }}{{ frappe.utils.fmt_money(product.sale_price or 0, precision=2) }}</span>
</a>
{% endmacro %}

Shop Base Theme ships placeholder.svg for products without an image.

3. Add the home page template

Create acme_theme/themes/acme/pages/index.html:

{% extends "components/theme_layout.html" %}
{% from "components/macros/product_card.html" import product_card %}

{% block body %}
<section class="acme-hero">
	<h1>{{ _("Welcome to {0}").format(app_name) }}</h1>
	<p>{{ acme_tagline }}</p>
	<a class="acme-button" href="/{{ frappe.lang or 'en' }}/products">{{ _("Shop all products") }}</a>
</section>

<section class="acme-section">
	<h2>{{ _("Latest products") }}</h2>
	<div class="acme-grid">
		{% for product in latest_products %}
			{{ product_card(product) }}
		{% endfor %}
	</div>
</section>
{% endblock %}

A page extends your layout and fills the body block. If a page adds to the head block, call {{ super() }} first, or the page loses the layout's stylesheets.

4. Add the product page

The product page uses Commera's product controller as it is. Create acme_theme/themes/acme/pages/products/details.py:

from commera.www.products import details


def get_context(context):
	details.get_context(context)
	return context

Then create acme_theme/themes/acme/pages/products/details.html:

{% extends "components/theme_layout.html" %}

{% block body %}
<section class="acme-product" x-data='{
		product: JSON.parse({{ product_variant.as_json() | tojson }}),
		selected_variant: {{ (json.dumps(selected_item) if size_selected else "") | tojson }},
		sizes: {{ available_sizes | tojson }},
		price: {{ (selected_price or 0) | tojson }},
		default_price: {{ (default_price or 0) | tojson }},
		brand: {{ (product.brand or "") | tojson }},
		select_size(size) {
			const url = new URL(window.location.href);
			url.searchParams.set("size", size);
			window.location.assign(url.toString());
		},
		add_to_cart() {
			if (!this.selected_variant) {
				toast($store.i18n.t("Please select a size first!"), { type: "warning" });
				return;
			}
			$store.cart.add(this.product, this.selected_variant, this.price, this.default_price, this.brand, this.sizes, 1);
			toast($store.i18n.t("Item added to your bag"), { type: "success" });
		}
	}'>
	<img class="acme-product-image" src="{{ (images or [None])[0] or '/assets/commera/themes/shop_base_theme/images/placeholder.svg' }}" alt="{{ product_variant.display_name }}">

	<div class="acme-product-info">
		<h1>{{ product_variant.display_name }}</h1>
		<p class="acme-price">{{ get_currency_symbol() }}{{ frappe.utils.fmt_money(selected_price or 0, precision=2) }}</p>

		{% if available_sizes | length > 1 %}
		<div class="acme-sizes">
			{% for size in available_sizes %}
			<button type="button" @click='select_size({{ size.size | tojson }})'
				class="acme-size{% if size_selected and selected_size == size.size %} is-selected{% endif %}">{{ size.size }}</button>
			{% endfor %}
		</div>
		{% endif %}

		<button type="button" class="acme-button" @click="add_to_cart()" {% if not product_in_stock %}disabled{% endif %}>
			{{ _("Add to cart") if product_in_stock else _("Out of stock") }}
		</button>
	</div>
</section>
{% endblock %}

The product controller reads the product from the URL and the size from ?size=. It sets product_variant, selected_item, available_sizes, selected_price, default_price and product_in_stock. A size change reloads the page, so the controller can find the price and stock of that size.

$store.cart.add() and toast() come from Shop Base Theme. $store.cart.add() parses the variant as JSON and throws an error on an empty value. The check in add_to_cart() stops that and shows a message.

Single-quote the x-data attribute. The tojson filter writes double quotes, and they would end a double-quoted attribute early.

Check it works

Open /en?preview_theme=Acme and /en/products/<a product route>?preview_theme=Acme. The pages have no styles yet. You add them in the next step. Preview and ship explains the preview parameter.

Changes to .html files show when you reload the page. Changes to a .py controller also show on reload, because Commera loads the file again when it changes.

Next steps

Last updated 5 days ago
Was this helpful?
Thanks!