HystoreWiki

9 min readView as Markdown

Hystore code theme guide

A Hystore code theme is a folder of Liquid, CSS and JSON files that draws a store's storefront: header, footer, home, shop, product pages, news, players. The store's own code keeps the cart, payment, account and sign-in pages, which never load the theme.

This guide is the reference for people and for AI assistants editing a theme. Everything here is checked by the Hystore servers: a file that breaks a rule is refused when it is saved, with the file, the line and the reason.

Workflow

npx @hystudios/hystore login            # approve in the dashboard (no token to paste)
npx @hystudios/hystore theme pull       # download the draft into this folder
npx @hystudios/hystore theme dev        # watch the folder, upload every save, live preview
npx @hystudios/hystore theme check      # check locally, without uploading
npx @hystudios/hystore theme push       # upload the changes to the draft
npx @hystudios/hystore theme publish    # ask to publish; someone with permission approves in the dashboard
npx @hystudios/hystore theme versions   # the last 30 published versions
npx @hystudios/hystore theme rollback 3 # bring version 3 back to the draft
npx @hystudios/hystore theme new        # start a folder from the Hystore Base theme
  • Edits always go to the draft. The live store only changes when a publish request is approved in the dashboard (Customization > Code theme).
  • The preview link (from theme dev) shows the draft on the store's real address for 24 hours and reloads by itself after each upload.
  • If someone changed a file in the dashboard after your last pull, the upload of that file is refused as a conflict: pull, merge, push again.

Hard rules

