docs
Themes

Theme requirements

Theme requirements

Every theme submitted to the Eekaam Theme Store is checked automatically the moment you upload the ZIP, and then reviewed by a person. This page is the automatic half: the file layout, the required files, the size and file-type limits, and the code patterns that cause an immediate rejection.

A submission that fails any check on this page is rejected before review, and the upload screen tells you exactly which file and which rule. Fix it and upload again — there is no penalty for a rejected upload.

Reference theme: eekaam-loom-theme is the production Loom theme, and it passes every requirement here. When a rule below is ambiguous, the reference theme is the ground truth — copy its structure rather than guessing.

Themes use Liquid, rendered server-side. If you have built a Shopify theme, nearly all of it transfers: the same directory names, the same {% schema %} blocks, the same JSON templates, the same AJAX cart endpoints.

1. Directory structure

A theme is a directory of seven folders plus a manifest. Nothing else at the top level is read.

my-theme/
├── theme.json                  Manifest: name, version, section limits
├── layout/
│   └── theme.liquid            Required. The HTML shell every page renders into
├── templates/
│   ├── index.liquid            Required. The home page
│   ├── index.json              Optional. Section order and settings for the home page
│   ├── product.liquid
│   ├── collection.liquid
│   ├── cart.liquid
│   └── …
├── sections/
│   └── hero.liquid             Merchant-configurable blocks, each with a {% schema %}
├── snippets/
│   └── product-card.liquid     Reusable fragments, included with {% render %}
├── assets/
│   ├── base.css
│   └── theme.js                CSS, JS, images, fonts
├── config/
│   ├── settings_schema.json    The theme settings a merchant can edit
│   └── settings_data.json      Default values for those settings
└── locales/
    ├── en.json
    └── ur.json                 Translation strings

Folder names are fixed and case-sensitive. A file placed outside these folders is uploaded but never rendered.

2. Required files

Exactly two files are mandatory. If either is missing the upload fails with missing_required.

PathWhy
layout/theme.liquidThe outer HTML document. Must contain {{ content_for_header }} in <head> and {{ content_for_layout }} in <body>.
templates/index.liquidThe home page. Without it a store has nothing to render at /.

Everything else is optional, but a theme with only these two files will not pass human review — see Testing before you submit.

3. Package format

Submit a single .zip. Both of these layouts are accepted:

  • Files at the root of the archive: layout/theme.liquid, templates/index.liquid, …
  • Everything inside one wrapper folder: my-theme/layout/theme.liquid, my-theme/templates/index.liquid, …

If all files share a single top-level folder, that folder is stripped automatically. Mixing the two — some files at the root, some inside a folder — is treated as no wrapper, and the nested files end up at the wrong paths.

Dotfiles and dot-directories (.git, .DS_Store, .env) are skipped silently, as is any path containing ... You do not need to clean them out first, but do not rely on them shipping.

# From inside your theme directory — no wrapper folder, no dotfiles
zip -r ../my-theme.zip . -x '.*' -x '*/.*'

4. Size limits

LimitValueError code
ZIP file10 MBzip_too_large
Total uncompressed size25 MBextracted_too_large
Any single image2 MBfile_too_large
Any single non-image file500 KBfile_too_large

The 500 KB per-file cap applies to Liquid, CSS, and JS. A bundled library that exceeds it must be split or loaded from a CDN. Images count as images for this purpose: .png, .jpg, .jpeg, .gif, .webp, .svg, .ico.

5. Allowed file types

Only these extensions are accepted. Anything else fails with disallowed_extension.

CategoryExtensions
Templates.liquid
Data.json
Code.css, .js
Images.svg, .png, .jpg, .jpeg, .gif, .webp, .ico
Fonts.woff, .woff2, .ttf, .eot
Text.txt, .md

There is no server-side language in a theme. .php, .py, .rb, .map, .scss, and .ts are all rejected — compile to .css and .js before packaging.

6. Code restrictions

Authored code in layout/, sections/, snippets/, and templates/ is scanned for patterns that indicate a theme is trying to reach outside the storefront sandbox. A match fails the upload with dangerous_pattern.

