# 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>
```

```js
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`).
