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

# Nunjucks Theme API

> Context, filters, and cart endpoints for Nunjucks storefronts.

# Nunjucks Theme API

**Version:** `1.0.0` (`MARKT_NUNJUCKS_API_VERSION`)

Framework-agnostic Nunjucks engine for MARKT storefronts. Themes use standard Nunjucks (`extends`, `include`, `macro`, `for`, `if`) plus the stable context below.

For product grids and tags, also read [Shortcodes & Tags](/storefront/shortcodes) and [Build a Custom Theme](/storefront/custom-themes).

**Before shipping a live Nunjucks theme**, read [Nunjucks Theme Pitfalls](/storefront/nunjucks-pitfalls) (`autoescape`, Alpine `x-data`, `cartPage`, lean cart API, visibility).

Legal pages: [Policy & Legal Pages](/storefront/policy-pages) — `store.policies.*` and files `privacy-policy.njk` / `terms.njk` / `refund-policy.njk`.

***

## Architecture

```text theme={null}
Theme files (.njk, settings.json, assets/)
        │
        ▼
  MarktNunjucksLoader (sandboxed paths)
        │
        ▼
  Extension preprocessors (optional)
        │
        ▼
  Nunjucks compile + render
        ├── MARKT core filters/globals
        └── Extension tags/filters (optional)
        │
        ▼
  HTML (+ SEO/i18n on live requests)
```

| Layer       | Location                           |
| ----------- | ---------------------------------- |
| Core engine | `lib/builder/nunjucks/core/`       |
| Public API  | `lib/builder/nunjucks/api/`        |
| Extensions  | `lib/builder/nunjucks/extensions/` |

***

## Lifecycle

| Phase       | What happens                                                                 |
| ----------- | ---------------------------------------------------------------------------- |
| **Import**  | ZIP validated; engine detected; `settings.json` merged from templates        |
| **Preview** | Compiled to RAM cache (content hash + assets + extensions)                   |
| **Live**    | Route → `buildMarktStorefrontContext()` → render → SEO/i18n                  |
| **Publish** | Revision hash; HTML cached; assets at `/markt-theme-assets/live/{revision}/` |

***

## Context objects (stable v1)

| Object                                                  | Description                                                                                          |
| ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `store`                                                 | `id`, `name`, `description`, `url`, `logo`, `favicon`, `stats`, `policies`, `integrations`, `social` |
| `products`                                              | All catalog products (including group members)                                                       |
| `standaloneProducts`                                    | Products with `groupId == null` — use for main grids                                                 |
| `groups`                                                | Storefront groups (`id`, `name`, `minPrice`, `maxPrice`, `productIds`, …)                            |
| `catalogItems`                                          | Mixed catalog: groups first, then standalone (`kind: 'group' \| 'product'`)                          |
| `product`                                               | Current product or `null`                                                                            |
| `reviews` / `reviewsPaginator`                          | Feedback                                                                                             |
| `categories`                                            | Category links                                                                                       |
| `productsPaginator`                                     | Paginated **standalone** products (excludes grouped)                                                 |
| `customer`                                              | Logged-in customer or `null`                                                                         |
| `invoices`, `tickets`, `ticket`                         | Customer portal                                                                                      |
| `currency`, `currencyRates`, `currencySymbols`          | Money display                                                                                        |
| `productUpsells`                                        | Related products                                                                                     |
| `helpers.components.products.getItemsByIds(items, ids)` | Filter by id/slug                                                                                    |
| `settings`, `global`                                    | Theme `settings.json`                                                                                |
| `templateName`, `templateContent`                       | Active template + body                                                                               |
| `components_order`                                      | Builder component order                                                                              |
| `path`, `locale`, `t()`                                 | Routing + i18n                                                                                       |
| `canonical_url`, `seo_title`, `seo_description`         | SEO                                                                                                  |

Types: `lib/builder/nunjucks/api/types.ts`

### `MarktProduct` (summary)

Typical fields: `id`, `slug`, `name`, `description`, `image`, `minPrice` / `maxPrice` (not a top-level `price`), `visibility`, `stock` (`-1` = unlimited), `variants`, `groupId`.

### Product groups

* Prefer `catalogItems` / `standaloneProducts` for homepage grids — never list every `products` entry as a card when groups exist.
* Group members: `helpers.components.products.getItemsByIds(products, group.productIds)`.

### On-hold (`visibility`)

| Value                 | In catalog?      | Purchase?                               |
| --------------------- | ---------------- | --------------------------------------- |
| `public` / `unlisted` | Yes              | Yes                                     |
| `on-hold`             | Yes              | **No** — disable cart / buy; show badge |
| `private`             | Never in payload | —                                       |

