Start here
Install the script
One tag, about 12KB gzipped over the wire, loaded with defer so it never blocks rendering. It sets a visitor ID, sends a pageview on load, reports how the page was read when you leave it, and exposes a small API on window.numberhill.
The script tag
Paste this into your <head>, replacing YOUR_SITE_ID with the ID from your dashboard settings.
<script defer data-website-id="YOUR_SITE_ID"
src="https://numberhill.com/js/numberhill.js"></script>Script attributes
| Attribute | Required | Description |
|---|---|---|
| data-website-id | required | Your site ID. Every event is stored against it. |
| data-api | optional | Override the ingest endpoint. Defaults to the origin the script was served from plus /api/collect — useful when proxying through your own domain. |
| data-mode | optional | cookie (default), cookieless, or consent — cookie tracking behind a small popup shown only to visitors in countries where cookie consent is legally required (see cookies & privacy). All modes are served by the same file; without this attribute the mode is inferred from the filename, so set it explicitly if you proxy the script under a name of your own — otherwise a cookieless install would silently fall back to writing a cookie. |
| data-consent-banner | optional | off suppresses the built-in consent popup, for sites running their own banner or CMP — call numberhill.consent('granted'|'denied') from it instead. Consent mode only. |
| data-consent-text | optional | Replaces the built-in popup's message. Consent mode only. |
| data-autoidentify | optional | Identify visitors from the email field of any form they submit, instead of calling numberhill.identify() yourself. Off unless present, and it reads nothing but the email address. |
| defer | recommended | Keeps the script off the critical rendering path. |
Framework snippets
Next.js (App Router)
export default function RootLayout({ children }) {
return (
<html lang="en">
<head>
<script defer data-website-id="YOUR_SITE_ID" src="https://numberhill.com/js/numberhill.js" />
</head>
<body>{children}</body>
</html>
);
}Google Tag Manager
Create a new tag → Custom HTML, paste the script tag, and set the trigger to All Pages. Leave “Support document.write” unchecked.
Shopify
Online Store → Themes → Edit code → layout/theme.liquid, and paste before </head>.
Page builders bypass theme.liquid
Pages built with GemPages, PageFly, or a similar builder render through the builder's own layout files, so a snippet intheme.liquid never runs there — a homepage built this way records no visits while the rest of the store tracks fine, which reads as “analytics is broken” when it is one file short. In the layout/ folder, paste the same snippet before </head> in every file that has one (e.g. theme.gempages.blank.liquid).WordPress
Appearance → Theme File Editor → header.php, and paste before </head>. With a child theme or a headers plugin, use that instead so updates don't overwrite it.
Single-page apps
The script patches history.pushState and listens for popstate, so most client-side route changes are counted automatically with no router integration.
Two kinds of navigation it cannot see: routes changed with history.replaceState — which is what router.replace() and most redirect-after-login flows use — and hash routing. If your app navigates either way, call window.numberhill.pageview() after the route changes. It is safe to call more often than needed for a genuine navigation, and it also closes out the previous page's time-on-page and scroll depth, so calling it is strictly better than not.
// Next.js app router, or any router with a change event
useEffect(() => {
window.numberhill?.pageview();
}, [pathname]);Verify it works
- Load your site, open DevTools → Network, and look for a
POSTto/api/collectreturning202. - In the console,
window.numberhill.visitorId()should return a 32-character hex string. - Your dashboard's Realtime view should show the visit within a few seconds.
Ad blockers
Some blockers drop third-party analytics requests. Serving the script and/api/collect from your own domain via a proxy — then pointing data-api at it — avoids most of that. If you rename the file while proxying it, set data-mode explicitly: the mode falls back to the filename, and a renamed cookieless script would otherwise run in cookie mode.