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 (exceptdata: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, andurl()that is notassets/<name>, ahttps://fonts.gstatic.com/...file or a smalldata:image/...;base64image. The classhystore-requiredis reserved. - Liquid tags
include,layoutandblock(userender, the layout file andhystore_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, notsections/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
{
"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 nodarkclass on<html>: style withhtml:not(.dark) ...orhtml.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,idin 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 }}.
{%- 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 %}:
<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 %}
sectionis{ 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):
{
"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 fromconfig/values.jsonundersections.hero.settings.
Components
components/<name>.liquid, used with {% render 'name', key: value %}. A
component sees only what is passed to it (plus the globals).
{% 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):
{% 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).
<img src="{{ 'banner.webp' | asset_url }}" alt="{{ store.name }}">
@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:
<script src="/sdk/v1.js"></script>
<script src="app.js"></script>
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 bedata:images or the theme's own images and fonts by their full address (https://theme-assets.hystore.com.br/..., asGET /v1/theme/assetsand the MCP toollist_assetsgive it). app.jsonis optional:heightfrom 40 to 2000 pixels (320 by default),namefor screen readers,permissionsonlycustomer.- 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 checkhas 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,/perfiland/entrar(or usedata-hystore-open).
