Skip to main content

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"]
}
  • blocks is a map of block name → Twig template under templates/. The template is rendered on the server into that spot of the layout. Every theme layout offers head, body_start and body_end; e-commerce templates add hooks such as product_form_before, product_form_after, cart_before, cart_after, account_login_before and account_register_after (see the theme documentation for the full list).
  • scripts lists JavaScript files under assets/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 using import/export.
Installed apps only

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​

VariableContents
settingsThe 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.
appThe install: uuid, name, version, settings (the same values as settings, translations included).
configYour decoded app.json.
siteThe site being rendered (uuid, name, locale, domain, ...).
trackingThe 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>
Never json_encode|raw

raw 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​

HookWhere it renders
headInside <head>, after the site's own meta and asset tags
body_startRight after the opening <body>
body_endRight before the closing </body>, after your scripts
product_form_before / product_form_afterAround the add-to-cart form
product_variants_before / product_variants_afterAround the variant pickers
account_before / account_afterAround the whole customer account section
account_login_before / account_login_afterAround the login form
account_register_before / account_register_afterAround the registration form
account_dashboard_start / account_dashboard_endInside the account dashboard
account_order_before / account_order_afterAround 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 ofUseFor
{{ 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.

EventPagePayload
view_itemproductcurrency, value, items[] (item_id, item_name, item_brand, price, quantity, item_category…, item_sku)
view_item_listcategoryitem_list_id, item_list_name, items[]
view_cartcartcurrency, value, items[] (with item_variant for a variant line)
begin_checkoutcheckoutcurrency, value, items[]
purchaseorder confirmation, once paidtransaction_id, order_uuid, currency, value, tax, shipping, coupon, payment_completed, items[]
searchsearch resultssearch_term, results
sign_upthe page after registeringmethod
loginthe page after a form loginmethod
newsletter_signupthe newsletter status pagemethod, 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.

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.

Third-party tags wait for consent

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.

Purchase waits for payment, then dedupes on reload

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.

EventRaised bydetail
sitepack:cart:updatedevery cart change{cart, source: 'local' | 'sync'} (the whole cart; cart is null after an order)
sitepack:cart:addedaddToCart, updateCartQuantity{items: [line…], cart} — the lines whose quantity went up, quantity being the increase
sitepack:cart:removedupdateCartQuantity, clearCart{items: [line…], cart} — the lines whose quantity went down
sitepack:order:createdcreateOrder{order, redirect_url, cart} — intent, raised right before the browser leaves for the payment provider
sitepack:checkout:thank-youthe 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:generatedleads.generate, contactSiteOwner(data, 'lead'){}
sitepack:contact:completedcontactSiteOwner (default trackAs: 'contact') and the contact form element{form: 'contact'}
sitepack:newsletter:subscribednewsletter.subscribe{pending}
sitepack:account:loginaccount.login{}
sitepack:cookies:consentthe 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 momentServer-side tracking (GA4)Browser SDK eventWebhook
Views a product, cart, category, searchview_item, view_cart, ...sitepack:cart:*none, client-side only
Places an order (before payment)order_createdsitepack:order:createdorder.created
Order is paidpurchasesitepack:checkout:thank-youorder.paid
Submits a form or leadgenerate_lead, contact_form_completedsitepack:lead:generated, sitepack:contact:completedlead.created, lead.completed
Subscribes to the newsletternewsletter_signupsitepack:newsletter:subscribednewsletter.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.

Browsing events stay in the browser

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​