Storefront Blocks & Events
An app does not only live in the admin. With blocks and scripts in its app.json it runs on every page of the storefront, and SitePack hands it two streams of events to act on: the events the server recorded while rendering the page, and the events the SitePack SDK raises in the browser while the visitor interacts. Together they let any app react to a shop end to end without touching the theme, whether it is analytics, a tag manager, a marketing pixel, or a chat or review widget.
Blocks and scripts (app.json)
{
"blocks": {
"head": "head.twig",
"body_start": "body-start.twig"
},
"scripts": ["app.js"]
}
blocksis a map of block name → Twig template undertemplates/. The template is rendered on the server into that spot of the layout. Every theme layout offershead,body_startandbody_end; e-commerce templates add hooks such asproduct_form_before,product_form_after,cart_before,cart_after,account_login_beforeandaccount_register_after(see the theme documentation for the full list).scriptslists JavaScript files underassets/js/. Each is served from the app CDN (https://cdn-app.sitepack.eu/<uuid>/<version>/assets/js/<file>) and loaded at the end of<body>as a deferred classic script, versioned per published release. There is no module scope, so wrap your code in an IIFE rather than usingimport/export.
The storefront renders an app only on sites it is installed on, from the version that was published when the merchant installed it, with that site's settings. Uninstalling removes the blocks and scripts from the site on the next page load.
What a block template receives
| Variable | Contents |
|---|---|
settings | The merchant's values from your app's settings page (admin.settings_schema), typed by the schema and with the defaults filled in. A translatable setting holds the text for the page's language. |
app | The install: uuid, name, version, settings (the same values as settings, translations included). |
config | Your decoded app.json. |
site | The site being rendered (uuid, name, locale, domain, ...). |
tracking | The page's server-side tracking context, see below. |
A typical use is to hand the settings to your script:
<script>
window.MyApp = {{ settings|sitepack_json }};
</script>
json_encode|rawraw is not available to apps or themes, and sitepack app:publish refuses it. It is how a
merchant's own text becomes stored XSS: one setting containing </script> breaks out of your
script tag. sitepack_json encodes with the HTML-hex flags so it cannot. See
The sandbox.
The same values are available to your backend as merchant_settings on GET /api/public/v1/app/settings (scope app:settings:read).
Where a block can hook
| Hook | Where it renders |
|---|---|
head | Inside <head>, after the site's own meta and asset tags |
body_start | Right after the opening <body> |
body_end | Right before the closing </body>, after your scripts |
product_form_before / product_form_after | Around the add-to-cart form |
product_variants_before / product_variants_after | Around the variant pickers |
account_before / account_after | Around the whole customer account section |
account_login_before / account_login_after | Around the login form |
account_register_before / account_register_after | Around the registration form |
account_dashboard_start / account_dashboard_end | Inside the account dashboard |
account_order_before / account_order_after | Around an order in the account |
These work the same on a legacy SitePack theme and on a custom theme, so you write your block once and it reaches every site that installs your app. More than one app can hook the same spot; blocks render in install order.
The sandbox
Your template is third-party code on every page of someone else's site, so it renders sandboxed. Apps and themes run off one allow-list: whatever a theme may use, your app may use.
That includes the storefront's own functions, so a block can do real work instead of only printing settings:
{% for product in sitepack_products({limit: 4, category: settings.category}) %}
<a href="{{ product.url }}">{{ product.name }} - {{ sitepack_price(product) }}</a>
{% endfor %}
sitepack_products(), sitepack_categories(), sitepack_price(), sitepack_icon(),
sitepack_slug() and calculate_text_color() are all available. The
theme functions reference documents what each returns, and the
filters reference which filters you may use.
Emitting unescaped output
raw is not on the list, for themes or for apps. Use the filter that fits the case:
| Instead of | Use | For |
|---|---|---|
{{ text|raw }} | {{ text|sitepack_html }} | Rich text a merchant wrote. Sanitized against an allow-list: formatting survives, scripts and event handlers do not. |
{{ data|json_encode|raw }} | {{ data|sitepack_json }} | Data going into a <script> tag. Encoded with the HTML-hex flags, so a value containing </script> cannot terminate the element. |
Two things a theme may do that an app cannot, because a block is one standalone template:
include and extends.
A template that steps outside this is refused, and the page is served without your block with the reason logged. Test your templates before you publish.
Server-side events: window.SitePackTracking
Every storefront page carries what the server knows about it, rendered next to the SitePack SDK bootstrap:
window.SitePackTracking = {
page_type: "product", // page | home | product | category | cart | checkout | order_created | search | blog | article | account | account_login | account_register
events: [
{
name: "view_item",
payload: {currency: "EUR", value: 49.95, items: [{item_id: "…", item_name: "…", price: 49.95, quantity: 1}]}
}
]
};
Items, prices and totals come straight from the server, so they are exact, and the shape follows the GA4 recommended events so most analytics tools understand them as they are.
| Event | Page | Payload |
|---|---|---|
view_item | product | currency, value, items[] (item_id, item_name, item_brand, price, quantity, item_category…, item_sku) |
view_item_list | category | item_list_id, item_list_name, items[] |
view_cart | cart | currency, value, items[] (with item_variant for a variant line) |
begin_checkout | checkout | currency, value, items[] |
purchase | order confirmation, once paid | transaction_id, order_uuid, currency, value, tax, shipping, coupon, payment_completed, items[] |
search | search results | search_term, results |
sign_up | the page after registering | method |
login | the page after a form login | method |
newsletter_signup | the newsletter status page | method, pending |
Events for a flow that ends in a redirect (registering, logging in, the newsletter form) are deferred: SitePack parks them in the session and the next page the visitor sees carries them, once.
Consent: SitePackTracking.consent
The context also carries what the visitor allowed in the SitePack cookie banner:
window.SitePackTracking.consent = {decided: true, essential: true, functional: true, analytics: true, marketing: false};
decided is false until the visitor answers the banner, and every category is false with it. The old single-button bar has no categories; closing it counts as consent for everything.
SitePack's own statistics are first-party and need no consent. Anything your app loads from a third party (GA4 through a tag manager, an ad pixel) must stay off until the visitor allows the matching category: check analytics for measurement and marketing for advertising, both on the server (tracking.consent in your block templates) and in the browser (sdk.cookies.consent()). The banner raises sitepack:cookies:consent on window the moment the visitor decides, so a tag that waited can start in that same page view; the page reloads right after.
The confirmation page renders purchase only once the order is paid, or once it is placed for an order that needs no online payment (bank transfer, on account). A shopper who lands there before paying gets no purchase, so no revenue is counted early. A paid page still re-renders it on every reload, so deduplicate on transaction_id in your script (for example, keep the last handful of ids you have already reported in localStorage).
Client-side events: the SDK's sitepack:* events
The SitePack SDK raises a CustomEvent on window whenever a visitor does something through it. The payload is in event.detail and always carries source: 'sdk', so you can tell an SDK event apart from one a theme dispatches under the same name.
| Event | Raised by | detail |
|---|---|---|
sitepack:cart:updated | every cart change | {cart, source: 'local' | 'sync'} (the whole cart; cart is null after an order) |
sitepack:cart:added | addToCart, updateCartQuantity | {items: [line…], cart} — the lines whose quantity went up, quantity being the increase |
sitepack:cart:removed | updateCartQuantity, clearCart | {items: [line…], cart} — the lines whose quantity went down |
sitepack:order:created | createOrder | {order, redirect_url, cart} — intent, raised right before the browser leaves for the payment provider |
sitepack:checkout:thank-you | the confirmation page, once the order is paid | {order} — the completed sale; the purchase payload, raised once per page (the paid counterpart of order:created) |
sitepack:lead:generated | leads.generate, contactSiteOwner(data, 'lead') | {} |
sitepack:contact:completed | contactSiteOwner (default trackAs: 'contact') and the contact form element | {form: 'contact'} |
sitepack:newsletter:subscribed | newsletter.subscribe | {pending} |
sitepack:account:login | account.login | {} |
sitepack:cookies:consent | the SitePack cookie banner | {consent: {decided, essential, functional, analytics, marketing}} (source cookiebar) |
A cart line is the SDK's cart product (productUuid, title, brand, priceCents, promoPriceCents, quantity, variantUuid, variantOptionValues). No personal data travels in these events.
window.addEventListener('sitepack:cart:added', (event) => {
if (event.detail.source !== 'sdk') return; // ignore a theme's own copy of the event
for (const line of event.detail.items) {
console.log('added', line.title, 'x', line.quantity);
}
});
Client-side and server-side, in one app
The two streams above are the same story told from two places, and a complete integration usually uses both.
In the browser (client-side). Your scripts read window.SitePackTracking and listen for the sitepack:* events. This is real time and covers everything a visitor does, including pure browsing (view_item, view_cart, add_to_cart, search) that never leaves the page. It runs in the visitor's browser, so it depends on your script loading and, for third-party tags, on the visitor's consent.
On your server (server-side). A storefront action that creates or changes a resource in SitePack also fires a webhook to your backend. This is server to server: signed, retried once when your server does not answer, and independent of the browser, so it is the reliable side for anything you must not miss, such as recording a sale or syncing a lead into a CRM.
The two sides describe the same moment, but under different names and at slightly different times, so line them up carefully. Each stream keeps its own naming convention on purpose: the server-side tracking events follow GA4 (view_item, purchase), the browser events are namespaced SitePack SDK events (sitepack:order:created), and webhooks follow the resource.verb shape of the rest of the webhook catalog (order.created). They match by meaning, not by string.
The same storefront moment, in each stream
Only a storefront action that becomes a resource in SitePack has a server-side webhook. Pure browsing stays in the browser.
| Storefront moment | Server-side tracking (GA4) | Browser SDK event | Webhook |
|---|---|---|---|
| Views a product, cart, category, search | view_item, view_cart, ... | sitepack:cart:* | none, client-side only |
| Places an order (before payment) | order_created | sitepack:order:created | order.created |
| Order is paid | purchase | sitepack:checkout:thank-you | order.paid |
| Submits a form or lead | generate_lead, contact_form_completed | sitepack:lead:generated, sitepack:contact:completed | lead.created, lead.completed |
| Subscribes to the newsletter | newsletter_signup | sitepack:newsletter:subscribed | newsletter.subscribed, later newsletter.confirmed |
Mind the two order webhooks and their timing. order.created fires when the shopper places the order, which for an online payment is before the money is in; order.paid fires later, when the payment reaches "completed" (the server-side counterpart of the purchase event). Placing an order also creates the customer record, so customer.created fires alongside order.created. The webhook payload is thin, so read the resource back with your app's scopes; see Webhooks for the payload shape, signing and retries.
view_item, view_item_list, view_cart, begin_checkout, add_to_cart and search are never sent to your server. They are per-pageview, per-visitor signals that belong in the client-side stream, where consent and volume are handled for you. SitePack does not deliver a webhook for every page view.
Putting it together
A typical analytics or tag-management app combines all of the above: a head block renders the merchant's settings and any consent defaults, and loads its third-party container only once tracking.consent allows the matching category; a body_start block adds a <noscript> fallback under the same rule; and one script turns window.SitePackTracking plus the sitepack:* events into whatever calls the third party expects, each behind a switch on the app's settings page, starting the moment sitepack:cookies:consent grants consent. None of this is specific to one app. The same blocks, tracking context, events and consent flow are open to every app on the platform.
Next Steps
- App Manifest (app.json): Where
blocks,scriptsand the settings schema live. - App Settings: The settings page merchants fill in.
- Authentication (OAuth): Reading the same settings from your backend.
- SDK: Ecommerce: The cart calls behind the cart events.