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.