eekaam.docs

Themes

Building a theme


Building a theme

An Eekaam theme is a directory of Liquid, JSON, CSS and JavaScript that the platform renders on the server for every storefront request. This page is the whole loop: scaffold a theme, work on it against a dev store with hot reload, check it, package it and submit it. The middle section explains how a request becomes a rendered page, which is where most Shopify habits need adjusting.

The file layout, size limits and upload rules are on Theme requirements. Every CLI command and flag is on Eekaam CLI.

1. Log in and pick a dev store

Theme commands run against an active dev store. Dev stores are free, come with a sample catalogue, and are what the dev server previews against.

The active store is saved in ~/.eekaam/config.json. The eekaam.toml file that theme init writes is not read by theme dev, and theme dev has no --store flag even though theme init suggests one. Switch stores with eekaam store use.

npm install -g @eekaam/cli
eekaam auth login
eekaam store create "Theme sandbox"
eekaam store list
eekaam store use <store-id>

2. Scaffold

theme init downloads the current Loom theme, which is the same code as the reference theme, into a new directory and sets name in theme.json. If the download fails it falls back to a smaller built-in starter. Either way you start from a theme that renders. Change it rather than writing a layout from nothing.

eekaam theme init my-theme
cd my-theme

3. Run the dev server

In order, theme dev:

  1. Refuses to start unless layout/theme.liquid exists.
  2. Uploads the theme's .liquid, .json, .css, .js, image and .woff/.woff2 files to the active dev store.
  3. Serves the store through a local proxy at http://127.0.0.1:9292 and opens it.
  4. Watches the directory. On save it uploads the file, clears the store's template cache and signals the browser.

A changed stylesheet is swapped in place without a reload. Any other change reloads the page. Deleting a local file deletes it on the store too, unless you pass --no-delete. Paths matched by .eekaamignore are skipped.

The preview uses the dev store's real products, markets and settings. Test the awkward states on purpose: an empty cart, a sold-out variant, a product with no images.

eekaam theme dev
eekaam theme dev --port 3100 --no-open

4. How a request becomes a page

Routes and templates

Each storefront route renders one template from templates/, into the layout.

RouteTemplaterequest.page_type
/indexindex
/products/:handleproductproduct
/products, /collections/:handle, /collections/allcollectioncollection
/collectionscollectionslist-collections
/cartcartcart
/search?q=searchsearch
/blogblogblog
/blog/:handlearticlearticle
/pages/:handlepagepage
/contactcontactpage
/account/login, /account/register, /accountaccount-login, account-register, accountSame as the template
/terms-and-conditions, /privacy-policy, /refund-policylegalpage
Anything not found404404

JSON templates always render through a Liquid template

This is the biggest difference from Shopify. On Eekaam, templates/<name>.liquid always renders. A JSON template does not replace it. The JSON file supplies the page's sections: their order, settings and blocks. The Liquid template decides where they go by looping over section_list.

For a product page the sections are chosen in this order:

  1. templates/product.<suffix>.json, when the product has a template suffix set
  2. templates/product.json
  3. Neither exists: section_list holds only what the store has saved, which for a new theme is nothing

templates/product.liquid renders in every case. A suffix chooses different sections, never a different Liquid file. Pages (/pages/:handle) resolve suffixes the same way.

section_list leaves out disabled sections and any section whose type is header, footer or announcement-bar. The layout renders those.

The JSON file is a page's starting layout. Once a merchant edits the page in the theme editor, the store's saved layout is what renders. If a local change to a JSON template doesn't show in theme dev, check whether that page has been saved in the editor on your dev store.

{
  "sections": {
    "main": { "type": "product-main", "settings": { "show_sku": true } },
    "related": { "type": "product-recommendations", "settings": { "heading": "You may also like" } }
  },
  "order": ["main", "related"]
}

The layout

Every template renders into layout/theme.liquid as content_for_layout. A theme has exactly one layout. {% layout %} is accepted but has no effect, so neither alternative layouts nor {% layout none %} work. The mandatory objects are listed under The layout file.

Put the header and footer in the layout. Use {% sections 'header-group' %} when the merchant should edit them, or {% section 'header' %} for a fixed include. Sections and schema covers both.

<!doctype html>
<html lang="{{ request.locale.iso_code }}">
  <head>
    <title>{{ page_title }} – {{ shop.name }}</title>
    {{ 'base.css' | asset_url | stylesheet_tag }}
    {{ content_for_header }}
  </head>
  <body>
    {% sections 'header-group' %}
    <main>{{ content_for_layout }}</main>
    {% sections 'footer-group' %}
    {{ 'theme.js' | asset_url | script_tag }}
  </body>
</html>

Sections

{% render_section section %} loads sections/<section.type>.liquid with section bound to that entry. If the section file is missing or fails to parse, the section renders nothing and the error goes to the server log, not the page. Check the preview after adding a section type.

Snippets

{% render 'product-card', product: product %} looks for snippets/product-card.liquid, then sections/product-card.liquid. Unlike Shopify, the snippet also sees every variable of the template that rendered it, and the arguments you pass override them. Pass what a snippet needs anyway, because it keeps the snippet reusable.

Do not use {% include %}. It reads from the server's filesystem rather than from your theme, so on a store it fails.

5. Check

SeverityFinding
Errorlayout/theme.liquid, templates/index.liquid or config/settings_schema.json is missing
ErrorA .json file, including theme.json, is not valid JSON
ErrorA link to an external CDN such as cdnjs or jsDelivr (Google Fonts and GSAP are allowed)
ErrorSomething that looks like a hard-coded API key, secret or password
ErrorAn image over 2 MB, or the theme over 10 MB in total
Warningtemplates/product, collection, cart, 404 or search .liquid, sections/header.liquid, sections/footer.liquid or locales/en.json is missing
WarningA Liquid file where the counts of {% and %} differ
Warningtheme.json is missing, or has no name or version

theme check is a quick local pass, not the upload validator. The upload applies its own rules, including the 500 KB per-file cap, the 25 MB extracted limit, the file-type allowlist and the blocked code patterns (Theme requirements). It also does not parse Liquid. A syntax error such as a keyword argument to a filter only shows up when the page renders, so load every template in theme dev before packaging.

eekaam theme check
eekaam theme check --json --fail-on-warning   # for CI

6. Package

theme package runs theme check first and stops on any error. It then writes {name}-{version}.zip using the values in theme.json, leaving out paths matched by .eekaamignore. A ZIP over 10 MB is deleted and the command fails. Bump version for every submission (The manifest).

eekaam theme package
eekaam theme package --output dist/my-theme.zip

7. Submit

Upload the ZIP to your theme in the partner dashboard. The upload is validated at once against Theme requirements and then reviewed by a person. The theme store covers what happens next.

Reference

Updated 15 September 2026