PatternReason
eval(Arbitrary code execution
exec(Arbitrary code execution
__import__(Arbitrary code execution
process.envReading server environment
fs.readFileReading the server filesystem
require('child_process' | 'fs' | 'net' | 'http' | 'https' | 'os' | 'path')Node built-ins that do not exist in a browser

Files under assets/ are exempt, so a vendored library that happens to contain one of these strings will not block your submission. Keep third-party code in assets/ and your own code in the template folders.

Only the first 512 KB of each file is scanned. This is not an invitation to hide code past that offset — doing so is grounds for removal from the Theme Store.

7. The layout file

layout/theme.liquid wraps every page. Two objects are mandatory:

  • {{ content_for_header }} — inside <head>. The platform injects analytics, Web Vitals, and the theme editor bridge here. Omit it and the theme editor will not work.
  • {{ content_for_layout }} — inside <body>. The rendered template goes here.

Do not hand-write analytics, pixel, or live-reload scripts. They are injected for you, and duplicating them is a review failure.

<!doctype html>
<html lang="{{ request.locale.iso_code }}">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{ page_title }}</title>
    {{ content_for_header }}
  </head>
  <body>
    {% section 'header' %}
    <main>{{ content_for_layout }}</main>
    {% section 'footer' %}
  </body>
</html>

8. Templates

Each page type resolves to a template of the same name in templates/. A template may be Liquid, JSON, or both — when both exist, the JSON file supplies the section order and settings and the Liquid file is the fallback.

Loom ships templates for index, product, collection, collections, cart, blog, article, page, search, contact, account, account-login, account-register, track-order, 404, checkout, legal, and order-confirmation. You do not have to implement all of them, but a missing 404 or search template is a common review comment.

A JSON template lists sections by ID, in render order, with their saved settings and blocks:

{
  "sections": {
    "hero": {
      "type": "hero",
      "settings": { "title": "Curated for You", "text_alignment": "left" },
      "blocks": {
        "heading-1": { "type": "heading", "settings": { "text": "Curated for You", "size": "h1" } }
      },
      "block_order": ["heading-1"]
    }
  },
  "order": ["hero"]
}

9. Sections and blocks

A section is a Liquid file in sections/ ending in a {% schema %} block. The schema is what the merchant sees in the theme editor, and it must be valid JSON — a trailing comma here fails the theme, not just the section.

Every setting needs a type, an id, and a label. select and radio settings need options; range settings need min, max, and step.

{% schema %}
{
  "name": "Rich text",
  "class": "section-rich-text",
  "settings": [
    {
      "type": "select",
      "id": "text_alignment",
      "label": "Alignment",
      "default": "center",
      "options": [
        { "value": "left", "label": "Left" },
        { "value": "center", "label": "Center" }
      ]
    }
  ],
  "blocks": [
    { "type": "heading", "name": "Heading", "settings": [] }
  ],
  "presets": [{ "name": "Rich text" }]
}
{% endschema %}

10. Snippets

Snippets are fragments in snippets/, rendered with {% render 'name' %}. They take no global state — pass everything they need as arguments. Use them for anything that appears in more than one section: product cards, price formatting, icons, pagination.

11. Assets

CSS, JS, images, and fonts live in assets/ and are referenced with the asset_url filter, never a hard-coded path:

Hard-coding /assets/… breaks as soon as the theme is served from the CDN. Themes that do this are rejected.

{{ 'base.css' | asset_url | stylesheet_tag }}
{{ 'theme.js' | asset_url | script_tag }}

12. Settings schema

config/settings_schema.json is an array of groups, each with a name and a settings array. These are the global theme settings — logo, colors, typography, layout — reachable from the theme editor's Settings panel and readable in Liquid as {{ settings.<id> }}.

config/settings_data.json holds the default values. Ship sensible defaults: a theme that renders as a blank white page until the merchant fills in ten fields will not pass review.

[
  {
    "name": "Logo",
    "settings": [
      { "type": "image", "id": "logo", "label": "Logo" },
      { "type": "range", "id": "logo_width", "label": "Width",
        "default": 90, "min": 20, "max": 300, "step": 2, "unit": "px" }
    ]
  }
]

13. Locales

Every merchant-visible string must come from locales/, not be hard-coded in a template. At minimum ship locales/en.json. Loom ships en and ur.

Translations are looked up with the t filter, which resolves dot-notation keys and interpolates named arguments:

The lookup tries locales/en.default.json first, then locales/en.json. A key that is missing from both does not render as an error — the last segment is humanised instead, so products.add_to_cart silently becomes "Add to cart". Convenient, but it means a missing translation is invisible in English and blank-looking in every other locale. Check your locale files against your templates before submitting rather than relying on the page to look broken.

{{ 'products.add_to_cart' | t }}
{{ 'cart.items.count' | t: count: cart.item_count }}

14. The manifest

theme.json at the theme root describes the theme and caps how many times each section may be added to a page. It is metadata only — it is not installed to the storefront.

Bump version on every submission. Two uploads with the same version are treated as the same release.

{
  "name": "Loom",
  "version": "1.6.5",
  "author": "Eekaam",
  "description": "A sophisticated, minimal theme with clean typography and elegant spacing",
  "documentation_url": "https://example.com/docs",
  "support_url": "https://example.com/support",
  "templates": ["index", "product", "collection", "cart"],
  "sections": {
    "hero": { "limit": 2 },
    "rich-text": { "limit": 5 }
  },
  "locales": ["en", "ur"]
}

15. Presets

A preset is one listing in the Theme Store. One theme can ship several — the same code with different settings, images, and copy, sold as distinct visual styles.

Each preset needs its own name, screenshots, and demo store before it can be submitted. A preset marked Info required on the submit screen blocks the whole submission, not just that preset.

16. Testing before you submit

Poorly tested themes are rejected without a detailed review, and repeated failures can suspend your partner account. Before uploading, confirm on a real store:

  • Every template renders with real data — and with no data (empty cart, empty collection, no search results, sold-out product)
  • Add to cart, update quantity, and remove work without a page reload
  • The theme editor loads, and every section can be added, reordered, and removed
  • Every setting in settings_schema.json visibly changes something
  • The storefront works at 320 px, 768 px, and 1440 px wide
  • No console errors on any page
  • No hard-coded asset paths, and no strings that should have been in locales/

17. Reference theme

Clone the reference theme and read it alongside this page:

That ZIP passes every check on this page, and is the fastest way to confirm your packaging step is correct before you start changing code.

git clone https://github.com/hammadsohail/eekaam-loom-theme.git
cd eekaam-loom-theme
zip -r ../loom.zip . -x '.*' -x '*/.*'

18. Common rejection reasons

Error codeWhat it meansFix
missing_requiredlayout/theme.liquid or templates/index.liquid is not at that exact pathCheck for a stray wrapper folder or a renamed file
disallowed_extensionA file type outside the allowlistRemove build artefacts (.map, .scss, .ts) before zipping
file_too_largeOne image over 2 MB, or one source file over 500 KBCompress images; split or CDN-host large libraries
zip_too_largeArchive over 10 MBUsually uncompressed images or a committed node_modules
extracted_too_largeUncompressed contents over 25 MBSame causes as above
dangerous_patternA sandbox-escape pattern in authored codeMove third-party code to assets/; remove the pattern from your own
zip_invalidThe archive could not be openedRe-create it with zip -r, not a rename of a folder

Updated 8 September 2026