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

> Hard rules from shipping live Nunjucks themes — autoescape, Alpine x-data, cartPage, lean cart API, visibility.

# Nunjucks Theme Pitfalls

Shipping checklist for Nunjucks theme authors. These caused real production bugs.

Also see [Nunjucks Theme API](/storefront/nunjucks), [Products & Catalog](/storefront/products), [Shortcodes & Tags](/storefront/shortcodes), [Policy & Legal Pages](/storefront/policy-pages).

***

## 1. `autoescape` is `false`

Join Markt runs Nunjucks with **`autoescape: false`**.

Do **not** put HTML fields (`product.description`, `seo_description`) into attributes:

```njk theme={null}
{# BAD — quotes/HTML break the tag; text leaks above the navbar #}
<meta name="description" content="{{ seo_description }}">
```

Do instead — strip HTML for meta, or encode with `json`:

```njk theme={null}
<meta name="description" content={{ (seo_description | striptags) | json }}>
```

***

## 2. Never put `| json` inside `x-data="..."`

```njk theme={null}
{# BAD — quotes break the attribute and dump JSON on the page #}
<div x-data="productBuy({ name: {{ product.name | json }}, variants: {{ product.variants | json }} })">
```

Put boot data in a JSON script; read it in JS / `x-init`. Prefer **lean** fields (no description HTML):

```njk theme={null}
<script type="application/json" id="product-buy-boot">
  {{ { id: product.id, name: product.name, slug: product.slug, variants: product.variants } | json }}
</script>
<div x-data="productBuy()" x-init="bootFromJson('product-buy-boot')">
```

***

## 3. Never dump full `products` into `<head>` scripts

Full catalog JSON (with HTML descriptions) in `<head>` can break out of `<script>`.

* Lean only: `id`, `name`, `slug`, `image`, `minPrice` / `maxPrice`, variant `id` / `price` / `name`
* Prefer end of `<body>`

***

## 4. Do not re-register `Alpine.data('cartPage')`

`components/cart-page.njk` → platform injects **`markt-cart-client`**, which owns `cartPage` + `checkout()`.

A theme stub registered later **overwrites** it → cart checkout dies.

**Do not** redefine `cartPage`. Use the injected client only.

***

## 5. Cart API payload must be lean

`GET /api/v1/cart?storeId=&shopId=&cart=` — `cart` must be:

```json theme={null}
[{ "productId": "cuid", "variantId": "0", "quantity": 1 }]
```

Do **not** nest full `product` / `variant` blobs (URL size / empty parse).

Do **not** replace `localStorage` / `appCart` with `[]` when `fullCart` is empty unless the platform explicitly cleared lines — otherwise the drawer looks empty after add-to-cart.

`POST /api/v1/checkout` needs real `productId` (+ `variantId`, `quantity`). Passing only `{ name }` caused checkout **"Unknown Product"** (fixed in platform `createV1Checkout` — still send ids from themes).

***

## 6. Visibility rules

| Value                 | Catalog | Buy                                |
| --------------------- | ------- | ---------------------------------- |
| `public` / `unlisted` | show    | yes                                |
| `on-hold`             | show    | **no** — disable add-to-cart / buy |
| `private`             | hide    | no                                 |

Treat `on-hold` / `private` in the theme even if cache briefly leaks rows. Public catalog cache is \~**120s** — visibility changes can lag until revalidate.

***

## 7. Catalog data sources

| Need       | Use                                                    |
| ---------- | ------------------------------------------------------ |
| Main grids | `catalogItems` or `standaloneProducts`                 |
| Groups     | `groups` + `getItemsByIds(products, group.productIds)` |
| Card price | `minPrice` / `maxPrice`                                |
| PDP price  | `variants[].price`                                     |
| Rating     | format `store.stats.averageRating` to **1 decimal**    |

Do not loop raw `products` for homepage cards when groups exist (duplicates).

***

## 8. Checkout cart lines (platform)

`createV1Checkout` must pass full cart lines into `createOrder`:

`productId`, `variantId`, `name`, `quantity`, `price` (line id may be `productId:variantId`).

Name-only cart lines → **Unknown Product** with a correct total. Fixed on the platform; theme authors should still always send product ids from `appCart.items`.

***

## 9. Policy / legal templates

Seller text lives in Configure → store policies. Filenames must match the router:

| URL                             | Template file                  | Body                     |
| ------------------------------- | ------------------------------ | ------------------------ |
| `/privacy-policy`               | `templates/privacy-policy.njk` | `store.policies.privacy` |
| `/terms` or `/terms-of-service` | `templates/terms.njk`          | `store.policies.terms`   |
| `/refund-policy`                | `templates/refund-policy.njk`  | `store.policies.refund`  |

Do **not** use Vite paths (`/privacy`, `/refunds`) or wrong filenames (`privacy.njk`, `refunds.njk`) — they will not resolve.

Full guide: [Policy & Legal Pages](/storefront/policy-pages).