These are refused (the save fails):

  • <script>, <style>, <iframe>, <form>, <input>, <textarea>, <select>, <object>, <embed>, <base>, <meta>, <link>, SVG <foreignObject>.
  • Event attributes (onclick, onload, ...), javascript:/vbscript:/data: links (except data:image/png|jpeg|gif|webp;base64), srcdoc, http-equiv, numeric character references (&#...;): write the character.
  • In CSS: @import, expression(), behavior, -moz-binding, @namespace, @document, image-set(), backslash escapes outside strings, and url() that is not assets/<name>, a https://fonts.gstatic.com/... file or a small data:image/...;base64 image. The class hystore-required is reserved.
  • Liquid tags include, layout and block (use render, the layout file and hystore_block).

There is no JavaScript in the theme's pages. Interaction comes from data-hystore-* attributes and <hystore-*> elements (see Interactive pieces). The store cleans the rendered HTML again and its Content Security Policy blocks scripts. What needs JavaScript goes in an isolated app (see Apps).

A theme only reads public data of the store and the nick and skin of who is looking. It never sees balances, emails, orders or payments, and it cannot change prices: buying goes through data-hystore-add-to-cart, handled by the store.

Folder

theme.json                 name, color modes, global settings (required)
layout/theme.liquid        frame of every page; must output {{ content }} (required)
pages/<page>.liquid        one file per page (pages/home.liquid is required)
templates/<page>.json      sections of a page, in order, editable in "Customize theme"
sections/<name>.liquid     blocks with a {% schema %}, used by templates or {% section %}
components/<name>.liquid   pieces used with {% render 'name' %}
styles/<name>.css          every file is loaded, in name order
locales/pt.json en.json es.json   texts for the | t filter
config/values.json         values chosen in "Customize theme"
assets/<name>.<ext>        images (png, jpg, webp, gif, avif up to 3 MB) and fonts (woff, woff2 up to 1 MB)
apps/<app>/<file>          isolated apps with JavaScript: html, js, css, json (see Apps)
  • Names: lower case letters, digits, - and _, one level inside each folder (sections/hero.liquid, not sections/home/hero.liquid); apps take two (apps/roulette/app.js).
  • Limits: 400 text files, 256 KB per file, 5 MB of text; 100 assets, 40 MB. SVG files are not accepted as assets (inline <svg> in Liquid is fine).
  • Pages: home, shop, category, product, search, news, post, page, players, 404, maintenance. A page the theme does not have is drawn by the store's classic layout.
Page Address Extra objects
home /
shop /loja, /loja/<server> categories, server (with a server)
category /loja/<server>/<category> category, products (48)
product /loja/<server>/<category>/produto/<slug> product
search /busca?q=... search.query, search.results (48 products)
news /noticias
post /noticias/<slug> post
page /p/<slug> page.slug
players /jogadores
404, maintenance

Cart (/carrinho), payment, profile (/perfil), purchases, password and sign in (/entrar) are the store's own pages: link to them, never rebuild them.

theme.json

JSON
{
  "name": "My theme",
  "version": "1.0.0",
  "author": "Me",
  "color_modes": ["dark", "light"],
  "settings": [
    { "id": "accent", "type": "color", "label": "Accent color", "default": "" },
    { "id": "radius", "type": "range", "label": "Corner radius", "min": 0, "max": 28, "default": 14 },
    { "id": "show_search", "type": "checkbox", "label": "Search in the header", "default": true }
  ]
}
  • color_modes: the modes the theme draws (dark, light); dark when absent. In light mode the page has no dark class on <html>: style with html:not(.dark) ... or html.dark ....
  • Setting types: text, textarea, richtext, image, url, color, number, range (min, max, step), checkbox, select (options: strings or { "value", "label" }), product, category, server, list (lines of text). Up to 60 settings, id in lower case.
  • Global settings are {{ settings.<id> }} everywhere, and colors and numbers are also CSS variables --theme-<id> on the theme root.
  • The store's primary color is hsl(var(--primary)) (text on it: hsl(var(--primary-foreground))).

config/values.json holds what the store owner chose: { "settings": { "accent": "#ff5500" } }. Keep its shape when editing it.

Layout and pages

layout/theme.liquid wraps every page and must contain {{ content }} where the page goes. The store adds a required area after the theme (ads on free plans, legal links); it cannot be hidden or styled.

A page is pages/<page>.liquid, templates/<page>.json, or both. With both, the page file places the template's sections with {{ template_sections }}.

Liquid
{%- comment -%} pages/home.liquid {%- endcomment -%}
<div class="intro">{{ 'home.welcome' | t }}</div>
{{ template_sections }}

Sections

A section is sections/<name>.liquid ending with one {% schema %}:

Liquid
<section class="hero">
  <h1>{{ section.settings.title | default: store.name }}</h1>
  {%- for block in section.blocks -%}
    <p>{{ block.settings.text }}</p>
  {%- endfor -%}
</section>
{% schema %}
{
  "name": "Hero",
  "settings": [
    { "id": "title", "type": "text", "label": "Title", "default": "" }
  ],
  "blocks": [
    { "type": "line", "name": "Line", "settings": [{ "id": "text", "type": "text", "label": "Text" }] }
  ]
}
{% endschema %}
  • section is { id, type, settings, blocks }; settings come with their defaults. A section sees what its page sees (product, loaded data...).
  • A template lists sections (at most 40 drawn per page):
JSON
{
  "sections": {
    "hero": { "type": "hero", "settings": { "title": "Welcome" } },
    "news": { "type": "latest-news", "disabled": true }
  },
  "order": ["hero", "news"]
}

type is the file name in sections/; "disabled": true hides it.

  • {% section 'hero' %} draws a section in a fixed place (layout, page); its values come from config/values.json under sections.hero.settings.

Components

components/<name>.liquid, used with {% render 'name', key: value %}. A component sees only what is passed to it (plus the globals).

Liquid
{% render 'product-card', product: item %}

Texts in three languages

locales/pt.json, en.json, es.json hold texts (nested objects allowed). {{ 'nav.shop' | t }} shows the text of the visitor's language, falling back to Portuguese, then to the key itself. A missing locale file is a warning.

Data

Everywhere:

Object Fields
store id, name, slogan, description, logo, ip, type (game or digital), game (minecraft, hytale or null), currency
route path, page, params (server_id, category_slug, product_slug, slug), query.q
modules forum, wiki, support, forms, launcher, discord, vip (true when on)
customer logged_in, nick, skin_url
cart count
settings global settings of theme.json
locale pt, en or es
color_mode dark or light

More data, on demand, with {% load %} (at most 24 loads per page; the same load is fetched once):

Liquid
{% load products, limit: 8, featured: true as featured %}
{% for p in featured %}{% render 'product-card', product: p %}{% endfor %}
Load Options Gives
products limit (12), category, server, featured, query list of products
product slug, server a product or null
categories server list of categories
servers list of { id, name, url, image, online }
server_status { online, players_online, players_max, version } or null
news limit (6), page list of posts
post slug a post or null
ranking period (month or all), limit (5) list of { position, nick, total_spent, orders, skin_url }
top_supporter { nick, total_spent, purchases, skin_url } or null
goal { target, current, percent, ends_at, rewards } or null
discord { url, server_id, widget }
launcher { name, version, downloads } or null
socials map instagram, discord, twitter, tiktok, telegram, whatsapp → URL
online_players { total, servers: [{ name, count, max, players }] }
  • Product: id, name, slug, url, price, original_price, on_sale, image, short_description, description (HTML), duration_days, min_quantity, featured, sold_out, digital, category_slug, server_id.
  • Category: id, name, slug, url, description, image, product_count.
  • Post: id, title, slug, url, author, image, excerpt, content (HTML), published_at.

Filters

Hystore:

  • t: text of the visitor's language, {{ 'cart.add' | t }}.
  • money: price in the store's currency, {{ product.price | money }}.
  • asset_url: address of an uploaded asset, {{ 'logo.webp' | asset_url }}.
  • skin_url: Minecraft head of a nick, {{ customer.nick | skin_url: 64 }}.
  • raw: only for HTML the store already cleaned (product.description, post.content). Everything else is escaped.

Standard Liquid: abs, append, at_least, at_most, capitalize, ceil, compact, concat, date, default, divided_by, downcase, escape, escape_once, first, floor, join, json, last, lstrip, map, minus, modulo, newline_to_br, plus, prepend, remove, remove_first, replace, replace_first, reverse, round, rstrip, size, slice, sort, sort_natural, split, strip, strip_html, strip_newlines, sum, times, truncate, truncatewords, uniq, upcase, url_decode, url_encode, where.

Tags: if/elsif/else, unless, case/when, for (break, continue), assign, capture, echo, liquid, comment, raw, cycle, increment, decrement, tablerow, plus render, section, load, hystore_block, schema.

Ready-made blocks

{% hystore_block 'name', key: value %} places a piece of the store that needs live data or the store's logic:

heading, image, button, icon, video, card, testimonial, countdown, banner, spacer, divider, server-hero, products, featured-product, categories, servers, ranking, top-supporter, goal, news, discord, launcher, server-status, server-ip, socials, logo, menu, account-actions, language, color-mode, legal-links, copyright.

The name must be written in quotes. Options are text, numbers or true/false.

Interactive pieces

Attributes (the store does the work, no JavaScript in the theme):

Attribute What it does
data-hystore-add-to-cart="<product id>" adds the product to the cart (asks to sign in first)
data-hystore-open="cart|account|login|search" opens that place
data-hystore-copy="text" copies the text (server IP...)
data-hystore-color-mode="toggle|dark|light" changes the color mode
data-hystore-language="pt|en|es" changes the language
data-hystore-cart-count the element always shows the cart count
data-hystore-tabs [data-hystore-tab="x"] buttons show [data-hystore-panel="x"]
data-hystore-accordion (="single" keeps one open) each [data-hystore-accordion-item] has a [data-hystore-accordion-toggle] and a [data-hystore-accordion-panel]
data-hystore-modal="id" opens #id that has data-hystore-modal-panel; data-hystore-modal-close closes
data-hystore-carousel [data-hystore-slides] scrolls, [data-hystore-prev]/[data-hystore-next], data-hystore-autoplay="5000"
data-hystore-countdown="2026-12-24T23:59:00Z" fills [data-hystore-days], [data-hystore-hours], [data-hystore-minutes], [data-hystore-seconds] (or its own text)
data-hystore-animate gets data-visible when it scrolls into view (animate it in CSS)

Elements the store fills in: <hystore-cart-count>, <hystore-account> (sign in or the account menu; add data-cart to show the store's cart button next to it), <hystore-server-status>, <hystore-online-players>, <hystore-search data-placeholder="...">, <hystore-skin data-nick="Steve" data-size="128">, <hystore-video data-youtube="<id or link>">, <hystore-app data-app="<name>"> (an isolated app, see Apps).

Links inside the store (<a href="/loja">) change page without reloading; account and payment pages always load as a new page.

Images and fonts

Put them in assets/ (the tool uploads them; the server checks the bytes).

Liquid
<img src="{{ 'banner.webp' | asset_url }}" alt="{{ store.name }}">
CSS
@font-face { font-family: "Display"; src: url(assets/display.woff2) format("woff2"); font-display: swap; }
.hero { background-image: url(assets/banner.webp); }

Apps

An app is a small web page with JavaScript (a calculator, a prize wheel, a mini game, an animation) that the theme places with <hystore-app data-app="roulette"></hystore-app>. It lives in apps/<app>/ and starts at index.html:

apps/roulette/index.html   the page (required)
apps/roulette/app.js       its script
apps/roulette/app.css      its style
apps/roulette/app.json     { "name": "Prize wheel", "height": 420, "permissions": ["customer"] }

It runs isolated, in a frame on another domain (hystoreapps.cloud): no network (fetch, XHR and WebSocket are blocked), no cookies or storage, no forms, no popups, and it cannot change the store page or navigate it. It talks to the store only through the bridge:

HTML
<script src="/sdk/v1.js"></script>
<script src="app.js"></script>
JavaScript
hystore.products({ category: 'vips', limit: 6 }).then(function (products) {
  // [{ id, name, url, price, priceText, originalPrice, onSale, image, shortDescription, ... }]
});
hystore.categories();                    // active categories
hystore.addToCart(productId, 1);         // 'added', or 'login' (the store asks to sign in)
hystore.navigate('/loja');               // a page of the store
hystore.open('cart');                    // 'cart' | 'login' | 'account' | 'search'
hystore.context();                       // { locale, colorMode, store: { name, currency } }
hystore.customer();                      // { loggedIn, nick, skinUrl }, only with "customer" in app.json
hystore.resize(600);                     // or hystore.autosize() to follow the page
  • Everything is public data: prices come from the store, and the cart only takes a product id. Money and account pages never show apps.
  • Load only the app's own files and /sdk/v1.js; scripts and styles from other sites, frames and forms are refused when saving. Images can be data: images or the theme's own images and fonts by their full address (https://theme-assets.hystore.com.br/..., as GET /v1/theme/assets and the MCP tool list_assets give it).
  • app.json is optional: height from 40 to 2000 pixels (320 by default), name for screen readers, permissions only customer.
  • Limits: 10 apps per theme, 20 files and 512 KB per app.
  • Too many messages are ignored; adding to the cart and changing page have their own small limits.

Checklist before publishing

  • npx @hystudios/hystore theme check has no errors.
  • Every text a visitor reads goes through | t, in pt, en and es.
  • The theme works in the color modes it declares and on a phone.
  • Links to cart, account and sign in point to /carrinho, /perfil and /entrar (or use data-hystore-open).