Using javascript
Fountain is designed to work without JavaScript by default, using standard HTML forms. However, it 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 UI dynamically
- Build single-page checkout experiences
- Provide instant feedback to users
API Endpoints
Add to Cart
Endpoint: site()->cartAddUrl()
(typically /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 separator = '|'; // Default Fountain separator
const params = new URLSearchParams({
add: fontUuid,
license: `${fontUuid}${separator}${licenseId}`
});
const response = await fetch(`/cart-add?${params}`, {
headers: {
'X-Requested-With': 'fountain'
}
});
const data = await response.json();
return data;
}
Example: Add Font with License and Addon
async function addToCartWithAddon(fontUuid, licenseId, addonId) {
const separator = '|';
const params = new URLSearchParams({
add: fontUuid,
license: `${fontUuid}${separator}${licenseId}`,
addon: `${fontUuid}${separator}${addonId}`
});
const response = await fetch(`/cart-add?${params}`, {
headers: {
'X-Requested-With': 'fountain'
}
});
return await response.json();
}
Remove from Cart
Endpoint: site()->cartRemoveUrl()
(typically /cart-remove
)
Methods: GET, POST
Header: X-Requested-With: fountain
Parameters
Parameter | Type | Description |
---|---|---|
remove |
string | UUID to remove entire product, or uuid|license to remove specific license |
Example: Remove Entire Product
async function removeProduct(fontUuid) {
const params = new URLSearchParams({
remove: fontUuid
});
const response = await fetch(`/cart-remove?${params}`, {
headers: {
'X-Requested-With': 'fountain'
}
});
return await response.json();
}
Example: Remove Specific License
async function removeLicense(fontUuid, licenseId) {
const separator = '|';
const params = new URLSearchParams({
remove: `${fontUuid}${separator}${licenseId}`
});
const response = await fetch(`/cart-remove?${params}`, {
headers: {
'X-Requested-With': 'fountain'
}
});
return await 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 |
Complete Example
Here's a complete example of a cart management module:
class FountainCart {
constructor() {
this.separator = '|'; // Match Kirby option
this.baseUrl = window.location.origin;
}
async request(url, params) {
const queryString = new URLSearchParams(params).toString();
const response = await fetch(`${this.baseUrl}${url}?${queryString}`, {
headers: {
'X-Requested-With': 'fountain'
}
});
if (!response.ok) {
throw new Error(`Cart request failed: ${response.status}`);
}
return await response.json();
}
async addProduct(uuid, licenseId = null, addonId = null) {
const params = { add: uuid };
if (licenseId) {
params.license = `${uuid}${this.separator}${licenseId}`;
}
if (addonId) {
params.addon = `${uuid}${this.separator}${addonId}`;
}
return await this.request('/cart-add', params);
}
async removeProduct(uuid) {
return await this.request('/cart-remove', {
remove: uuid
});
}
async removeLicense(uuid, licenseId) {
return await this.request('/cart-remove', {
remove: `${uuid}${this.separator}${licenseId}`
});
}
updateUI(cartData) {
// Update your UI with the cart data
console.log('Cart updated:', cartData);
document.querySelector('.cart-count').textContent =
cartData.cart.items.length;
document.querySelector('.cart-subtotal').textContent =
cartData.subtotal;
}
}
// Usage
const cart = new FountainCart();
document.querySelectorAll('.add-to-cart-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
e.preventDefault();
const uuid = btn.dataset.uuid;
const license = btn.dataset.license;
try {
const data = await cart.addProduct(uuid, license);
cart.updateUI(data);
} catch (error) {
console.error('Failed to add to cart:', error);
}
});
});
Important Notes
CSRF Protection
POST requests require a valid CSRF token. For GET requests (recommended for cart operations), CSRF is not required.
Debug Mode
Important: The JSON response is only returned when Kirby's debug mode is enabled. In production (debug: false), the getJson()
method returns null
.
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:
<!-- Works without JavaScript -->
<form action="/cart-add" method="post">
<input type="hidden" name="product_<?= $font->uuid() ?>"
value="<?= $font->uuid() ?>|<?= $font->uri() ?>">
<input type="hidden" name="license_<?= $font->uuid() ?>"
value="<?= $font->uuid() ?>|desktop-license">
<button type="submit">Add to Cart</button>
</form>
<script>
// Enhance with JavaScript
document.querySelector('form').addEventListener('submit', async (e) => {
e.preventDefault();
// Use JavaScript API instead
const data = await cart.addProduct(uuid, licenseId);
cart.updateUI(data);
});
</script>
This ensures your cart works for all users, regardless of JavaScript availability.