Headless and HATEOAS

HTML Forms

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

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

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 or Datastar 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.

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

site/snippets/kart/wish-or-forget.htmx.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 }
Kirby Kart is not affiliated with the developers of Kirby CMS. We are merely standing on the shoulder of giants.
© 2025 Bruno Meilick All rights reserved.