Start typing to see suggestions. Use arrow keys to navigate, Enter to select.

Using javascript

Fountain is designed to work without javascript by default, using standard HTML forms. However, Fountain provides a javascript API for building dynamic, enhanced user experiences when adding and removing items from the cart.

How it works

Fountain routes detect javascript requests via the X-Requested-With: fountain header. When this header is present, routes return JSON responses instead of redirecting. This allows you to:

  • Add/remove items without page reloads
  • Update cart dynamically
  • Provide instant feedback to users

API endpoints

The javascript API endpoints use the same as regular <form> post do. Routes can be found here.

Example: Add to cart

Endpoint: site()->cartAddUrl() (default is /cart-add)
Methods: GET, POST
Header: X-Requested-With: fountain

Parameters

Parameter Type Description
add string Font UUID (required for JS requests)
uri string Font URI/slug (optional)
license string License ID(s), separated by configured separator
addon string Addon ID(s), separated by configured separator
csrf string CSRF token (required for POST requests)

Example: Add font with license

async function addToCart( fontUuid, licenseId ) {
    const params = new URLSearchParams( {
        add: fontUuid,
        license: licenseId  // just the ID; multiple licenses would be "id1|id2"
    });

    const response = await fetch( '/cart-add?' + params, {
        headers: { 'X-Requested-With': 'fountain' }
    });

    return response.json();
}

Example: Add font with license and addon

async function addToCart( fontUuid, licenseId ) {
    const params = new URLSearchParams( {
        add: fontUuid,
        license: licenseID,
        addon: addonID  // just the ID; multiple addons would be "id1|id2"
    });

    const response = await fetch( '/cart-add?' + params, {
        headers: { 'X-Requested-With': 'fountain' }
    });

    return response.json();
}

Response format

Both endpoints return JSON when the X-Requested-With: fountain header is present. The response includes the updated cart state:

{
    "licensee": null,
    "subtotal": "99.00",
    "cart": {
        "items": [
            {
                "uuid": "abc123def456",
                "licenses": [ "desktop-license" ],
                "addons": [ "webfont-addon" ]
            }
        ]
    }
}

Response fields

Field Type Description
licensee string|null Licensee name if set
subtotal string Cart subtotal as formatted string
cart.items array Array of cart items
cart.items[].uuid string Font UUID
cart.items[].licenses array Array of license IDs
cart.items[].addons array Array of addon IDs

Important notes

CSRF Protection

POST requests require a valid CSRF token. For GET requests (recommended for cart operations), CSRF is not required.

Debug mode

In debug mode, the JSON response is returned pretty print, for debug output.

To enable debug mode in your config.php:

return [
  'debug' => true
];

Separator configuration

The separator character used to join values (default: |) can be configured in your Kirby config:

return [
  'nymarktype.fountain.separator' => '|'
];

Input validation

All routes include strict input validation:

  • UUIDs must match pattern: /^[a-zA-Z0-9\-]{8,50}$/
  • URIs must match pattern: /^[a-zA-Z0-9\-_\/]+$/
  • Suspicious input is logged with IP address

Error handling

Always wrap cart operations in try-catch blocks to handle network errors, validation failures, or server issues gracefully.

Progressive enhancement

Fountain is built with progressive enhancement in mind. Start with working HTML forms, then enhance with javascript. This example is from the default template, and shows one way to do it. Each style includes a visually hidden <select> element for every license type, javascript reads the selected option and adds it to the cart.

<?php foreach( $site->licenses()->toStructure() as $key => $entity ) : ?>
    <?php $levels = $entity->licenseEntityLevels()->toStructure() ?>
    <select name="<?= $font->getLicenseFormName( $key ) ?>"  @change="updateSelections()" style="display:none">
        <option selected>—</option>
        <?php foreach ( $levels as $level ) : ?>
            <option value="<?= $font->getLicenseFormValue( $level->licenseEntityLevelId() ) ?>" data-price="<?= $font->getLicensePrice( $level->licenseEntityLevelId() ) ?>">
                <?= $font->getLicenseTitle( $level->licenseEntityLevelId() ) ?>
            </option>
        <?php endforeach ?>
    </select>
<?php endforeach ?>

This ensures your cart works for all users, regardless of JavaScript availability.

Start typing to see suggestions. Use arrow keys to navigate, Enter to select.