By the end of this article you will be sending KO the things people do on a site that is produced by a generator — Astro, Hugo, Next, Eleventy, Jekyll. Compared with a regular website there are two differences, both in your favour: the templating engine substitutes the context values, and the markup can be checked before you publish. The counter has to be on your pages already: connecting a website.
What the counter already does
Page views, visits, sources, campaigns, device and visitor are recorded by the counter itself: every navigation here is a fresh load, and the view is sent without your involvement.
Your own code sends what the counter cannot see: a button was clicked, a plan was chosen, a result was awaited, a form was submitted.
Two limits follow from the reloads, shared with a regular website: the wrapper is declared in the shared layout (so it lands on every page), and an event fired while leaving the page ships unreliably — the reliable anchor goes on the receiving page.
Add the wrapper
The wrapper goes into the shared layout, and the templating engine substitutes the values into it. The code is the same as on a regular website — it is given in full there; only the differences are below.
In Astro the script needs the is:inline attribute. Without it Astro turns the script into a
module, the module runs after the markup, and the first events happen before siteTrack
exists. Frontmatter values are passed through define:vars:
---
const prodHost = new URL(SITE.url).hostname;
const pageKind = ...; // section, computed from the page address at build time
const locale = ...;
---
<script is:inline define:vars={{ prodHost, pageKind, locale }}>
(function () {
var onProd = location.hostname === prodHost || location.hostname === 'www.' + prodHost;
/* then the same wrapper code: call(), siteTrack, siteTrackData */
siteTrackData({ page_kind: pageKind, locale: locale }); // context first, before any event
})();
</script>
Hugo, Jekyll, Eleventy and Next do the same with their own means — an inline script in the shared layout with variables substituted.
The domain gate is mandatory, and here it doubles as protection against preview builds:
addresses like deploy-preview-42.netlify.app would otherwise register in the project as
separate sites.
The counter can be loaded late
An option that only a build step makes practical: the counter is attached on the first interaction rather than immediately, with a fallback timer for people who never interact. It helps speed, but it has a price — events from the first seconds wait in the queue, and an event fired while leaving the page with the counter not yet loaded is lost entirely.
The wrapper is declared before that block and works whether the counter arrived or not.
Send your first event
The call goes wherever the markup lives — in a component, a template, or the page’s own script:
document.getElementById('buy').addEventListener('click', function () {
siteTrack('checkout_started', { plan: 'pro', place: 'pricing_table' });
});
An event name is Latin letters, lowercase, past tense; what tells cases apart goes into the data, not into the name. The full rules, together with repeat protection and normalising values taken from the address, are in the event dictionary.
Compute page context at build time, not in the browser. Derive page_kind, language and
page type from the page address where that address is formed — in your routes and locale
rules. A second copy of the address scheme in JS drifts from the first silently and the report
shows the wrong section; computed at build time, a new section arrives in the report under its
own name by itself.
Layers accumulate: the shared layer is set in the layout, a page adds its own, and the last one wins.
Check that the event arrives
First, before publishing. Build the site and look at the built files rather than the
sources: those are what ships. The wrapper block must be in the HTML before the counter, and
is:inline must be there (without it the markup carries a module reference instead of your
code).
That same spot is worth covering with a script: render the built pages in several states and assert that the HTML contains the event name, the wrapper call and the repeat-protection key — and that there is no call where no event should be sent. Events break silently, and the report shows it weeks later.
Then, on the live site: the Network tab, the action, a visit.php?type=event request
carrying your event name and the fields inside data. If there is no request, ?ko_debug in
the address turns on the counter’s console output.
Turn the event into a metric
- In the project’s Metrics section create a metric: your own key (say
siteCheckoutStarted) and the formula{sites_events:visitor:checkout_started}— your event name in the third part. - Rates and conversions are formulas over metric keys:
{siteCheckouts}/{sitePricingViews}. - People rather than clicks:
{sites_events:visitor:checkout_started[:value=visitors_id][:aggregate=uniq]}. - A breakdown by event data is the
field:data.plandimension in your report’s Breakdown section. You do not need a metric per value. - If the event carries money, pass the amount as
incomenext todata, not inside it — that is what makes it add up as money.
If it doesn’t add up
Nothing is sent from a preview build — and that is correct. The domain gate silences everything outside the live address. Test on the live site.
The markup carries a module reference instead of your code. The script is missing
is:inline, so it runs after the first events.
Fewer events than actions. The counter is loaded late and the event was fired while leaving the page. Move the anchor to the receiving page.
The wrong section in the report. Context was computed in the browser from the address and drifted from your routes. Compute it at build time.
More events than actions. The page reloads itself and there is no key-based protection — see the event dictionary.
Related pages
- Counter calls — call shape, envelope, queue, options
- The event dictionary — naming, and where to put the call
- Custom events from a regular website — HTML and site builders, the full wrapper code
- Custom events from a single-page app — if pages do not reload