Shopmorphic theme guide

This guide is for two readers: a shop owner, and the AI assistant they paste it into (Claude, ChatGPT, Gemini, Grok, or any other). Everything a theme can use is listed here. Nothing else exists, so do not invent variables.

What a theme is

A folder, zipped for upload, containing:

theme.json          name, version, author, description
templates/          Jinja2 templates (list below)
assets/             CSS, images, fonts - served at /theme/<file>

theme.json:

{ "name": "Sea Glass", "version": "1.0.0", "author": "Your name", "description": "Calm and coastal." }

Templates use plain Jinja2 in a sandbox. Any template you leave out falls back to the built-in Default theme's version, so a theme can start as one file (layout.html plus assets/theme.css) and grow.

Templates: layout.html, home.html, product.html, collection.html, collections.html, search.html, cart.html, checkout.html, order.html, page.html, blog.html, post.html, faq.html, 404.html.

Optional pages a theme may override (the Default theme's versions are used when a theme leaves them out): privacy.html, terms.html (the shop's legal pages, same variables as page.html), and my_orders.html (the buyer's own orders, see below).

Every page template should start with {% extends "layout.html" %} and fill {% block content %}. Link the stylesheet with <link rel="stylesheet" href="{{ urls.theme_asset }}theme.css">.

Rules a theme must follow

  1. Show product.shipping.text and shop.tax_note on the product page. Hiding delivery cost is the biggest reason people abandon a basket; the platform rejects a theme that leaves them out.
  2. Keep the platform's forms as they are: the add-to-basket form posts to {{ urls.cart }}/add, the checkout form posts to itself. Every POST form needs <input type="hidden" name="csrf_token" value="{{ csrf_token }}">.
  3. Work without JavaScript. Add JavaScript for polish if you like.
  4. Load nothing from other sites unless the shop owner asked for it. Fonts and images belong in assets/.
  5. Print {{ json_ld|json_ld }} and {{ page.breadcrumbs|breadcrumb_ld }} in <head> so search engines and AI assistants can read the shop.
  6. Use {{ shop.custom_head|safe }} in <head> so the owner's extra tags work.

Filters

Variables on every page

shop - name, slug, currency, url (https://slug.shopmorphic.com) - tagline, contact_email, address (multi-line), logo_url (may be empty) - accent - one of: yellow amber orange deep_orange red pink purple deep_purple indigo blue light_blue cyan teal green light_green lime brown grey blue_grey - social.instagram, social.facebook, social.tiktok (URLs or empty) - nav - list of {title, url} (published collections, then Blog if on) - blog_enabled, newsletter_enabled, rewards_enabled (booleans) - shipping_note - the owner's one-line delivery promise - tax_note - "Prices include VAT." or "VAT at 20% is added at checkout." or empty - faq_title, custom_head

cart - count, subtotal (integer minor units), subtotal_text - lines - list of {key, title, variant_title, qty, unit_text, line_total_text, answers: [{label, value}], url, in_stock, image: {thumb_url, alt} or null} - country, country_name - where the visitor asked for delivery (the shop's own country until they pick one), can_ship (boolean) - delivery - list of {id, label, price, price_text, days_min, days_max}, the delivery options for that country, cheapest first; empty when the shop does not deliver there

currency - code - the currency prices are showing in, base - the shop's own - options - list of {code, name} the shop offers, switchable (boolean: more than one), url - post cur (and optional next) there to switch. All *_text values and cart amounts are already in currency.code; bare integer amounts such as product.price stay in the shop's own currency.

page - title, description, canonical - breadcrumbs - list of {title, url}, first is Home

urls - home, cart, checkout, search, collections, faq, blog, contact, privacy, terms, my_orders, theme_asset. Link privacy, terms and my_orders from the footer: buyers have a legal right to find them.

platform - {name, url}. now_year. csrf_token. json_ld (dict or none).

Product card (in lists)

products / collection.products / c.products are lists of: - id, title, slug, url - price (minor units), price_text, price_from (true when variants differ) - compare_at_price, compare_at_price_text, on_sale - in_stock, is_bundle - image - {url, thumb_url, alt, width, height} or null

Product page

product has everything a card has, plus: - description_html (safe to print with |safe), sku - images - list of {url, thumb_url, alt, width, height} - options - list of {name, values} - variants - list of {id, title, option_values, price, price_text, compare_at_price_text, in_stock, sku, image} - custom_fields - list of {id, label, kind (text|textarea|select), choices, required, max_length, price_delta, price_delta_text}; post each as cf_<id> - bundle_items - list of {title, quantity, url} - shipping - {text, label, price, price_text, free_over, free_over_text, days_min, days_max, country, country_name} or null; the estimate is for the country remembered from the basket page (text says where) - seo_title, seo_description - collections - list of {title, url}

The add-to-basket form posts product_id, variant_id (when there are variants), qty, and cf_<id> fields.

Collections

collections - list of {id, title, slug, url, description_html, products}. collection on a collection page has the same keys.

Search

query (string) and products.

Basket page

Uses cart and countries (list of (code, name)). Quantity form posts key and qty to {{ urls.cart }}/update; a button named action with value remove removes the line. A form posting country to {{ urls.cart }}/country remembers the delivery country; show cart.delivery so the cost is known before checkout.

Checkout page

Show one submit button per entry in gateway_choices, named pay with the entry's id as its value. A button named apply re-prices with the discount code. A checkbox use_points spends points.

Order page

order - {number, status (pending|paid|cancelled|refunded), name, email, lines (as cart lines), subtotal_text, discount, discount_text, shipping_text, tax, tax_label, tax_text, total_text, address (multi-line), points}

My orders page (my_orders.html, optional)

Buyers have no accounts. They type their email, get a one-time link, and the link signs this browser in for that address.

Legal pages (privacy.html, terms.html, optional)

content - {title, body_html}, the shop's privacy policy or terms of sale, which the owner edits in the dashboard.

Blog, posts, pages, FAQ

Uploading

Zip the folder, then in the dashboard go to Design and upload it. The platform checks the manifest, renders every template with sample data, and rejects the upload with a plain message if something is missing.