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

# Shortcodes & Tags

> Nunjucks tags, helpers, and HTML data-markt shortcodes for products, cart, and checkout.

# Shortcodes & Tags

Join Markt is **not** Shopify Liquid. Developers use four surfaces:

1. **Nunjucks tags** — `{% markt_component %}`, `{% markt_snippet %}`
2. **Nunjucks context + filters** — `products`, `store`, `shopUrl`, `formatPrice`, …
3. **Helpers** — `helpers.components.products.getItemsByIds(items, ids)`
4. **HTML embed attributes** — `data-markt-*` buy buttons on external sites
5. **Vite / React** — TypeScript + storefront API (no template tags)

API version: `MARKT_NUNJUCKS_API_VERSION` `1.0.0` — see [Nunjucks Theme API](/storefront/nunjucks).

***

## Display products (Nunjucks)

### Homepage / products grid (groups-aware)

Prefer `catalogItems` so group cards appear once and grouped products are not duplicated:

```njk theme={null}
<section class="product-grid">
  {% for item in catalogItems %}
    {% if item.kind == 'group' %}
      <article class="product-card">
        <a href="{{ ('/group/' ~ item.group.id) | shopUrl }}">
          <h3>{{ item.group.name }}</h3>
          <p>From {{ item.group.minPrice | formatPrice(currency) }}</p>
        </a>
      </article>
    {% else %}
      {% set product = item.product %}
      <article class="product-card">
        <a href="{{ ('/product/' ~ product.slug) | shopUrl }}">
          {% if product.image %}
            <img src="{{ product.image | imageUrl }}" alt="{{ product.name }}" />
          {% endif %}
          <h3>{{ product.name }}</h3>
          {% if product.visibility == 'on-hold' %}<p>On hold</p>{% endif %}
          <p>{{ product.minPrice | formatPrice(currency) }}</p>
        </a>
      </article>
    {% endif %}
  {% else %}
    <p>No products yet.</p>
  {% endfor %}
</section>
```

Context comes from `buildMarktStorefrontContext()`. Use `standaloneProducts` for ungrouped-only lists; use full `products` for detail pages and group members.

### Featured by id or slug

```njk theme={null}
{% set ids = ['starter-plan', 'pro-plan'] %}
{% set featured = helpers.components.products.getItemsByIds(products, ids) %}

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

### Limit for homepage

```njk theme={null}
{% for product in products.slice(0, 8) %}
  {# first 8 #}
{% endfor %}
```

### Current product page

On `templates/product.njk`, use `product` (or `null` if missing).

***

## Platform tags

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

Legacy imported themes may use `{% render_component %}` / `{% render_snippet %}` via the optional `markt.sellauth` extension — **native themes should use `markt_*` only**.

***

## Core filters

| Filter                          | Example                                        |
| ------------------------------- | ---------------------------------------------- |
| `assetUrl`                      | `{{ "pro.css" \| assetUrl }}`                  |
| `shopUrl`                       | `{{ "/products" \| shopUrl }}`                 |
| `apiUrl`                        | `{{ "v1/cart" \| apiUrl }}` → `/api/v1/cart`   |
| `imageUrl`                      | Absolute URL for relative images               |
| `formatPrice`                   | `{{ product.price \| formatPrice(currency) }}` |
| `formatDate` / `formatDateTime` | Locale dates                                   |
| `json`                          | Safe JSON for `<script>`                       |
| `themeColor`                    | Accent from `settings.json`                    |

***

## Cart (theme + platform)

Cart source of truth: browser `localStorage` key `cart` (`appCart`).

* Add: `appCart.add(productId, variantId, quantity)` — skip when `product.visibility == 'on-hold'`
* Resolve lines: `GET /api/v1/cart?storeId=&shopId=&cart=`
* Checkout: theme `checkout()` using `appCart.items` → `POST /api/v1/checkout` → `/checkout/{orderId}`

Use camelCase fields (`product.image`, `product.slug`, `variant.quantityMin`). There is **no** `{% markt_cart %}` tag — cart is JS + API.

***

## Embed buy shortcodes (external HTML)

For merchant sites outside the theme (Webflow, custom HTML). Load the embed script, then use **product CUID** attributes — not Liquid tags.

```html theme={null}
<script src="https://YOUR_APP_ORIGIN/embed/embed.iife.js" defer></script>
<script>
  document.addEventListener('DOMContentLoaded', () => {
    Markt.init()
  })
</script>

<button
  data-markt-product-id="PRODUCT_CUID"
  data-markt-variant-id="VARIANT_UUID"
  data-markt-quantity="1"
  data-markt-theme="auto"
>
  Buy Now
</button>
```

| Attribute                | Required    | Notes                       |
| ------------------------ | ----------- | --------------------------- |
| `data-markt-product-id`  | Yes         | Product CUID                |
| `data-markt-variant-id`  | If variants | Stable UUID                 |
| `data-markt-quantity`    | No          | Default `1`                 |
| `data-markt-theme`       | No          | `auto` \| `light` \| `dark` |
| `data-markt-return-url`  | No          | After paid                  |
| `data-markt-email`       | No          | Prefill                     |
| `data-markt-coupon-code` | No          | Coupon                      |
| `data-markt-metadata`    | No          | JSON string map             |

JS API: `Markt.init()`, `Markt.open({ items, theme })`, `Markt.close()`.

Dashboard → product → **Embed Checkout** generates snippets. Demo: `/embed/demo.html`.

***

## Vite / React (no `{% %}` shortcodes)

```tsx theme={null}
import { buildCatalogItems } from '@/lib/product-groups'
import { isStoreProductOnHold } from '@/lib/storefront-api'

const { products, groups } = useStore()
const items = buildCatalogItems(products, groups).slice(0, 8)

return (
  <section>
    {items.map((item) =>
      item.type === 'group' ? (
        <GroupCard key={item.group.id} group={item.group} />
      ) : (
        <ProductCard
          key={item.product.id}
          product={item.product}
          onHold={isStoreProductOnHold(item.product)}
        />
      ),
    )}
  </section>
)
```

Visual Builder sections map to React components; sellers edit content in the dashboard.

***

## Anti-patterns

* Inventing Shopify-style `{% product_grid %}` Liquid tags
* Looping raw `products` for the homepage when groups exist (duplicates)
* Allowing Add to cart on `visibility === 'on-hold'`
* Putting `| json` inside `x-data="..."` or HTML attributes without encoding
* Dumping full product descriptions into `<head>` scripts
* Redefining `Alpine.data('cartPage')`
* Hardcoding prices that ignore the catalog API
* Custom card payment forms inside the theme
* Calling `/api/dashboard/*` from the public storefront
* Using SellAuth snake\_case field names in native themes

See [Nunjucks Theme Pitfalls](/storefront/nunjucks-pitfalls).
