# Headless and HATEOAS

Source: https://kart.bnomei.com/docs/forms/headless-and-hateoas
Updated: 2025-08-08T14:06:44+00:00
Summary: Guide to using Kirby Kart plugin: integrate HTML forms or headless apps with JSON/CSRF, and leverage HATEOAS via Htmx for dynamic cart and wishlist interactions

## HTML Forms

By default, you would use HTML forms like this to send data to the Kirby Kart plugin routes.

Code (php):  
```
<?php
/** @var ProductPage $page */
$product ??= $page;

if ($product->inStock()) { ?>
    <form method="POST" action="<?= $product->add() ?>">
        <input type="hidden" name="redirect" value="<?= $redirect ?? $page->url() ?>">
        <button type="submit" onclick="this.disabled=true;this.form.submit();">Add to cart</button>
    </form>
<?php } else { ?>
    <p><mark>Out of stock</mark></p>
<?php }
```

  
## Headless

To use the same routes but for a headless setup when using a modern Javascript framework like Next.js, Vue or Solid to build your website you need to adjust it a bit. Given both the `Content-Type` and `X-CSRF-TOKEN` the Kirby Kart plugins router will respond with JSON instead of redirecting (like it would in the HTML form example).

You can force the Kirby Kart router to always respond with JSON in setting the `bnomei.kart.router.mode = json` in the config.

Code (typescript):  
```
import { createSignal } from "solid-js";
import { csrfToken } from "./store"; // Import global CSRF token

const CartToggleButton = (props) => {
  const [inCart, setInCart] = createSignal(props.product.inCart); // Initial state

  const toggleCart = async () => {
    const url = inCart()
      ? `/kart/cart-remove?product=${props.product.uuid.id}`
      : `/kart/cart-add?product=${props.product.uuid.id}`;

    const response = await fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-CSRF-TOKEN": csrfToken(),
      },
    });

    if (response.ok) {
      setInCart(!inCart()); // Toggle state on success
    }
  };

  return props.product.inStock ? (
    <button onClick={toggleCart}>
      {inCart() ? "Remove from cart" : "Add to cart"}
    </button>
  ) : (
    <p><mark>Out of stock</mark></p>
  );
};

export default CartToggleButton;
```

  
> TIP: You can use the `kart/csrf`-endpoint to retrieve the current CSRF session token.

> TIP: The Kirby Kart plugin supports KQL in exposing the most common props by default.

> TIP: Instead of relying on the default behaviour of the router you can also use snippets to customize the JSON output. See HATEOAS example below for more details.

## HATEOAS

If you want your website to swap HTML instead of making a form request and reloading then [Htmx](https://htmx.org) or [Datastar](https://data-star.dev) are great choices. The Kirby Kart router will automatically detect the `HX-Request`-header sent by Htmx and respond with a matching snippet (see below).

While you could force the Kirby Kart router to always respond with HTML in setting the `bnomei.kart.router.mode = html` in the config you might not want that for every request. It might be easier to send a fake `HX-Request` header for Datastar instead.

The `bnomei.kart.router.snippets` config setting provides a default mapping for snippets. Only the snippets listed in the array will be called and you need to overwrite this setting to add your own.

Code (php):  
```
'bnomei.kart.router.snippets' => [
    // define the snippets that are allowed to be called
    'kart/cart-add',
    'kart/cart-buy',
    'kart/cart-later', // dummy
    'kart/cart-remove', // dummy
    'kart/captcha',
    'kart/login',
    'kart/login-magic',
    'kart/signup-magic',
    'kart/wish-or-forget',
    'kart/wishlist-add' => 'kart/wish-or-forget.htmx',  // htmx
    'kart/wishlist-now',  // dummy
    'kart/wishlist-remove' => 'kart/wish-or-forget.htmx',  // htmx
    // overwrite to change or set your own
],
```

  
For all plain values, the router will respond with the HTML code generated by the snippet. So the route `kart/cart-add` will render the snippet `site/snippets/kart/card-add.php`.

Note the two mappings that have the Htmx comment. Here the route is mapped to a snippet with a different name to allow the dynamic behaviour.

`kart/wishlist-add` outputs `site/snippets/kart/wish-or-forget.htmx.php`   
and the route `kart/wishlist-route` outputs `site/snippets/kart/wish-or-forget.htmx.php` as well.   
This works since the snippet is aware of the state of the product in the wishlist.

Path: site/snippets/kart/wish-or-forget.htmx.php  
Code (php):  
```
<?php
$product ??= $page;
?>
<?php if (! kart()->wishlist()->has($product)) { ?>
    <button hx-post="<?= $product->wish() ?>" 
            hx-disabled-elt="this" 
            hx-swap="outerHTML" 
            title="add to wishlist">Add to wishlist</button>
<?php } else { ?>
    <button hx-post="<?= $product->forget() ?>" 
            hx-disabled-elt="this" 
            hx-swap="outerHTML" 
            title="remove from wishlist">Remove from wishlist</button>
<?php }
```

  