```njk theme={null}
{% set onHold = product.visibility == 'on-hold' %}
{% if onHold %}
  <button disabled>Unavailable</button>
{% else %}
  <button @click="appCart.add(product.id, variantId, qty)">Add to cart</button>
{% endif %}
```

Checkout / embed APIs also reject on-hold — UI disable is still required.

***

## Filters

| Filter                          | Example                            |
| ------------------------------- | ---------------------------------- |
| `assetUrl`                      | `{{ "pro.css" \| assetUrl }}`      |
| `shopUrl`                       | `{{ "/cart" \| shopUrl }}`         |
| `apiUrl`                        | `{{ "v1/cart" \| apiUrl }}`        |
| `imageUrl`                      | Absolute image URL                 |
| `formatPrice`                   | `{{ 9.99 \| formatPrice("USD") }}` |
| `formatDate` / `formatDateTime` | Locale dates                       |
| `json`                          | Safe JSON for scripts              |
| `replace`                       | String replace                     |
| `hex_to_rgb`                    | `{{ "#6366f1" \| hex_to_rgb }}`    |
| `themeColor`                    | Accent from settings               |
| `ytEmbedLink`                   | YouTube → embed URL                |

***

## Tags

| Tag               | Example                               |
| ----------------- | ------------------------------------- |
| `markt_component` | `{% markt_component "navbar" %}`      |
| `markt_snippet`   | `{% markt_snippet "meta-tags.njk" %}` |

***

## Globals

| Global        | Example                      |
| ------------- | ---------------------------- |
| `range`       | `{% for i in range(0, 5) %}` |
| `formatPrice` | `{{ formatPrice(19.99) }}`   |

***

## Minimal theme layout

```text theme={null}
layouts/base.njk
templates/home.njk
snippets/header.njk
macros/button.njk
assets/style.css
settings.json
```

```njk theme={null}
<h1>{{ store.name }}</h1>
<a href="{{ '/products' | shopUrl }}">Products</a>
<link href="{{ 'style.css' | assetUrl }}" rel="stylesheet" />

{% for product in products %}
  <a href="{{ ('/product/' ~ product.slug) | shopUrl }}">
    {{ product.name }} — {{ product.minPrice | formatPrice(currency) }}
  </a>
{% endfor %}
```

***

## Cart

| Step      | Mechanism                                               |
| --------- | ------------------------------------------------------- |
| Add       | `appCart.add(productId, variantId, quantity)` on `#app` |
| Badge     | `appCart.countWithQuantities`                           |
| Cart page | `GET /api/v1/cart?storeId=&shopId=&cart=` → `fullCart`  |
| Sync      | Response `cart` replaces `appCart` with product cuids   |

`MarktCartItem` shape (camelCase):

```json theme={null}
{
  "productId": "cuid",
  "variantId": "0",
  "quantity": 1,
  "currency": "USD",
  "product": { "name": "…", "slug": "…", "image": "…", "price": 9.99, "url": "…" },
  "variant": { "name": "…", "price": 9.99, "quantityMin": 1, "quantityMax": 10 }
}
```

Themes with `components/cart-page.njk` get `markt-cart-client` injected for quantity/remove/checkout sync.

***

## settings.json

```json theme={null}
{
  "global": {
    "properties": { "theme_color": "#6366f1" },
    "components": {}
  },
  "templates": {
    "shop": {
      "layout": "master",
      "components": {
        "hero": { "type": "hero", "properties": {} }
      },
      "components_order": ["hero"]
    }
  }
}
```

Missing template entries are auto-discovered from `templates/*.njk`.

***

## Extensions (optional)

```ts theme={null}
import { registerNunjucksExtension } from '@/lib/builder/nunjucks/extensions/registry'

registerNunjucksExtension({
  id: 'acme.marketplace',
  name: 'Acme compatibility',
  detect: (files) => Boolean(files['acme.json']),
  preprocess: (source) => source,
  register: ({ env, renderTemplate }) => { /* tags/filters */ },
  enrichContext: (ctx) => ({ ...ctx, acmeData: {} }),
})
```

Built-in **`markt.sellauth`** activates for legacy `{% render_component %}`, `{% render_snippet %}`, `apiInternalUrl`, or `schema.json`. Native themes use Join Markt tag names only.

***

## Stability

* **Minor/patch:** additive fields/filters/extensions
* **Major:** removing/renaming core context keys, filters, or globals

***

## Validation

```bash theme={null}
npx tsc --noEmit
npm test -- tests/builder
```
