# Sneferu documentation > Sneferu is the theme language and SDK for Hekana stores: HTML with hk-* tags and a small template syntax. The CLI checks a theme, previews it with sample data and compiles it into one artifact a store can run safely. --- Source: https://hekana.dev/ # Overview ## What a section looks like This is the hero section every new theme starts with: a dark band with a large heading, a sentence and a button. A section is one file: the markup first, then an `` that lists what the merchant can edit. ```html title="sections/hero.snf" {% for block in section.blocks %} {% endfor %} { "name": "Hero", "version": 1, "blocks": { "allowed": ["chip"], "max": 4 }, "settings": { "title": { "type": "text", "label": "Title", "default": "Hello", "maxLength": 80 }, "subtitle": { "type": "textarea", "label": "Subtitle", "default": "", "maxLength": 200 } } } ``` ![A dark band with a large white heading, a line of text and an orange button](https://hekana.dev/developers/examples/starter-hero@2x.webp "The starter hero in the dev preview, 1080 px wide.") #### What each line does 1. **Line 1** `` is the band. `background="theme:secondary"` paints it with a colour from the theme's `theme.json`. `padding-y="96"` is the space above and below the content; on phones it is 48. 2. **Line 2** `` puts everything inside it in a column, 16 px apart. 3. **Line 3** `` is the big title. `{{ section.settings.title }}` prints the `title` setting: what the merchant typed, or the default. 4. **Line 4** `` prints the `subtitle` setting. 5. **Lines 5-7** draws any small labels the merchant added inside the hero (blocks). A new hero has none. 6. **Line 8** `` is a link drawn as a button. `t('shop_now')` takes its words from the theme's locale file. 7. **Lines 11-34** `` names the section and declares its settings: `title` and `subtitle`, each with a label and a default. More complete sections, each with a picture: the [Cookbook](https://hekana.dev/guide/cookbook). ## Quick start Three commands take you from an empty folder to a theme on a store. You need Node.js 20 or newer; `npx` downloads the CLI the first time. ### 1. Create a theme ```bash npx sneferu init my-theme ``` Copies the starter theme into a new folder, `my-theme`, and names the theme after the folder. `npm create sneferu my-theme` does the same. ### 2. Preview it ```bash npx sneferu dev ./my-theme ``` ![The starter theme in the dev preview: the hero, then the first sample product cards](https://hekana.dev/developers/examples/starter-dev@2x.webp "The top of the page straight after init, 1080 px wide.") Open `http://127.0.0.1:4100`. The preview fills the theme with sample products and reloads every time you save a file. ### 3. Push a version ```bash npx sneferu push ./my-theme --package my-theme ``` Checks the theme, compiles it and uploads it to Hekana as a new version. Sign in first with `npx sneferu login`; any Hekana account can [get an API key](https://hekana.dev/guide/get-an-api-key). Add `--dev-store` to publish the version on your free dev store. See [Push and publish](https://hekana.dev/guide/publishing). --- Source: https://hekana.dev/guide/getting-started # 1. Getting started Sneferu is the language and SDK for Hekana store themes. A theme is a folder of HTML-like `.snf` templates with `hk-*` tags, a `theme.json` manifest, locale files and CSS. The CLI checks it, previews it with sample data and compiles it to one JSON artifact that Hekana renders. ## Prerequisites | Need | Version | | - | - | | Node.js | 20 or newer (`node -v`) | | An editor | any; `.snf` files are HTML, so HTML highlighting works. [Format your theme](https://hekana.dev/guide/format-your-theme) sets up Prettier and format on save | ## Create a theme ```bash npx sneferu init my-theme cd my-theme ``` `init` copies the starter theme into `my-theme/` and sets `theme.json` `id` from the folder name (`my-theme`) and `name` to its title case (`My Theme`). It refuses a non-empty folder unless you pass `--force`. > To skip `npx`, install the CLI once: `npm i -g sneferu`. It is then > available as `sneferu` (or the short `snf`), for example `sneferu dev my-theme`. > `npm create sneferu my-theme` does the same as `npx sneferu init my-theme`. > `sneferu` is a short name for the `@hekana/sneferu` package; both install the same tool. ## Start from a template `init` takes a template name. The default, `starter`, is the SDK's own complete store; the others are finished example themes, each with its own design on every page. ```bash npx sneferu init my-shop --template arcade npx sneferu init --list ``` | Template | What you get | | - | - | | `starter` | A neutral complete store: header, footer, every page type, tokens to edit. The default | | `arcade` | Gaming: neon cyan and lime on near-black, for game gift cards and top-ups | | `console` | Gaming: clean and light, for console games and subscriptions | | `esports` | Gaming: bold and loud, for tournaments, teams and gear | | `creator` | Digital: minimal, for a creator selling files, courses and presets | | `license` | Digital: dark SaaS look, for software licenses and plans | | `bento` | Digital: soft bento tiles, for apps and templates | A template is a normal theme folder. It has a template for every page type (home, product, collection, search, cart, 404, content page), header, footer and announcement bar as section groups, theme settings, and Arabic and English locale files, so nothing falls back to the SDK's default store. `init` sets the theme `id` and `name` from the folder name and leaves everything else yours to change. `sneferu validate`, `format --check` and `build` pass on a fresh copy. A name that is not in the list prints the list and exits with an error. ## Preview: `dev` ```bash npx sneferu dev # current folder npx sneferu dev ./my-theme --port 4200 ``` ![The starter theme in the dev preview: a dark hero with a heading and button, then the first sample product cards](https://hekana.dev/developers/examples/starter-dev@2x.webp "The top of the dev preview straight after init, 1080 px wide. The products and prices are the preview's sample data.") Open `http://127.0.0.1:4100`. A new theme looks like the picture: the starter's hero section and a grid of sample products, in English, left to right. | What | Detail | | - | - | | Address | `http://127.0.0.1:4100` (local only; `--port` changes the port) | | Language | The theme's `defaultLocale`. `?locale=` shows another locale the theme lists in `locales` | | Other templates | `http://127.0.0.1:4100/__template/` renders `templates/.snf` | | Live reload | Saving any file recompiles and reloads the open page | | Errors | A theme that does not compile shows the diagnostics list instead of the page | | CSS | Every `assets/*.css` file (top level of `assets/`) is inlined after the token variables | | Assets | `asset_url('logo.svg')` points at `/__assets/logo.svg` | The preview uses sample data, not a real store: | Data | Sample value | | - | - | | `store.name` | `Sample store` | | Currency | EGP, so `money` prints `EGP 520.00` | | `collection('featured')`, `'new'`, `'gift-cards'`, `'subscriptions'` | 4 to 6 products each, with `title`, `url` (`/products/`, or `/en/products/` in English), `price` (piastres), `compare_at`, `image.url` | | `product('sample-1')` | the first of those products | | Theme settings | every setting at its `default` | | Sections | the default sections listed in the template's `` (see [chapter 3](https://hekana.dev/guide/sections-and-blocks)) | Any other collection is empty and any other product is nothing, which is a good way to check your empty states. ### Your own sample data To preview with products like your store's, put a `.sneferu-dev/data.json` in the theme folder (or pass `--data `). Folders starting with a dot are never part of the theme and are never pushed. ```json title=".sneferu-dev/data.json" { "store": { "name": "Pages" }, "collections": { "ebooks": { "title": "E-books", "products": [ { "title": "Notes on design", "url": "/products/notes", "price": 9900, "compare_at": null, "image": { "url": "https://example.com/notes.png" } } ] } } } ``` | Key | Effect | | - | - | | `store` | merged over the sample store, e.g. `store.name` | | `collections` | added to the sample collections; a handle you list (even `featured`) replaces the sample one | | `products` | added to the `product('handle')` lookups, keyed by handle; a product has a page at `/products/` only when it is listed here | | `menus` | menus by handle, `{ "handle", "title", "items": [{ "title", "url", "children"? }] }`; they replace the sample `main` and `footer` | For different text per language, split the file by locale: `{ "ar": { "store": … }, "en": { "store": … } }`. The file is read on every request: save it and reload the page. A file that is not valid JSON shows an error page naming the file. `init` writes a complete store: a layout with header, footer and announcement groups, a template for every page type (home, product, collection, search, cart, page, 404), the sections, blocks and snippets they use, tokens in `theme.json` and `assets/theme.css`. `npx sneferu init --template starter` is the same thing. Everything in it is editable by the merchant: see [Make everything editable](https://hekana.dev/guide/make-everything-editable). ## Check: `validate` ```bash npx sneferu validate ``` ``` ✔ theme my-theme@0.1.0 is valid ``` or, with problems, one line per diagnostic and a non-zero exit code: ``` sections/hero.snf:3:5 error SN2004 unknown property "colour" on . Did you mean "color"? ✖ 1 error, 0 warnings ``` Format: `file:line:col severity CODE message`. Every code is listed in [diagnostics.md](https://hekana.dev/reference/diagnostics). `validate` exits `0` when valid and `1` when not, so it can gate CI. ## Compile: `build` ```bash npx sneferu build # → dist/my-theme-0.1.0.sneferu.json npx sneferu build --out out # another output folder ``` `build` runs the same checks as `validate`, writes `/-.sneferu.json` (the artifact) and copies `assets/` to `/assets/`. It prints the artifact hash. You do not upload this file: `sneferu push` sends the source and Hekana compiles it itself (see [chapter 7](https://hekana.dev/guide/publishing)). ## Commands | Command | Arguments | Options | | - | - | - | | `init [dir]` | folder to create, default `my-theme` | `--force` write into a non-empty folder; `--template ` what to scaffold (default `starter`); `--list` list the templates. Also writes an `AGENTS.md` for AI coding agents (see [Build themes with AI agents](https://hekana.dev/guide/ai-agents)) | | `dev [dir]` | theme folder, default `.` | `--port ` default `4100`; `--data ` sample data JSON, default `.sneferu-dev/data.json` when it exists | | `validate [dir]` | theme folder, default `.` | | | `build [dir]` | theme folder, default `.` | `--out ` default `dist` | | `format [dir]` | theme folder, default `.` | `--check` only report files that are not formatted and exit `1` (for CI). Formats `.snf` files, `theme.json` and `locales/` with Prettier and the Sneferu plugin; see [Format your theme](https://hekana.dev/guide/format-your-theme) | | `login` | | `--api-url ` API to sign in to, default `https://api.hekana.com/api/v1`. Signs in through the browser and saves a developer key to `~/.config/sneferu/credentials.json`; revokes the previously saved key for the same API | | `logout` | | revokes the saved key on Hekana and deletes the file | | `whoami` | | prints the account, key prefix and dev store the CLI uses | | `push [dir]` | theme folder, default `.` | `--package ` required, must equal `theme.json` `id`, and not a reserved slug (`starter`, `hekana`, `default`, `admin`, `official`, `theme`, `themes`, `test`, `demo`, `preview`, and anything starting with `hekana-`); `--dev-store` also installs and publishes the version on your dev store and prints its URL. Uses the key saved by `login`, or env `HEKANA_TOKEN`; env `HEKANA_API_URL` | `login`, `logout`, `whoami` and `push` are covered in [Get an API key](https://hekana.dev/guide/get-an-api-key) and [Push and publish](https://hekana.dev/guide/publishing). ## Next 1. [Theme structure](https://hekana.dev/guide/theme-structure): what every file does. 2. [Sections and blocks](https://hekana.dev/guide/sections-and-blocks): the parts merchants edit. 3. [Data and expressions](https://hekana.dev/guide/data-and-expressions): `{{ }}`, filters, loops. 4. [Styling and responsive](https://hekana.dev/guide/styling-and-responsive): tokens, CSS, RTL. 5. [Security and limits](https://hekana.dev/guide/security-and-limits): what a theme cannot do. 6. [Get an API key](https://hekana.dev/guide/get-an-api-key): developer access, `sneferu login`, CI. [Push and publish](https://hekana.dev/guide/publishing): `sneferu push`, your dev store, review. 7. [Cookbook](https://hekana.dev/guide/cookbook): five complete store sections, with pictures. [Format your theme](https://hekana.dev/guide/format-your-theme): `sneferu format`, Prettier and VS Code. 8. [Tutorial: build a digital products store theme](https://hekana.dev/guide/tutorial-digital-store): a whole theme, from mockup to live store. [Tutorial: build a gaming store theme](https://hekana.dev/guide/tutorial-gaming-store): another whole theme, a dark gaming top-up store. 9. [Tag reference](https://hekana.dev/reference/tags): every `hk-*` tag and prop. --- Source: https://hekana.dev/guide/theme-structure # 2. Theme structure ``` my-theme/ ├── theme.json manifest (required) ├── layout/ │ └── theme.snf page frame (required) ├── templates/ │ └── index.snf home page (required); one file per page type ├── groups/.json first content of the shared header, footer, announcement ├── sections/*.snf page parts merchants add, reorder and configure ├── blocks/*.snf repeatable items inside a section ├── snippets/*.snf partials for {% render %} ├── assets/ css, js, images, fonts ├── locales/ │ └── en.json UI strings, one file per locale └── migrations/*.json settings migrations (reserved) ``` `node_modules/`, `dist/`, `.git/`, `.sneferu-dev/` and `.DS_Store` are ignored by the CLI, and so are, at the top of the folder only, `package.json` and lock files, `README.md`, `LICENSE`, `CHANGELOG.md`, `AGENTS.md`, `CLAUDE.md` and a `docs/` folder (for notes, screenshots and AI agent instructions). None of these is checked or pushed. Anything else in the folder is part of the theme and is checked. ## Files and what they are for | Path | Role | Notes | | - | - | - | | `theme.json` | Identity, version, design tokens, theme-wide settings | See the field table below | | `layout/theme.snf` | Wraps every page: header, footer, and `` where the page goes | Exactly this file name | | `templates/.snf` | A page type. Usually `` plus a `` listing the default sections | `index` is required. Also `product`, `collection`, `search`, `cart`, `page`, `404` (and alternates such as `product.codes`); a page type you do not write comes from the SDK's default store. See [Page types](https://hekana.dev/guide/page-types) | | `groups/.json` | The sections a store starts with in a shared group (`header`, `footer`, `announcement`) | See [Section groups](https://hekana.dev/guide/section-groups) | | `sections/.snf` | A section type. File name = type (`sections/hero.snf` is type `hero`). Has a `` with `name` and `settings` | Rendered inside `
` | | `blocks/.snf` | An item a merchant can add inside a section (a quote, a slide, a feature) | Rendered inside `
` | | `snippets/.snf` | A reusable partial, no schema | `{% render 'name', key: value %}` | | `assets/` | CSS, JS, images, fonts: `.css .js .png .jpg .jpeg .webp .avif .gif .svg .woff .woff2 .ttf .otf .ico` | The only folder that may have sub-folders. `assets/*.css` is the theme stylesheet. Font files are accepted but not served on the store: use `tokens.fonts` | | `locales/.json` | Flat `{ "key": "text" }` maps, values strings only | Used by `t('key')` and `\| t` | | `migrations/*.json` | Settings migrations between theme versions | Accepted, not used yet | Section, block and snippet file names may not start with `hk-` (SN3043): that prefix is reserved for the default store's own sections, blocks and snippets, which a theme borrows under `hk-`. A minimal layout: ```html
``` A template: ```html {"sections":[ {"type":"hero","settings":{"title":"Welcome"}}, {"type":"featured"} ]} ``` `` and `` are allowed only in the layout and in templates (SN2011). ## theme.json ```json { "id": "nile-fashion", "name": "Nile Fashion", "description": "A theme for clothing stores", "version": "1.0.0", "author": "Hekana", "category": "fashion", "engine": "hk2", "engineCompatibility": "^2.0.0", "defaultLocale": "en", "locales": ["en"], "settings": { "accent": { "type": "color", "label": "Accent", "default": "#B45309" } }, "tokens": { "colors": { "primary": "#111827", "secondary": "#1F2937", "accent": "#B45309", "background": "#FFFFFF", "text": "#111827", "muted": "#6B7280" }, "layout": { "contentWidth": "1200px" } } } ``` | Field | Required | Rule | Error | | - | - | - | - | | `id` | yes | 3-41 characters, lower-case letters, digits, `-`, starts with a letter. Equals the package slug you push to | SN3012 | | `name` | yes | a string, e.g. `"Nile Fashion"` | SN3013 | | `description` | yes | a string | SN3016 | | `version` | yes | `x.y.z`. Bump it for every push; a pushed version cannot be replaced | SN3014 | | `author` | yes | non-empty string | SN3015 | | `category` | yes | non-empty string (`general`, `fashion`, `electronics`, …) | SN3017 | | `engine` | yes | exactly `"hk2"` | SN3010 | | `engineCompatibility` | yes | `^2.0.0` or an exact version; must include the current engine 2.0.0 | SN3011 | | `defaultLocale` | yes | `"en"` or `"ar"` | SN3018 | | `locales` | yes | array that includes `defaultLocale`, e.g. `["en"]` | SN3018 | | `settings` | yes | object of fields (may be `{}`), same field format as sections. Merchants edit these for the whole theme; read them as `settings.accent` or `settings('accent')` | SN3021 | | `tokens.colors` | yes | `primary`, `secondary`, `accent`, `background`, `text` as `#RRGGBB`. Extra keys (`muted`, `border`, …) are allowed | SN3019 | | `tokens.layout.contentWidth` | no | `"<3-4 digits>px"`, e.g. `"1200px"`; becomes `--hk-content-width` | ignored if malformed | | `tokens.fonts` | no | `heading`, `body`, `mono`: font families from the catalogue (see [Fonts](https://hekana.dev/guide/styling-and-responsive#fonts)) | SN3032 | | `money.format` | no | `"decimal"` (default, `1,290.00`) or `"whole"` (`1,290`, two decimals kept when there are piastres): the default for `money`, `` and product cards (see [Money formats](https://hekana.dev/guide/data-and-expressions#money-formats)) | SN3033 | | `tokens.radius` | no | accepted in the manifest, not turned into CSS yet | | | `previews` | no | array of preview image names | | Labels and names (`name`, `description`, every setting `label`, section and block names) are plain strings. Arabic labels are optional: you may write `{ "en": "Accent", "ar": "…" }`, and when `ar` is missing merchants see the `en` label. How tokens become CSS variables is in [chapter 5](https://hekana.dev/guide/styling-and-responsive). ## Locales ```json // locales/en.json { "shop_now": "Shop now", "view_all": "View all" } ``` - Keys are flat; every value must be a string (SN3025). No nesting, no plurals. - A missing key falls back to the default locale's file, then to the key itself. - Put interface words (buttons, empty states, labels) in locales. Put content the merchant should change (titles, descriptions) in section settings instead. ## Names and paths - File and folder names: lower-case letters, digits, `.`, `_`, `-`; must start with a letter or digit. No spaces, upper case, `..`, absolute paths, backslashes or symlinks (SN3002). - Only the folders above, one level deep (except `assets/`), with the listed extensions (SN3003). `sections/hero/hero.snf` is rejected. - Template files end in `.snf`. The older `.sneferu` still works, and one theme can mix the two, but one name may exist in only one form per folder: `sections/hero.snf` next to `sections/hero.sneferu` is an error (SN3044). - Every text file must be UTF-8 (SN3022). ## Size limits | Limit | Value | Error | | - | - | - | | Files per theme | 400 | SN3005 | | Whole theme | 25 MB | SN3006 | | One asset | 2 MB | SN3007 | | One template (`.snf` or `.sneferu`) | 300 KB | SN3008 | | All `assets/*.css` together (checked by the platform on push) | 512 KB | `HK2_CSS_TOO_LARGE` | Compress images before adding them (WebP or AVIF), and prefer the platform's product images over bundled photos. --- Source: https://hekana.dev/guide/format-your-theme # 10. Format your theme `@hekana/prettier-plugin-sneferu` teaches [Prettier](https://prettier.io) to format `.snf` files. It indents nested tags and `{% if %}` / `{% for %}` blocks, puts long attribute lists one per line, tidies the spacing in `{{ }}` and `{% %}`, and pretty-prints the JSON in ``. The `sneferu format` command runs the same plugin over a whole theme, without any setup. Formatting never changes what your theme does: it only moves whitespace that HTML does not render, and the compiled artifact of a theme is identical before and after formatting. ## Before and after A section written on as few lines as possible: ```html title="sections/hero.snf (before)" {% for block in section.blocks %}{% endfor %} {"name":"Hero","version":1,"settings":{"title":{"type":"text","label":"Title","default":"Hello"}}} ``` The same file after `npx sneferu format`: ```html title="sections/hero.snf (after)" {% for block in section.blocks %} {% endfor %} { "name": "Hero", "version": 1, "settings": { "title": { "type": "text", "label": "Title", "default": "Hello" } } } ``` `sneferu init` already writes its starter theme in this style. ## Format with the CLI The CLI includes Prettier and the plugin, so there is nothing to install: ```bash npx sneferu format # current folder npx sneferu format ./my-theme ``` ``` sections/hero.snf formatted ✔ formatted 1 of 8 files ``` `format` formats every `.snf` file, `theme.json` and the files in `locales/`. It reads your Prettier configuration (`.prettierrc` and the other files Prettier supports, and `.editorconfig`) if the theme has one. CSS files in `assets/` are left as they are. A file that does not parse (for example a tag that is never closed) is reported and left unchanged; run `validate` to see the problem. As a last safety check, `format` compiles every file before and after and refuses to write a change the compiler would see. ### Check formatting in CI `--check` writes nothing. It lists the files that are not formatted and exits with `1`, so it can fail a pull request: ```bash npx sneferu format --check ``` ``` sections/hero.snf is not formatted ✖ 1 of 8 files need formatting (run sneferu format) ``` For example, in a GitHub Actions workflow: ```yaml title=".github/workflows/theme.yml" name: theme on: [pull_request] jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npx sneferu format --check - run: npx sneferu validate ``` ## Use Prettier directly To format from your editor, or together with the rest of a project, install Prettier and the plugin in the theme folder: ```bash npm install --save-dev prettier @hekana/prettier-plugin-sneferu ``` Then add a `.prettierrc` file next to `theme.json`: ```json title=".prettierrc" { "plugins": ["@hekana/prettier-plugin-sneferu"] } ``` Now Prettier formats `.snf` files like any other: ```bash npx prettier --write "**/*.{snf,sneferu}" npx prettier --check "**/*.{snf,sneferu}" ``` `package.json`, `node_modules/`, lock files and files whose name starts with a dot (such as `.prettierrc` and `.vscode/`) are not part of the theme: `validate`, `build` and `push` skip them. ## Set up VS Code The [Sneferu extension](https://hekana.dev/guide/vscode) formats `.snf` files with this plugin on its own, with no other setup. Without it, use the Prettier extension: 1. Install the [Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) (`esbenp.prettier-vscode`). 2. Install Prettier and the plugin in the theme folder and add `.prettierrc`, as above. The extension uses the Prettier and the plugin it finds there. 3. Add the settings below to `.vscode/settings.json` in the theme folder. ```json title=".vscode/settings.json" { "files.associations": { "*.snf": "html", "*.sneferu": "html" }, "[html]": { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true } } ``` 1. **Lines 2-5** open `.snf` (and older `.sneferu`) files in HTML mode, for highlighting, tag completion and Emmet. Prettier still picks the Sneferu plugin, because it chooses the formatter from the `.snf` extension, not from the editor mode. 2. **Lines 6-9** make Prettier the formatter for these files and format on save. If you use an extension that registers its own `sneferu` language, put the same two lines under `"[sneferu]"` instead of `"[html]"`. Other editors work the same way: any editor with a Prettier integration formats `.snf` files once the plugin is in `.prettierrc`. ## Options Set options in `.prettierrc`, for example `{ "printWidth": 100 }`. `sneferu format` reads the same file. | Option | Default | Does | | - | - | - | | `printWidth` | `120` | The line length the formatter aims for. A tag whose attributes do not fit goes one attribute per line; text wraps at this width | | `tabWidth` | `2` | Spaces per indentation level | | `useTabs` | `false` | Indent with tabs instead of spaces | | `singleQuote` | `false` | Quote attribute values with `'` instead of `"`. Strings inside `{{ }}` in an attribute then use the other quote | | `sneferuSingleQuote` | `true` | Use `'` for strings inside `{{ }}` and `{% %}` (`"` when `false`). A string that contains the chosen quote keeps the other one | | `indentSchema` | `true` | Indent the JSON inside `` one level. `false` keeps it at the level of the tag | `printWidth` defaults to `120` for `.snf` files (Prettier's own default is `80`): theme tags carry many attributes, and 120 keeps most of them on one line. ## What the formatter changes | Construct | Rule | | - | - | | Elements and `hk-*` tags | One child element per line, indented one level, where whitespace is free to change (see below) | | Attributes | All on one line when the tag fits in `printWidth`, otherwise one per line with `>` or `/>` on its own line. Values are quoted with `"` (or `'` with `singleQuote`); a value is never rewritten except for `{{ }}` inside it | | `{{ }}` | One space inside the braces and around `\|`, `==`, `and`, `or`; filter arguments as `filter: a, b`; no spaces around `.`, `[ ]` or `( )` | | `{% if %}` / `{% elsif %}` / `{% else %}` / `{% endif %}`, `{% for %}` / `{% endfor %}` | The body goes on its own lines, indented, when it contains tags; `limit: n` with one space | | `{% render %}` | Arguments as `'name', key: value, key: value` on one line | | `` | The JSON is pretty-printed: two spaces per level, every object on its own lines, short arrays of plain values on one line. Keys stay in the order you wrote them and values stay exactly as written. JSON that does not parse is left as it is | | Text | Runs of spaces and line breaks become one space, and long text wraps at `printWidth` | | `` | Kept as written | | `
` | Its content is kept exactly as written |
| Blank lines | One blank line between two tags is kept; several become one |

### Whitespace that matters

HTML ignores some whitespace and not other whitespace. Between two block tags
(`
`, `

`, ``, `` …), at the start and end of a block, and between the items of `` and ``, whitespace is not rendered, so the formatter adds and removes line breaks there freely. Between inline content, such as text and `` in a paragraph, a space shows on the page, so the formatter keeps it (it may turn it into a line break, which looks the same), and where you wrote no space it does not add one: ```html

{{ price | money }} a month

``` stays on one line with exactly one space between the two tags, and `ab` stays joined. The compiler follows the same rules: it collapses whitespace runs and drops the whitespace HTML does not render, which is why the artifact is identical before and after formatting. ## Skip a part Put `` before a tag, a `{% if %}` / `{% for %}` block or a `{{ }}` to keep it exactly as written: ```html ``` The comment is dropped when the theme is compiled, like every HTML comment. ## Next [Cookbook](https://hekana.dev/guide/cookbook): five complete store sections, formatted with this plugin. --- Source: https://hekana.dev/guide/vscode # 13. Editor setup: VS Code The Sneferu extension for Visual Studio Code knows the language: it completes `hk-*` tags, props, filters, settings and translation keys, shows the tag reference on hover, runs the Sneferu compiler as you type, flags problems a reviewer would catch, formats with the Sneferu Prettier plugin and shows a live preview of the theme beside the editor. It includes the CLI, so you do not need Node.js or `npm install` to use it. ```bash code --install-extension sneferu-0.2.1.vsix ``` ![VS Code with a .snf file open and a completion list of hk-\* tags: hk-badge, hk-block, hk-button, hk-card and more, each with its label and category](https://hekana.dev/developers/examples/vscode-completion@2x.webp "Typing `. - Ctrl+/ (Cmd+/ on macOS) toggles `` comments. `{{`, `{%`, quotes and brackets close themselves, and typing `>` after ``. - Renaming an opening tag renames its closing tag. - Snippets: type `section`, `section-blocks`, `schema`, `setting`, `template-schema`, `if`, `ifelse`, `for`, `blocks`, `render`, `t`, `product-grid`, `image` or `layout` and press Tab. ## Completion and hover | Where you type | What it offers | | - | - | | `<` | `hk-*` tags (required props filled in), allowed HTML elements | | inside a tag | the tag's props, their `tablet-` and `mobile-` variants, `class`, `id`, `role`, `title` | | inside a prop value | the prop's values: `align="start"`, `theme:accent` for colours, `true`/`false` | | `{{ ` or `{% if ` | `store`, `settings`, `section`, `block`, `forloop`, loop variables, lookups | | after `.` | properties: `section.settings.*` from the file's own ``, `settings.*` from `theme.json`, product fields in `{% for p in collection('…').products %}` | | after `\|` | filters | | `t('` or `'…' \| t` | translation keys from `locales/` | | `{% render '` | snippet names | | `asset_url('` | files in `assets/` | | `"type": "` in `` | field types | Hover shows the same reference: a tag's props with types, ranges and defaults; a filter or lookup; a setting's type, label and default; a translation key's text in every locale. The tags, filters and lookups come from the SDK when the extension is built (`docs/reference/tags.json` and the language reference), so each release of the extension matches an SDK release. ## Diagnostics The extension runs the same compiler as `sneferu validate` on the whole theme, including files you have not saved, about a quarter of a second after you stop typing. Problems appear in the editor and in the Problems panel with their SN code; click the code to open its entry in [Diagnostics](https://hekana.dev/reference/diagnostics). `theme.json` also gets completion and checks from a JSON schema. ## Theme checks Next to the compiler, the extension flags problems that compile but would be caught in review. Each has a name, shown as its code. Turn one off with `"sneferu.checks.disabled": ["OrphanedSnippet"]`, or all of them with `"sneferu.checks.enable": false`. ### UnusedSetting A setting in a section or block `` that the file never reads as `section.settings.` (or `block.settings.`), or a `theme.json` setting no template reads as `settings.`. The merchant would edit a field that changes nothing. Remove it or use it. ### UndefinedSetting `section.settings.` in a section whose schema does not declare ``. It is always empty. Usually a typo. ### TranslationKeyExists `t('key')` or `'key' | t` where the default locale file has no `key`. The page shows the key itself. The quick fix adds the key to every locale file, with the key as its text, for you to translate. ### MatchingTranslations A key in `locales/en.json` that `locales/ar.json` does not have, or the other way round. The quick fix copies the key and its text into the other file, for you to translate. ### MissingLocaleFile `theme.json` lists a locale in `locales` that has no `locales/.json`. ### ImgAltText `` or `` without an `alt` attribute. Describe the picture, or write `alt=""` for a decorative one. The quick fix adds `alt=""`. ### SchemaLabelEnglish A schema `name` or setting `label` with no English text: a string in Arabic only, or `{ "ar": "…" }` without `en`. The theme editor shows `en` to English-speaking merchants. Write `{ "en": "…", "ar": "…" }`. ### AssetSize An asset over 500 KB (`sneferu.checks.assetSizeWarnKb`). The compiler rejects assets over 2 MB (SN3007); this warns well before that. ### MissingAsset `asset_url('file')` where `assets/file` does not exist. ### OrphanedSnippet A snippet that no file renders with `{% render %}`. Shown as a hint. ## Navigation Ctrl+click (Cmd+click on macOS), or F12, on: - `{% render 'name' %}`: opens `snippets/name.snf`, or `snippets/name.sneferu` when the theme still uses the older extension - a translation key: its line in each locale file - `section.settings.` or `settings.`: the setting in the schema or `theme.json` - a section `"type"` in a template's ``: the section file - `asset_url('file')`: the asset ## Formatting **Format Document** (Shift+Alt+F, Shift+Option+F on macOS) uses Prettier and `@hekana/prettier-plugin-sneferu`, with the theme's `.prettierrc` or `.prettierrc.json` if it has one, so the result is the same as `sneferu format`. As with the CLI, a change that would alter what the compiler makes of the file is refused. To format on save, add to `.vscode/settings.json`: ```json title=".vscode/settings.json" { "[sneferu]": { "editor.formatOnSave": true } } ``` **Sneferu: Format Theme** formats every file of the theme at once. ## Live preview **Sneferu: Open Preview** (Ctrl+Shift+P, then type it), or the preview button at the top right of a `.snf` editor, starts `sneferu dev` for the theme and opens the preview beside the editor. - **Desktop**, **Tablet** (820 px) and **Phone** (390 px) set the width. - **AR** and **EN** switch the language, for themes that ship both. - A drop-down picks the template when the theme has more than `index`. - The preview reloads when you save a theme file. - **Inspect**, then click a section in the preview, opens that section's file. Alt+click does the same without turning Inspect on. - **Browser** opens the same page in your browser. The status bar shows the dev server and its port; click it, or run **Sneferu: Stop Dev Server**, to stop it. The preview uses the theme's sample data, `.sneferu-dev/data.json`, or the file set in `sneferu.preview.dataFile` (see [Getting started](https://hekana.dev/guide/getting-started)). ## Commands | Command | Does | | - | - | | Sneferu: Open Preview | starts `sneferu dev` and opens the preview | | Sneferu: Stop Dev Server | stops it | | Sneferu: Validate Theme | checks the whole theme and lists the problems | | Sneferu: Format Theme | `sneferu format` | | Sneferu: Build Theme | `sneferu build`, to the theme's `dist/` | | Sneferu: Push to Dev Store | `sneferu push --package --dev-store`, after you confirm | | Sneferu: Log In | `sneferu login` in a terminal (see [Get an API key](https://hekana.dev/guide/get-an-api-key)) | | Sneferu: Who Am I | `sneferu whoami` | | Sneferu: Restart Language Server | restarts completion and diagnostics | Push uses the theme's `id` from `theme.json` as the package and the key saved by Log In, or `HEKANA_TOKEN`. See [Push and publish](https://hekana.dev/guide/publishing). ## Settings | Setting | Default | Does | | - | - | - | | `sneferu.cliPath` | empty | the CLI to run; empty uses the theme's own `@hekana/sneferu` from `node_modules`, else the one in the extension | | `sneferu.preview.port` | `4100` | the first port tried; the next free one is used when it is taken | | `sneferu.preview.dataFile` | empty | sample data for the preview, relative to the theme | | `sneferu.preview.reloadOnSave` | `true` | reload the preview on save | | `sneferu.diagnostics.delay` | `250` | milliseconds after typing before the theme is checked | | `sneferu.checks.enable` | `true` | run the theme checks | | `sneferu.checks.disabled` | `[]` | checks to turn off, by name | | `sneferu.checks.assetSizeWarnKb` | `500` | the AssetSize budget | The extension runs the CLI with VS Code's own Node.js and no shell, so it needs nothing else installed on Windows, macOS or Linux. Over Remote SSH, WSL or Dev Containers it runs on the remote side and VS Code forwards the preview's port. --- Source: https://hekana.dev/guide/ai-agents # 22. Build themes with AI agents An AI coding agent (Claude Code, Codex, Cursor, Copilot and others) can build a Sneferu theme from a mockup or a brief. Sneferu gives it three things to work from, so it follows the same workflow and rules as a person reading these docs. | What | Where | Read by | | - | - | - | | `AGENTS.md` in every new theme | written by `sneferu init` next to `theme.json`, with a `CLAUDE.md` that points at it | any agent opened in the theme folder | | `AGENTS.md` in the SDK repo | at the top of [github.com/hekanahq/sneferu](https://github.com/hekanahq/sneferu) | agents working in the SDK repo, or told to read it | | The docs as plain text | [hekana.dev/llms.txt](https://hekana.dev/llms.txt) (index) and [hekana.dev/llms-full.txt](https://hekana.dev/llms-full.txt) (every page) | any agent that can fetch a URL | The quickest start is `npm create sneferu my-theme` (the same as `npx sneferu init my-theme`): the new folder already has its `AGENTS.md`, so open your agent in it. `AGENTS.md` and `CLAUDE.md` at the top of a theme folder are not part of the theme: `validate`, `build` and `push` skip them, like `README.md`, so they never change the compiled theme. ## What the agent is told The theme's `AGENTS.md` is short and imperative. It lists: 1. **The commands**: `dev` to preview, `format`, `validate`, `build`, and `push --dev-store` only when you ask for it. 2. **The files**: what `theme.json`, the layout, templates, groups, sections, blocks, snippets, locales and assets are for. 3. **The rules** the compiler enforces (allowed markup, no JavaScript, no form controls, safe links) and the ones it cannot (everything merchant-editable, both text directions, accessible sizes and contrast). 4. **What done means**: `validate` clean, `format --check` clean, every page type checked in every locale at a desktop and a phone width. ## Prompts to start from Copy one, change the details, and attach the mockup if you have one. A new theme from a mockup: ```text Create a Sneferu theme for a coffee shop from the attached mockup. Arabic and English, Arabic first. Start with `npx sneferu init cafe-nile`, read the AGENTS.md it writes, and follow it. Home page: hero, featured beans, subscription plans, footer. Every page type must match the design. Finish when `npx sneferu validate` and `format --check` are clean, and show me screenshots of the dev preview at 1440 and 390 px in both languages. ``` A theme from a written brief: ```text Build a Sneferu theme for a handmade jewellery store: warm neutrals, a rounded sans-serif, large product photos. Read https://hekana.dev/llms-full.txt first. Use the starter (`npx sneferu init`), keep every text editable by the merchant, and do not push anything. ``` One new section in an existing theme: ```text Add a "How it works" section to this Sneferu theme: a heading and three to five steps, each with an hk-icon, a title and a sentence. Steps are blocks. All text is localized settings with Arabic and English defaults. Add it to the home template's default sections and run validate. ``` Start from an example design: ```text Run `npx sneferu init --list`, pick the template closest to a dark gaming top-up store, scaffold it as `game-vault`, then change the colours and fonts to the attached palette. Keep the page structure. ``` Push to your dev store (after `npx sneferu login` in your own terminal): ```text Bump the version in theme.json and push this theme with `npx sneferu push . --package --dev-store`. Then open the dev store URL it prints and check the home, product and cart pages. ``` ## Review what the agent made Before you push a theme an agent wrote, check: | Check | How | | - | - | | It compiles | `npx sneferu validate` prints `✔ theme @ is valid` | | It is formatted | `npx sneferu format --check` | | Every page type is designed | `templates/` has `index`, `product`, `collection`, `search`, `cart`, `page` and `404` | | Nothing is hard-coded | No visible words in the `.snf` markup outside settings, blocks and `t('…')` | | Both languages | `dev` at `/` and `/en`, at 1440 and 390 px; nothing overflows | | Its own brand | Colours are `tokens.colors`, fonts are `tokens.fonts`, no other company's logo | ## Keep it current `AGENTS.md` comes from the CLI version that ran `init`. After upgrading the CLI, compare yours with the one a fresh `init` writes, or point the agent at [hekana.dev/llms-full.txt](https://hekana.dev/llms-full.txt), which always follows the latest release. --- Source: https://hekana.dev/guide/sections-and-blocks # 3. Sections and blocks A **section** is a page part a merchant adds, removes, reorders and configures: a hero, a product grid, testimonials. A **block** is an item inside a section that the merchant can add several of: one testimonial, one slide. Both are a template plus a `` that declares their settings. ## A section A hero: a dark band with a big heading, a sentence and a button. This is the smallest useful section. ```html title="sections/hero.snf" { "name": "Hero", "version": 1, "settings": { "title": { "type": "text", "label": "Title", "default": "Hello", "maxLength": 80 }, "subtitle": { "type": "textarea", "label": "Subtitle", "default": "", "maxLength": 200 } } } ``` ![A dark band with a large white heading, a line of text and an orange button](https://hekana.dev/developers/examples/starter-hero@2x.webp "This hero in the dev preview, 1080 px wide.") #### What each line does 1. **Line 1** `` is the band. `background="theme:secondary"` paints it with the theme's `secondary` colour from `theme.json`. `padding-y="96"` is the space above and below the content; `mobile-padding-y="48"` halves it on phones. 2. **Line 2** `` puts the heading, sentence and button in a column, 16 px apart, lined up at the start of the line. 3. **Line 3** `` is the title. `{{ section.settings.title }}` prints the `title` setting: what the merchant typed, or the default. 4. **Line 4** `` prints the `subtitle` setting. 5. **Line 5** `` is a link drawn as a button. `t('shop_now')` takes its words from `locales/en.json`. 6. **Lines 8-27** `` declares the settings the merchant edits in the store builder. Rules for the file: - The file name is the section type: `hero`. - `` is JSON (double quotes, no trailing commas; SN1107). Put it at the end of the file. - `name` is required (SN3030). It is what merchants see in the builder. - `version` is the schema version (a number, default 1). - `settings` is an object of fields. Read them as `section.settings.`. - Inside a section you also have `section.id`, `section.type` and `section.blocks`. ## Field types Every field is: ```json { "type": "…", "label": "…", "default": …, "min": …, "max": …, "step": …, "options": […], "maxLength": …, "required": …, "responsive": …, "visible_if": { "setting": "…", "equals": … } } ``` Only `type` and `label` are needed. One example of each type: | Type | Value in templates | Example field | | - | - | - | | `text` | short string | `"title": { "type": "text", "label": "Title", "default": "Summer sale", "maxLength": 80 }` | | `textarea` | longer string | `"body": { "type": "textarea", "label": "Body", "default": "" }` | | `rich_text` | string | `"story": { "type": "rich_text", "label": "Story", "default": "" }` | | `number` | number | `"padding": { "type": "number", "label": "Padding", "default": 64, "min": 0, "max": 240 }` | | `range` | number (slider) | `"count": { "type": "range", "label": "Count", "default": 8, "min": 2, "max": 24, "step": 1 }` | | `boolean` | `true` / `false` | `"show_title": { "type": "boolean", "label": "Show title", "default": true }` | | `select` | one of `options` | `"align": { "type": "select", "label": "Align", "options": ["start", "center", "end"], "default": "start" }` | | `color` | `#hex` or `theme:` | `"background": { "type": "color", "label": "Background", "default": "theme:background" }` | | `image` | image reference (URL string) | `"image": { "type": "image", "label": "Image" }` | | `video` | video reference | `"video": { "type": "video", "label": "Video" }` | | `link` | URL string | `"link": { "type": "link", "label": "Link", "default": "/products" }` | | `icon` | icon name | `"icon": { "type": "icon", "label": "Icon", "default": "truck" }` | | `product` | product handle | `"product": { "type": "product", "label": "Product" }` | | `collection` | collection handle | `"collection": { "type": "collection", "label": "Collection", "default": "featured" }` | | `menu` | menu handle | `"menu": { "type": "menu", "label": "Menu", "default": "main" }`: read it with `menu(section.settings.menu)` (see [Menus](https://hekana.dev/guide/menus)) | | `spacing` | string | `"gap": { "type": "spacing", "label": "Spacing", "default": "md" }` | | `typography` | string | `"font": { "type": "typography", "label": "Typography", "default": "body" }` | Notes: - `product` and `collection` store a handle string. `product()` and `collection()` take a literal handle or a setting of their own type: `collection(section.settings.collection)` reads the collection the merchant picked. Any other argument is SN1105 (see [Data and expressions](https://hekana.dev/guide/data-and-expressions)). - `required`, `visible_if` (show this field only when another setting equals a value) and `responsive` are hints for the editor. Rendering does not enforce `required`; always set a sensible `default`. - Labels are shown to merchants in the dashboard: keep them short and plain. ## How settings reach the page (coercion) The merchant's stored values are never trusted as-is. Before a section renders, each declared setting is filled and coerced: | Situation | Result | | - | - | | No stored value | `default` (or nothing, if there is no default) | | `text`, `textarea`, `rich_text`, `image`, `video`, `link`, `icon`, `product`, `collection`, `menu`, `spacing`, `typography` | must be a string, else `default`; cut to `maxLength` if set | | `number`, `range` | numbers and numeric strings are parsed and clamped to `min`/`max`; anything else gives `default` | | `boolean` | `true`, `"true"`, `"yes"`, `1` → `true`; `false`, `"false"`, `"no"`, `0` → `false`; else `default` | | `select` | must be one of `options`, else `default` | | `color` | must be `#` + 3-8 hex digits or `theme:`, else `default` | | Setting not declared in the schema | dropped; templates never see it | So a stored `padding` of `"999"` with `max: 240` renders as `240`, and an `align` of `"diagonal"` renders as the default `"start"`. A bad value never breaks the page. Values written directly on `hk-*` tags are not coerced by your schema; each tag checks its own props (a `size` outside its range is clamped, an unknown `variant` falls back to the tag default). See the [tag reference](https://hekana.dev/reference/tags). ## Template default sections A template's `` lists the sections a new page starts with, in order: ```html {"sections":[ {"type":"hero","settings":{"title":"Welcome","subtitle":"A theme built with Sneferu"}}, {"type":"featured","settings":{"title":"Featured products"}} ]} ``` - `type` must be a file in `sections/` (SN3020); each entry is `{ "type", "settings"? }` (SN3026). - `settings` here go through the same coercion as stored values. - A default section may also list `blocks` (see below). They are what `sneferu dev` shows. - Once a merchant edits the page, their saved list of sections replaces the defaults. ### Default blocks A default section can start with blocks. Each entry is `{ "type", "settings"? }`: ```html {"sections":[ {"type":"faq","settings":{"title":"Questions"},"blocks":[ {"type":"question","settings":{"text":"How long does shipping take?"}}, {"type":"question","settings":{"text":"Can I return an item?"}} ]} ]} ``` - Each `type` must be a file in `blocks/` (SN3028). - The section must allow the type in `blocks.allowed`, and the list may not be longer than `blocks.max` (SN3029). - The blocks render with the ids `default-
-`, for example `default-0-1`. ## Text in two languages Add `"localized": true` to a `text`, `textarea` or `rich_text` setting and the merchant can store one value per language: ```json "title": { "type": "text", "label": "Title", "localized": true, "default": { "ar": "أهلًا بك", "en": "Welcome" } } ``` Templates still read `{{ section.settings.title }}` and get a plain string: the value for the page's language, else the other language, else the default, else an empty string. A plain string saved by an older editor is used as it is in both languages. `maxLength` applies to the resolved string. Theme-level settings in `theme.json` can be localized the same way. ## Blocks A customer quotes section: the merchant adds as many quotes as they like, up to six, and edits each one. Each quote is a block. ```html title="blocks/quote.snf"

{{ block.settings.text }}

{ "name": "Quote", "version": 1, "settings": { "text": { "type": "textarea", "label": "Quote", "default": "Delivery was fast and the packaging was perfect.", "maxLength": 280 }, "author": { "type": "text", "label": "Name", "default": "Mona, Alexandria", "maxLength": 60 } } } ``` ```html title="sections/testimonials.snf" {% if section.blocks %} {% for block in section.blocks %} {% endfor %} {% else %} {% endif %} { "name": "Testimonials", "version": 1, "settings": { "heading": { "type": "text", "label": "Heading", "default": "What our customers say" } }, "blocks": { "allowed": ["quote"], "max": 6 }, "presets": [ { "name": "Three quotes", "blocks": [ { "type": "quote" }, { "type": "quote" }, { "type": "quote" } ] } ] } ``` ```css title="assets/quote.css" .quote { margin: 0; font-size: 17px; line-height: 1.6; } ``` ![A heading "What our customers say" and three bordered quote cards side by side](https://hekana.dev/developers/examples/testimonials@2x.webp "With three quote blocks, 1080 px wide. Rendered by the Sneferu runtime with three blocks passed in.") ![The same heading with the sentence "No quotes yet. Add one in the editor." under it](https://hekana.dev/developers/examples/testimonials-empty@2x.webp "With no blocks. A template can seed default blocks (see Default blocks above), so a new store starts with three.") #### What each line does In `blocks/quote.snf`: 1. **Lines 1-6** one quote: a bordered card with the quote and the customer's name. A block reads its own settings as `block.settings.`. 2. **Lines 7-26** the two settings of one quote: the text and the name. In `sections/testimonials.snf`: 1. **Line 4** `{% if section.blocks %}` is true when the merchant has added at least one quote. 2. **Lines 6-8** `{% for block in section.blocks %}` goes through the quotes in the merchant's order, and `` draws each one with `blocks/quote.snf`. 3. **Lines 10-11** what shows when there are no quotes yet. 4. **Lines 26-29** `"blocks"` tells the builder which block types this section takes (`quote`) and how many (6 at most). 5. **Lines 30-45** `"presets"`: when the merchant adds this section, the builder offers to start it with three quotes. How it works: - `section.blocks` is the list of the merchant's blocks in order, hidden ones removed. Each has `id`, `type` and coerced `settings`. - `` renders `blocks/.snf` with `block` in scope, wrapped in `
`. The section's scope (`section`, `store`, …) stays visible. - A block whose type is not in the theme is skipped with SN4012; the rest of the section renders. - `blocks.allowed` (which block types the editor offers) and `blocks.max` are read by the editor. The platform also caps a section at 50 blocks and a page at 60 sections. - `presets` are starting configurations the editor offers when the section is added. They are stored with the theme and used by the builder; the CLI does not render them. - Because default sections cannot carry blocks, `sneferu dev` shows a block section with no blocks. Design that state, as lines 10-11 do: it is also what a merchant sees right after adding the section. #### Try it 1. Save the three files and add the key to `locales/en.json`: `"no_quotes_yet": "No quotes yet. Add one in the editor."`. 2. Add `{"type":"testimonials"}` to `templates/index.snf`. 3. Run `npx sneferu validate` and look at the preview: you see the empty state. ## Snippets vs blocks | Use | When | | - | - | | Block | The merchant decides how many there are and edits each one | | Snippet (`{% render %}`) | You repeat markup in code, with values you pass in | `{% render 'price-tag', amount: p.price %}` renders `snippets/price-tag.snf` with `amount` in scope, on top of the caller's scope. Snippets nest up to 8 deep and may not render each other in a cycle (SN3027). --- Source: https://hekana.dev/guide/data-and-expressions # 4. Data and expressions Templates read data with `{{ expression }}` and control output with `{% if %}`, `{% for %}` and `{% render %}`. There is no arithmetic, no assignment and no function calls besides the lookups below: a template cannot run code. The full grammar is in [language.md](https://hekana.dev/reference/language). ## What a template can read | Name | Where | Contains | | - | - | - | | `store` | everywhere | the store, e.g. `store.name` | | `settings` | everywhere | theme settings from `theme.json` `settings`, defaults filled in and coerced: `settings.accent` | | `section` | sections, their blocks and snippets | `section.id`, `section.type`, `section.settings.*`, `section.blocks` | | `block` | blocks | `block.id`, `block.type`, `block.settings.*` | | `locale` | everywhere | `ar` or `en` | | `dir` | everywhere | `rtl` (Arabic) or `ltr` | | `forloop` | inside `{% for %}` | `forloop.index` (from 1), `forloop.first`, `forloop.last` | | loop variable, `render` arguments | where you name them | e.g. `p` in `{% for p in … %}` | ### Lookups Lookups take **literal** arguments (`collection('featured')`). Some take more: `collection()` also takes a `collection` setting, so the merchant can choose the collection (see [Let the merchant choose a collection](#let-the-merchant-choose-a-collection)), `product()` a `product` setting, `menu()` a `menu` setting (see [Menus](https://hekana.dev/guide/menus)), and `url()` takes any one expression (see [Links](#links)). Any other argument, such as `collection(product.title)`, is SN1105. | Lookup | Returns | | - | - | | `collection('featured')` | the collection: `.title`, `.products`. A missing collection is `{ products: [] }`, never an error | | `collection(section.settings.list)` | the collection the merchant picked in a `collection` setting; an empty collection when none is picked | | `collections()` | the store's categories (at most 50), see [List the store's categories](#list-the-stores-categories) | | `product('handle')` | the product, or nothing | | `menu('main')` | the store's menu: `.title`, `.items` (each `{ title, url, active, child_active, children }`). Takes a handle or a `menu` setting: `menu(section.settings.menu)`. A missing menu is `{ handle, title: '', items: [] }`, never an error. See [Menus](https://hekana.dev/guide/menus) | | `t('shop_now')` | the text from `locales/.json`, then the default locale, then the key itself | | `settings('accent')` | one theme setting (same as `settings.accent`) | | `asset_url('logo.svg')` | the URL of `assets/logo.svg` | | `url('/products')` | a link for this page: `/en/products` on an English page, `/products` on an Arabic one. Takes a literal or any expression: `url(section.settings.link)` | ### Product fields A product (from `collection(…).products` or `product(…)`) has: | Field | Example | | - | - | | `id` | `"8f1c…"` (stable; do not show it) | | `handle` | `ps-card` | | `title` | `PlayStation gift card` | | `url` | `/products/ps-card` | | `price` | `52000` (minor units: piastres; 52000 = 520.00 EGP) | | `compare_at` | `60000` or nil (the price before a discount) | | `image` | `{ url: "https://…" }` | | `available` | `true` when it can be bought | | `description` | `

The code arrives by email.

`: sanitised HTML in the page's language. `{{ product.description }}` prints it escaped, as text; output it as HTML with `` | | `excerpt` | `The code arrives by email.` (plain text, at most 200 characters) | | `kind` | `physical` or `digital` | | `digital_kind` | `code`, `file` or `manual` for a digital product; nil for a physical one | | `tags` | `["gift-card", "gaming"]` (at most 20, each at most 40 characters) | | `category` | `{ handle: "gift-cards", title: "Gift cards", url: "/category/gift-cards" }`, or nil | ```html {% if p.kind == 'digital' %}{{ 'instant' | t }}{% endif %}

{{ product.excerpt }}

``` In `sneferu dev` every sample product has all of these fields. ## Output ```html

{{ section.settings.title }}

{{ store.name }} · {{ t('shop_now') }}

{{ p.title }} ``` - Output is escaped for where it lands: text, attribute or URL. You cannot inject HTML through a setting. - URL attributes (`href`, `src`, …) go through a safe-URL check: `javascript:` and other unsafe values become `#`. - Dynamic `class` and `id` are reduced to letters, digits, `-`, `_` and spaces. - Strings, numbers and booleans print as text (`true`); objects and arrays print nothing. Missing data prints nothing; it never throws. - An attribute can mix text and expressions: `class="tier {{ section.settings.size }}"`. - On `hk-*` tags, an attribute that is exactly one `{{ }}` passes the raw value (a number, an array of products, an object). Mixed text makes a string. ## Expressions | Form | Example | | - | - | | Literals | `'text'`, `"text"`, `42`, `true`, `false`, `nil` | | Property | `section.settings.title`, `p.image.url` | | Index | `collection('featured').products[0].title` | | Size | `section.blocks.size`, `p.title.size` | | Compare | `==`, `!=` (strict), `>`, `<`, `>=`, `<=` (numbers only; false otherwise) | | Logic | `and`, `or`, `not`, parentheses | | Filters | `value \| filter`, `value \| filter: arg1, arg2` | ```html {% if p.compare_at > p.price %}{% endif %} {% if section.settings.layout == 'wide' and not section.settings.compact %}…{% endif %} ``` `>`/`<` compare numbers only: `'10' > 9` is false. Prices are numbers, so price comparisons work. ## Truthiness In `{% if %}`, `{% elsif %}`, `and`, `or`, `not`: | False | True | | - | - | | `nil` (missing), `false`, `0`, `''` (empty string), `[]` (empty array) | everything else, including `'0'`, `' '` and `{}` | So `{% if collection('featured').products %}` is false for an empty or missing collection, and `{% if section.blocks %}` is false when the merchant added no blocks. Use that for empty states: ```html {% if collection('featured').products %} {% else %} {% endif %} ``` `{% else %}` comes once, last (SN1109). ## Filters | Filter | Input → output | | - | - | | `money` | price in minor units → formatted price in the store currency. `{{ 129000 \| money }}` → `EGP 1,290.00`. Other currencies use the ISO code: `USD 1,290.00`. On an Arabic page the currency comes after the number. Western digits. Options: see [Money formats](#money-formats) | | `img` | image (object with `url`, or a URL) → image URL, optionally sized: `{{ p.image \| img: 400 }}` | | `date` | ISO date → long date: `{{ '2026-10-10' \| date }}` → `10 October 2026` | | `t` | key → translation: `{{ 'shop_now' \| t }}` (same as `t('shop_now')`) | | `upcase` / `downcase` | `{{ 'egp' \| upcase }}` → `EGP` | | `truncate` | `{{ p.title \| truncate: 20 }}` → at most 20 characters, ending in `…` (default 50) | | `default` | `{{ section.settings.eyebrow \| default: 'New' }}` → the fallback when the value is nil or `''` | | `size` | `{{ section.blocks \| size }}` → `3` (arrays and strings; `0` otherwise) | | `first` / `last` | first / last array item: `{{ tags \| first }}`. A filter result cannot be followed by `.property`; for a field of the first product write `collection('featured').products[0].title` | | `join` | `{{ tags \| join: ' · ' }}` → `a · b` (default separator `, `). On a missing value it prints its argument, so guard it: `{% if tags %}…{% endif %}` | | `escape` | converts to a string (output is escaped anyway) | | `json_attr` | value → JSON for a `data-*` attribute: `data-ids="{{ ids \| json_attr }}"` | | `tag_values` | tags → the values of the tags written `prefix:value`: `{% for n in product.tags \| tag_values: 'top' %}…{% endfor %}` lists `Bergamot` for the tag `top:Bergamot`. The prefix match ignores case; values are trimmed; empty and repeated ones are dropped; at most 20. A value may carry its Arabic after a bar (`top:Bergamot\|برغموت`): Arabic pages show the part after the bar, English pages the part before it | Filters chain left to right: `{{ p.title | truncate: 30 | upcase }}`. Prices are always integers in minor units. Never divide or format them yourself; use `money` or ``. Dates and prices always use Western digits, in every locale, matching the rest of Hekana. ### Money formats `money` takes two optional arguments: the format and the unit. | Expression | Arabic page | English page | | - | - | - | | `{{ 129000 \| money }}` | `1,290.00 ج.م` | `EGP 1,290.00` | | `{{ 129000 \| money: 'whole' }}` | `1,290 ج.م` | `EGP 1,290` | | `{{ 129050 \| money: 'whole' }}` | `1,290.50 ج.م` | `EGP 1,290.50` | | `{{ 120 \| money: 'whole', 'major' }}` | `120 ج.م` | `EGP 120` | - Format: `'decimal'` (the default) always prints two decimals; `'whole'` drops `.00` when the amount is a whole pound and keeps two decimals otherwise. - Unit: `'minor'` (the default) reads the input as piastres; `'major'` reads it as pounds. Product prices are always minor units, so `'major'` is for numbers you wrote yourself. The unit is the second argument, so `'major'` always needs a format before it: `money: 'decimal', 'major'` or `money: 'whole', 'major'`. To make `whole` the default for the whole theme, set it in `theme.json`: ```json "money": { "format": "whole" } ``` It applies to `money` without a format, to `` and to the price inside `` and ``. `` (or `"whole"`) overrides it for one tag, and `money: 'decimal'` for one expression. With the `whole` format `` wraps the number in ``, so it reads correctly next to `ج.م` on Arabic pages; with the default format its markup is unchanged. Anything other than `decimal` or `whole` in `theme.json` is SN3033. ## Links A link written in a template is used as written: `href="/products"` goes to the Arabic page even when the shopper is on English. Wrap store links in `url()` so they keep the page's language: ```html {{ t('all_products') }} {{ section.settings.label }} ``` | On an English page | Result | | - | - | | `url('/products')` | `/en/products` | | `url('/en/products')` | `/en/products` (already prefixed) | | `url('https://example.com')`, `url('mailto:…')`, `url('tel:…')`, `url('#reviews')` | unchanged | | `url('javascript:…')`, `url('//evil.example')`, an empty value | `#` | On an Arabic page store paths are unchanged (Arabic is the default language and has no prefix). On the store, Hekana also adds whatever the store's address needs; the result is always checked with the same URL rules as `href` (see [Security and limits](https://hekana.dev/guide/security-and-limits)). `url()` takes exactly one argument. `` does this for you: its link always follows the page's language. Product and category URLs (`p.url`, `c.url`) already include the prefix, so they do not need `url()`. ## Loops A short list of the first three products of a collection, numbered, with a "Sale" label on any product that has a lower price than before. ```html title="sections/on-sale.snf" {% for p in collection('featured').products limit: 3 %} {% if p.compare_at > p.price %} {% endif %} {% endfor %} { "name": "Product list", "version": 1, "settings": { "heading": { "type": "text", "label": "Heading", "default": "Today's picks" } } } ``` ![A "Today's picks" heading and three numbered product rows; the first has a Sale badge, EGP 520.00 and EGP 600.00 crossed out](https://hekana.dev/developers/examples/on-sale@2x.webp "1080 px wide, English. The product names are the dev preview's sample data.") #### What each line does 1. **Line 4** `{% for p in collection('featured').products limit: 3 %}` repeats lines 5-15 once for each of the first three products. Inside, `p` is the current product. 2. **Line 7** `{{ forloop.index }}` is 1, 2, 3 and `{{ p.title }}` is the product's name. 3. **Lines 9-11** `p.compare_at > p.price` is true when the product had a higher price before; only then the "Sale" badge shows. Its words come from the `sale` key in your locale files, and its look from the badge rules in the starter's `assets/theme.css`. 4. **Line 12** `` prints the price, and the old price crossed out when there is one. 5. **Line 16** `{% endfor %}` ends the repeated part. | Rule | Value | | - | - | | Default limit (no `limit:`) | 50 items | | Largest `limit:` | 250 (a limit below 1 or above 250 is SN2007) | | Iterations per page, all loops together | 2,000 | | Looping over something that is not an array | renders nothing | | `forloop.index` | 1, 2, 3 … | | `forloop.first` / `forloop.last` | `true` on the first / last item | There is no `forloop.length`; use `{{ list | size }}` or `list.size`. ### Loop limits from settings `limit:` may also read a `number` or `range` setting, so the merchant chooses how many items show: ```html {% for p in collection('featured').products limit: section.settings.count %} ``` The setting is `section.settings.`, `block.settings.` or `settings.`; anything else is SN1105, and a setting that does not exist or is not a `number` or `range` is SN2012. At render time a value above 250 uses 250, and a value below 1 or not a whole number (empty, text, `2.5`, `0`, `-3`) uses the default limit, 50. ## Let the merchant choose a collection A `collection` setting gives the merchant a collection picker in the builder. Pass the setting to `collection()` and the section shows whatever they pick: ```html title="sections/collection-shelf.snf" {% for p in collection(section.settings.list).products limit: section.settings.count %} {% endfor %} { "name": "Collection shelf", "version": 1, "settings": { "list": { "type": "collection", "label": "Collection", "default": "featured" }, "count": { "type": "range", "label": "Products", "min": 1, "max": 12, "default": 4 } } } ``` | Rule | Value | | - | - | | Argument | a handle in quotes, or `section.settings.`, `block.settings.`, `settings.` | | The setting | must exist and be of type `collection` (SN2012). In a section file `block.settings.*` is not checked, because the block type is only known on the page | | Nothing picked (empty value) | an empty collection: the loop renders nothing, nothing is fetched | | Before rendering | the store fetches every collection the page can show, the picked ones and their defaults included, up to 8 per page | ## List the store's categories `collections()` takes no arguments and returns the store's categories, at most 50, for a category menu or a grid of category tiles: ```html ``` | Field | Example | | - | - | | `handle` | `gift-cards` | | `title` | `Gift cards` (in the page's language) | | `url` | `/en/category/gift-cards` (with the language prefix of the page) | | `image` | `{ url: "https://…" }`, or nil when the category has no image | | `count` | `6` (the number of products) | A store with no categories returns an empty list. In `sneferu dev` the sample store has three categories: `new`, `gift-cards` and `subscriptions`. ## Translations ```html {{ 'free_delivery' | t }} ``` Use `t()` for interface words and settings for merchant content. A page uses `locales/.json` and falls back to the default locale for any missing key. ## Snippet arguments ```html {% render 'plan-card', name: section.settings.plan1_name, price: section.settings.plan1_price, best: section.settings.best == '1' %} ``` The [subscription plans](https://hekana.dev/guide/cookbook#subscription-plans) in the cookbook use this. Arguments are `key: expression`, separated by commas (commas inside quotes are fine: `label: 'a, b'`). The snippet sees them plus the caller's scope. --- Source: https://hekana.dev/guide/security-and-limits # 6. Security and limits A theme is untrusted input that runs on every store that installs it. Sneferu is built so that a theme **cannot** run code, read data it was not given, or make one page slow for everyone. The compiler enforces the rules below, and the runtime checks the compiled artifact again before rendering it (SN9xxx), so a hand-edited artifact cannot bypass them. ## Elements Plain HTML elements are checked against an **allow-list**. Anything not on it is SN2001, including custom elements. | Group | Allowed | | - | - | | Sectioning and grouping | `address article aside blockquote dd details div dl dt figcaption figure footer h1 h2 h3 h4 h5 h6 header hgroup hr li main menu nav ol p pre search section summary ul` | | Text | `a abbr b bdi bdo br cite code data del dfn em i ins kbd mark meter progress q rp rt ruby s samp small span strong sub sup time u var wbr` | | Media | `audio img picture source track video` | | Tables | `caption col colgroup table tbody td tfoot th thead tr` | | SVG (presentation only) | `svg g path circle ellipse line polyline polygon rect text tspan defs lineargradient radialgradient stop clippath mask pattern symbol use title desc image` | Never allowed: `script`, `style`, `iframe`, `object`, `embed`, `base`, `link`, `meta`, `form`, `animate`, `set`, `animatemotion`, `animatetransform`, `foreignobject`, `plaintext`, `xmp`, `noembed`, `noscript`, `frameset`, `frame`, `template`, `slot`, `portal`. Form controls (`button`, `input`, `select`, `textarea`) are not on the list. Form controls exist only through `hk-search-form` (see [Icons and search](https://hekana.dev/guide/icons-and-search#search-form)), which renders a fixed GET form to the store's search page. ## What a theme cannot do (yet) | Not available | Instead | | - | - | | JavaScript: no `