Skip to content

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.

html
<script defer data-website-id="YOUR_SITE_ID"
  src="https://numberhill.com/js/numberhill.js"></script>

Script attributes

AttributeRequiredDescription
data-website-idrequiredYour site ID. Every event is stored against it.
data-apioptionalOverride the ingest endpoint. Defaults to the origin the script was served from plus /api/collect — useful when proxying through your own domain.
data-modeoptionalcookie (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-banneroptionaloff 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-textoptionalReplaces the built-in popup's message. Consent mode only.
data-autoidentifyoptionalIdentify 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.
deferrecommendedKeeps the script off the critical rendering path.

Framework snippets

Next.js (App Router)

app/layout.tsx
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 in theme.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.

js
// 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 POST to /api/collect returning 202.
  • 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.