Node.js, Express, Cloudflare or your own
Every connector needs the same three settings from your Looku installation:
LOOKU_ENDPOINT=https://looku.example.comLOOKU_SITE_ID=myblogLOOKU_SIGNING_KEY=your signing keyKeep them out of git, like any password. The Node.js connector sends data only when NODE_ENV=production.
Node.js and Express
Section titled “Node.js and Express”-
Copy two files from the Looku repository into your project, for example into
lib/:packages/next/src/index.jsaslib/looku-next.js,packages/node/src/index.jsaslib/looku-node.js.
The second file imports the first as
@looku/next. Openlib/looku-node.jsand change that import to"./looku-next.js". -
Add the middleware to your Express app, before your routes:
import express from "express";import { lookuMiddleware } from "./lib/looku-node.js";const app = express();app.use(lookuMiddleware({ trustProxy: true }));For a plain
httpserver, calltrackNodeRequest(req, res)at the start of the request handler instead. -
Set
trustProxyto match your setup. The connector needs the visitor’s IP address to count visitors, and where it reads it from depends on what is in front of your app:In front of your app Option nothing, visitors connect directly leave trustProxyoutone reverse proxy (nginx, Caddy) that sets X-Forwarded-FortrustProxy: true(or1)two proxies trustProxy: 2Cloudflare only ipHeaders: ["cf-connecting-ip"]and notrustProxyGetting this wrong does not break your site, but visitors are then counted wrongly: either everyone looks like the proxy (one visitor), or a visitor could fake an address.
-
Restart your app with the three settings and
NODE_ENV=production, open a page and check the dashboard.
This connector counts only successful HTML responses (status 2xx, or 304 when the browser revalidates a page from its cache), so missing pages are not counted.
Static sites on Cloudflare Workers or Pages
Section titled “Static sites on Cloudflare Workers or Pages”A static site has no server of its own, so the connector runs in a Cloudflare Worker in front of it. The example in
examples/cloudflare-worker/worker.js
serves your site and sends a page view in the background after each successful HTML response.
- Copy
examples/cloudflare-worker/worker.jsinto your Worker project, andpackages/next/src/index.jsnext to it aslooku-next.js. - Add
LOOKU_ENDPOINTandLOOKU_SITE_IDas variables andLOOKU_SIGNING_KEYas a secret. With Wrangler:npx wrangler secret put LOOKU_SIGNING_KEY, which asks for the value. In the Cloudflare dashboard, open the Worker and go to Settings → Variables and Secrets (search: “Variables and Secrets”). - Deploy and open a page.
Only Cloudflare’s own cf-connecting-ip header is trusted for the visitor’s address, because Cloudflare always sets
it itself.
Your own connector
Section titled “Your own connector”Any language works. For each page view, send:
POST https://looku.example.com/ingestContent-Type: application/jsonX-Looku-Site: myblogX-Looku-Signature: t=1762160000,v1=5f2c…with a JSON body:
{ "type": "pageview", "ts": "2026-11-03T10:00:00.000Z", "path": "/hello", "referrer": "https://google.com/", "ip": "198.51.100.7", "ua": "Mozilla/5.0 …", "country": "FI", "utm_source": "news" }tis the current time in Unix seconds.v1is HMAC-SHA256 of the text<t>.<body>(the number, a dot, and the exact body bytes you send), with the signing key as the key, written as lowercase hex.path,ipanduaare required.country(two letters) and theutm_fields are optional.- Each signature is accepted once and only within five minutes of
t, so the sender’s clock must be right.
Answers: 202 accepted, 400 invalid body (the reason is in the response), 401 wrong signature, site id or time,
409 the same signature was already used, 413 too large.
Skip bots, prefetches and requests for files yourself, and respect Do Not Track. sendToLooku and buildPageview in
packages/next/src/index.js are a complete reference implementation. Besides known bots, buildPageview skips
requests without Accept-Language, requests with Do Not Track or Global Privacy Control, and requests over HTTPS
that name a modern browser but lack its Sec-Fetch-Mode header.