---
title: "Page Analytics"
url: "https://glokki.com/docs/page-analytics"
locale: "en"
updated: "2026-10-14T21:07:01.721Z"
---

# Page Analytics

Privacy-friendly, first-party, cookieless pageview analytics for your tenant site — views, unique visitors, referrers, and device breakdown. No cookies, no consent banner, two files of setup.

This guide covers wiring the SDK's `createInsightsRoute` proxy and the React `PageAnalytics` beacon into your tenant app. The collected data shows up on the CMS `/analytics` dashboard. For the rest of the delivery integration, see the [Integration Guide](/docs/integration).

---

## Table of Contents

1. [What it is](#what-it-is)
2. [Prerequisites](#prerequisites)
3. [Setup step 1 — add the proxy route](#setup-step-1--add-the-proxy-route)
4. [Setup step 2 — drop in the component](#setup-step-2--drop-in-the-component)
5. [How it handles the hard cases](#how-it-handles-the-hard-cases)
6. [Viewing data](#viewing-data)
7. [FAQ](#faq)

---

## What it is

First-party, **cookieless** pageview analytics. When a visitor loads a page on your site, a tiny client-side beacon reports the path and referrer to a proxy route you host on your own domain, which forwards it to the CMS. Because nothing is stored in a cookie or `localStorage`, and visitors are never tracked across days, **no consent banner is required**.

It captures:

- **Views** — total pageviews.
- **Unique visitors** — distinct visitors, counted per day (see [Privacy](#privacy)).
- **Referrers** — where traffic came from.
- **Device** — desktop / mobile / tablet, derived server-side from the User-Agent.

Two pieces do the work:

- **`createInsightsRoute`** (from `@glokki-cms/sdk`) — a same-origin route handler you mount in your app. It holds the delivery API key server-side and forwards beacons to the CMS ingest endpoint.
- **`PageAnalytics`** (from `@glokki-cms/react`) — a client component that renders `null` and fires the beacon on mount and on every client-side route change.

---

## Prerequisites

1. **A delivery API key** with the `delivery:read` scope. Create one in the CMS Dashboard → **Settings → API Keys**. This key is used **server-side only** by the proxy route — it is never sent to the browser.
2. **Your tenant slug** (the same slug you use elsewhere in the delivery integration).
3. The **base URL** of your CMS instance (e.g. `https://cms.example.com`).

Store them as environment variables. The names below are illustrative — there is no canonical naming convention enforced by the SDK, so use whatever your app already uses; just pass the right value into each config field:

```bash
CMS_URL=https://cms.example.com
CMS_TENANT=acme
CMS_API_KEY=ek_live_...
```

---

## Setup step 1 — add the proxy route

Create a route in your app that re-exports the handler from the SDK. Mounting it on a **neutral, same-origin path** (the default the component expects is `/_e`) is what makes the analytics first-party and adblock-resistant.

```ts
// app/_e/route.ts
import { createInsightsRoute } from "@glokki-cms/sdk"

export const { POST } = createInsightsRoute({
  baseUrl: process.env.CMS_BASE_URL!,
  tenantSlug: process.env.CMS_TENANT!,
  apiKey: process.env.CMS_API_KEY!,
})
```

Config fields:

| Field        | Description                                                                                                                                                                                   |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `baseUrl`    | The base URL of your CMS instance. The handler appends the ingest path itself.                                                                                                                |
| `tenantSlug` | Your tenant slug, sent upstream as `X-Tenant-Slug`.                                                                                                                                           |
| `apiKey`     | A delivery API key with the `delivery:read` scope. **Stays server-side** — it is read from the request handler, never exposed to the browser.                                                 |
| `ipHeaders`  | _Optional._ Ordered header names to read the visitor IP from. Defaults cover Cloudflare (`cf-connecting-ip`), Fastly (`fastly-client-ip`), `x-real-ip`, then the first `x-forwarded-for` hop. |

The handler forwards the beacon body to `POST /api/v1/delivery/analytics` on the CMS, attaching the visitor's IP and User-Agent so the CMS can derive uniques and device. It **always returns `204` immediately** and swallows upstream failures — analytics can never break or slow down your page.

---

## Setup step 2 — drop in the component

Render `<PageAnalytics />` once, high in your app — the root layout is fine — so it counts **only real visitor page views**:

```tsx
// app/layout.tsx
import { PageAnalytics } from "@glokki-cms/react"

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        {children}
        <PageAnalytics />
      </body>
    </html>
  )
}
```

Since `@glokki-cms/react` 0.10.0 the component stays silent when the CMS, not a visitor, opens the page — the CMS only filters out bots, so these would otherwise inflate views:

- every load inside the **CMS visual editor**, and the **editor preview route** used to edit block templates — the CMS opens both with `?_editor=1`;
- **draft previews** opened from the CMS (`?preview=true&token=...`).

If your app uses a different preview query parameter than the `preview=true` convention from the [Integration Guide](/docs/integration#10-preview-mode), don't render `<PageAnalytics />` on those views yourself. Pageviews already recorded from editing sessions stay in the stats.

### With `@glokki-cms/react` older than 0.10.0

Older versions fire on every page they are mounted on. Upgrade, or wrap the component in a client gate and mount `<Analytics />` inside `<Suspense fallback={null}>` (`useSearchParams()` needs the boundary, or Next.js opts the whole layout out of static rendering):

```tsx
// components/analytics.tsx
"use client"

import { PageAnalytics } from "@glokki-cms/react"
import { EDITOR_QUERY_PARAM } from "@glokki-cms/sdk"
import { useSearchParams } from "next/navigation"

export function Analytics() {
  const searchParams = useSearchParams()
  // The CMS opens the editor iframe and the editor preview route with ?_editor=1.
  const inEditor = searchParams.get(EDITOR_QUERY_PARAM) === "1"
  const inPreview = searchParams.get("preview") === "true"
  return inEditor || inPreview ? null : <PageAnalytics />
}
```

The component renders `null`. On mount and on every client-side route change it fires a single `navigator.sendBeacon` (falling back to a `keepalive` `fetch` on browsers without it), so the request survives navigation and never blocks render.

It accepts one optional prop:

| Prop       | Default | Description                                                                                                                                              |
| ---------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `endpoint` | `"/_e"` | The same-origin path of your proxy route. **Must match where you mounted `createInsightsRoute`.** Override it on both sides if you use a different path. |

---

## How it handles the hard cases

### CDN caching (Fastly / Cloudflare)

The beacon is **client-side**. It runs in the visitor's browser after the (possibly CDN-cached) HTML is delivered, so it always reaches your proxy and is counted — even for a fully edge-cached page. Server-side counting would miss every cache hit; this approach does not.

### Adblockers

Serving the proxy from your **own origin** on a **neutral path** (`/_e`) sidesteps the domain- and path-based blocklists that target third-party analytics scripts, so the block rate is low. But be honest: any client-side analytics can be blocked, so expect a small undercount. This is not, and cannot be, 100% adblock-proof.

### Privacy

Visitors are identified by a **daily-rotating, salted SHA-256 hash of IP + User-Agent**, computed on the CMS at ingest time. The **raw IP is never stored** — it is consumed to compute the hash and discarded. Because the salt rotates every day, today's hashes cannot be correlated with yesterday's: there is **no durable cross-day identifier**, "unique visitor" means unique-per-day, and **no cookie/consent banner is required**.

---

## Viewing data

Open the **`/analytics`** dashboard in the CMS. It shows views, unique visitors, top referrers, and device breakdown, with **7-day, 30-day, and 90-day** ranges and a trend chart. No extra configuration — pageviews appear as soon as the proxy and component are live.

---

## FAQ

**Do I need a cookie consent banner?**
No. Nothing is written to cookies or `localStorage`, and the visitor identifier rotates daily with no cross-day correlation, so there is no persistent tracking identifier.

**The proxy returned `204` but I see no data. What's wrong?**
The proxy deliberately returns `204` and swallows upstream errors so analytics never breaks the page. Check that `baseUrl`, `tenantSlug`, and a valid `delivery:read` `apiKey` are set, and that `<PageAnalytics endpoint>` matches the path where you mounted the route.

**Can I mount the proxy somewhere other than `/_e`?**
Yes — mount it anywhere and pass the same path to `<PageAnalytics endpoint="/your-path" />`. Keep it same-origin so the analytics stays first-party.

**Will this slow down my pages?**
No. The component fires a non-blocking `sendBeacon`, and the proxy responds immediately without waiting on the CMS.

**Why are my numbers higher than in my other analytics tool?**
A common cause is a `@glokki-cms/react` version older than 0.10.0, which also counts page loads inside the CMS editor, draft previews and the editor preview route. Upgrade, or gate it as shown in [Setup step 2](#with-glokki-cmsreact-older-than-0100).
