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.
| Path | Why |
|---|---|
layout/theme.liquid | The outer HTML document. Must contain {{ content_for_header }} in <head> and {{ content_for_layout }} in <body>. |
templates/index.liquid | The 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
| Limit | Value | Error code |
|---|---|---|
| ZIP file | 10 MB | zip_too_large |
| Total uncompressed size | 25 MB | extracted_too_large |
| Any single image | 2 MB | file_too_large |
| Any single non-image file | 500 KB | file_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.
| Category | Extensions |
|---|---|
| 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.
| Pattern | Reason |
|---|---|
eval( | Arbitrary code execution |
exec( | Arbitrary code execution |
__import__( | Arbitrary code execution |
process.env | Reading server environment |
fs.readFile | Reading 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.jsonvisibly 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 code | What it means | Fix |
|---|---|---|
missing_required | layout/theme.liquid or templates/index.liquid is not at that exact path | Check for a stray wrapper folder or a renamed file |
disallowed_extension | A file type outside the allowlist | Remove build artefacts (.map, .scss, .ts) before zipping |
file_too_large | One image over 2 MB, or one source file over 500 KB | Compress images; split or CDN-host large libraries |
zip_too_large | Archive over 10 MB | Usually uncompressed images or a committed node_modules |
extracted_too_large | Uncompressed contents over 25 MB | Same causes as above |
dangerous_pattern | A sandbox-escape pattern in authored code | Move third-party code to assets/; remove the pattern from your own |
zip_invalid | The archive could not be opened | Re-create it with zip -r, not a rename of a folder |
Updated 8 September 2026