> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shoppex.io/llms.txt
> Use this file to discover all available pages before exploring further.

# ThemeDocument structure

> Every field, page ID, and URL route inside a ThemeDocument, with pointers to blocks and design tokens.

A ThemeDocument is one JSON object: a name, a map of pages, blocks that repeat on every page, and one theme object for tokens.

<Note>
  This page follows the ThemeDocument schema in `packages/builder-contracts` and the route resolver that serves it. It covers the Easy lane only. See [Themes](/storefront/themes) for how Easy compares to an Advanced code storefront.
</Note>

## The shape of a document

| Field          | Type            | Holds                                                                                                                          |
| -------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `version`      | number          | Schema version. The parser accepts version 1 on input and always writes out version 2.                                         |
| `name`         | string          | Display name of the look, for example "Default" or "Starlight".                                                                |
| `pages`        | object          | A map from page ID to page. See [Page IDs](#page-ids).                                                                         |
| `globalBlocks` | array of blocks | Blocks that render on every page: header, footer, and the cart drawer.                                                         |
| `theme`        | object          | Scheme name, style slots, fonts, presets, and custom CSS. See [Design tokens and style slots](#design-tokens-and-style-slots). |

A minimal document looks like this:

```json theme={"system"}
{
  "version": 2,
  "name": "Default",
  "pages": {
    "home": {
      "slug": "/",
      "title": "Home",
      "blocks": [
        {
          "id": "blk_hero",
          "type": "hero",
          "variant": "split-right",
          "visible": true,
          "settings": { "hero.title": "Premium digital goods" }
        }
      ]
    }
  },
  "globalBlocks": [
    { "id": "blk_header", "type": "header", "visible": true, "settings": {} },
    { "id": "blk_footer", "type": "footer", "visible": true, "settings": {} }
  ],
  "theme": {
    "scheme": "default",
    "style_slots": { "color.primary": "#7C3AED" },
    "fonts": { "heading": "Inter", "body": "Inter" },
    "presets": []
  }
}
```

<Note>
  A document has hard limits. Element nesting can go up to 12 levels deep, and a single node can have up to 200 children. A page renders up to 2,000 nodes. That count includes its own blocks plus every global block. The whole document, as serialized JSON, cannot pass 2,000,000 bytes.
</Note>

## Page IDs

Every entry in `pages` has an ID. Some IDs are built in and reserved. A custom page created in the Builder gets its own ID instead, and cannot reuse a reserved one. Shoppex rejects the collision at conversion time.

| Page ID             | Common blocks                                                                                          |
| ------------------- | ------------------------------------------------------------------------------------------------------ |
| `home`              | Content blocks chosen by the merchant, for example `hero`, `products`, `faq`.                          |
| `product`           | `gallery`, a buy box (`buy-box`, `product-buy-box`, or `product-form`), `details`, `related-products`. |
| `cart`              | `page-cart`.                                                                                           |
| `checkout`          | `page-checkout`.                                                                                       |
| `all-products-page` | `page-all-products`.                                                                                   |
| `faq-page`          | `page-faq-page`.                                                                                       |
| `reviews-page`      | `page-reviews-page`.                                                                                   |
| `feedback-page`     | `page-feedback-page`.                                                                                  |
| `status-page`       | `page-status-page`.                                                                                    |
| `tool-page`         | `page-tool-page`.                                                                                      |
| `guide-page`        | `page-guide-page`.                                                                                     |
| `contact-page`      | `page-contact-page`.                                                                                   |
| `support-page`      | A contact form block, for example `contact-form`.                                                      |
| `terms`             | `terms-page`.                                                                                          |
| `privacy-policy`    | `terms-page`. A reserved ID for a store with a separate privacy page.                                  |
| `refund-policy`     | `terms-page`. A reserved ID for a store with a separate refund page.                                   |
| `trust-page`        | `page-trust-page`. Present in some themes only.                                                        |
| `not-found`         | `page-not-found`. Shoppex serves this page when no URL matches.                                        |

A merchant-created custom page can hold four block types only: `custom-html`, `image`, `media-embed`, and `text-block`. Shoppex converts it into a normal ThemeDocument page, keyed by the same ID the merchant chose.

## Which page renders which URL

Shoppex turns an incoming URL path into a page ID before it renders anything.

| URL                                                | Page ID                                                    |
| -------------------------------------------------- | ---------------------------------------------------------- |
| `/`, `/home`, `/index.html`                        | `home`                                                     |
| `/product/:slug`, `/products/:slug`                | `product`, with the slug as the product to show            |
| `/guide/:slug`                                     | `guide-page`, with the slug as the guide to show           |
| `/guide`                                           | `guide-page`                                               |
| `/all-products`, `/products`, `/all-products-page` | `all-products-page`                                        |
| `/cart`                                            | `cart`                                                     |
| `/checkout`                                        | `checkout`                                                 |
| `/faq`                                             | `faq-page`                                                 |
| `/reviews`, `/reviews-page`                        | `reviews-page`                                             |
| `/feedback`                                        | `feedback-page`, or `reviews-page` if that page is missing |
| `/status`                                          | `status-page`                                              |
| `/tool`                                            | `tool-page`, or `tool` if that page is missing             |
| `/contact`                                         | `contact-page`                                             |
| `/support`                                         | `support-page`, or `contact-page` if that page is missing  |
| `/terms`, `/terms-of-service`                      | `terms`                                                    |
| `/privacy-policy`, `/privacy-policy-page`          | `privacy-policy`                                           |
| `/refund-policy`, `/refund-policy-page`            | `refund-policy`                                            |
| `/page/<slug>` or `/page/<id>`                     | The custom page with that slug or ID                       |
| `/<page-id>`                                       | That exact page ID, as a last resort                       |

A page can add more paths in its own `aliases` field. Shoppex checks pages in document order and returns the first match, so an earlier page's alias can win over a later page's own slug.

Shoppex has no direct URL for `not-found`. It serves that page, with a 404 status, only when no other route matches.

## Blocks

| Field         | Type    | Notes                                                                           |
| ------------- | ------- | ------------------------------------------------------------------------------- |
| `id`          | string  | Stable across edits. Cannot be blank, `.`, or `..`, and cannot contain a slash. |
| `type`        | string  | Registered block type, for example `hero` or `products`.                        |
| `variant`     | string  | Optional. Selects a visual variant of the block.                                |
| `visible`     | boolean | Hides the block without removing it from the document. Defaults to `true`.      |
| `settings`    | object  | Block-specific settings.                                                        |
| `customCss`   | string  | Optional. Merchant CSS scoped to this one block. Up to 16,384 bytes (16 KB).    |
| `customClass` | string  | Optional. Extra CSS class names on the block root, separated by spaces.         |
| `children`    | array   | Optional. An element tree rendered inside the block.                            |

See the [Block reference](/storefront/block-reference) for every block type and every setting it reads.

A block's `children` array holds a small set of element types:

* `heading`, `text`, `button`, `image`, `icon`
* `spacer`, `divider`
* `countdown`, `marquee`, `accordion`, `tabs`, `before-after`, `carousel`, `stat-counter`, `flip-card`
* `group`, which nests other elements inside it, up to the depth and count limits above

## Design tokens and style slots

`theme.style_slots` is a flat map from a slot name to a value. A fixed set of core slots covers color, radius, spacing, and type:

| Style slot                                                                             | Value type                                |
| -------------------------------------------------------------------------------------- | ----------------------------------------- |
| `button.radius`, `input.radius`, `card.radius`                                         | responsive number                         |
| `button.background`, `button.foreground`, `button.border`                              | color                                     |
| `input.height`                                                                         | responsive number                         |
| `input.border`, `input.background`, `input.foreground`                                 | color                                     |
| `card.background`, `card.border`                                                       | color                                     |
| `section.padding.y`, `section.padding.x`, `container.width`                            | responsive number                         |
| `color.primary`, `color.accent`, `color.background`, `color.foreground`, `color.muted` | color                                     |
| `link.color`                                                                           | color                                     |
| `button.font.weight`, `typography.heading.weight`                                      | font weight (100 to 900, in steps of 100) |
| `typography.body.size`                                                                 | responsive number                         |

A responsive number takes a `base` value plus optional overrides at the `sm`, `md`, `lg`, and `xl` breakpoints, for example `{ "base": 14, "lg": 16 }`. A slot name that starts with `theme.`, for example `theme.hero.overlayOpacity`, is scheme-specific and free-form. Its value can be a color, a string, a number, a boolean, or a responsive number or string.

`theme.presets` is an array of named style bundles. Each preset targets one element type. It can set values only for these CSS properties:

* `fontSize`, `fontWeight`, `color`, `backgroundColor`
* `padding`, `paddingTop`, `paddingRight`, `paddingBottom`, `paddingLeft`
* `margin`, `marginTop`, `marginRight`, `marginBottom`, `marginLeft`
* `borderRadius`, `letterSpacing`, `lineHeight`, `textTransform`

An element in `children` picks up a preset through its own `preset` field, by ID.

`theme.custom_css` is merchant CSS applied after the scheme's own styles, up to 65,536 bytes (64 KB). Shoppex validates it before saving: `media`, `supports`, `font-face`, `keyframes`, `-webkit-keyframes`, `layer`, `container`, `scope`, `page`, and `property` at-rules are allowed. The `behavior` and `-moz-binding` properties are not, and neither are the `expression()`, `image-set()`, or `-webkit-image-set()` functions.

## Revisions

A publish turns the current draft into a new revision. A published revision never changes again. See [Visual themes](/storefront/visual-themes) for the full editing loop, and [Themes](/storefront/themes) for what a publish does across both storefront lanes.
