Script install

One script tag gives you page views, visitors, referrers, UTM campaigns, countries, devices, browsers and JavaScript errors. It sets no cookies and uses no local storage.

The snippet

<script defer src="https://t.mosaicdeck.com/s.js" data-site="site_01J9XEXAMPLE00000000000000"></script>

Copy your own snippet, with your site ID, from the site's Connections tab. Put it in the<head> of every page.

Options

AttributeWhat it does
data-siteRequired. Your site ID.
data-errors="false"Turns off JavaScript error capture (on by default, at most 10 per page load).
data-outbound="true"Tracks clicks on links to other domains as the custom event outbound with the link's url.
data-respect-dnt="true"Skips tracking for visitors who have Do Not Track turned on.
data-api="https://example.com/relay"Sends events through your own domain instead (see first-party proxy).

What the script does

  • Sends a page view on load and on every client-side navigation (pushState, replaceState and the back button), once per path. Only the path is sent, never the query string.
  • Records the referring site's hostname and utm_source / utm_campaign from the landing URL.
  • Sends with navigator.sendBeacon, so it never delays page unloads, and falls back to fetch.
  • Never runs on localhost, 127.*, *.local or file: pages.
  • Exposes window.mosaicdeck.track() for custom events.

Allowed domains

Events are only accepted from the hostnames listed for your site. If data arrives from another domain (a staging copy, awww variant), the Connections tab shows “Receiving data from an unlisted domain” so you can add it.

Content Security Policy

If your site sends a CSP, allow the script and its beacons: add https://t.mosaicdeck.com toscript-src and connect-src. With the proxy below, only script-src needs it.

Framework guides

Plain HTML

Paste the snippet inside <head> on every page, or in your shared header template.

<head>
  <script defer src="https://t.mosaicdeck.com/s.js" data-site="site_01J9XEXAMPLE00000000000000"></script>
</head>

Next.js

Use next/script. Client-side navigations are picked up automatically.

// app/layout.tsx (App Router). For the Pages Router, put the same <Script> in pages/_app.tsx.
import Script from "next/script";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script src="https://t.mosaicdeck.com/s.js" data-site="site_01J9XEXAMPLE00000000000000" strategy="afterInteractive" />
      </body>
    </html>
  );
}

Astro

---
// src/layouts/Layout.astro
---
<head>
  <!-- is:inline tells Astro to leave the tag exactly as written -->
  <script is:inline defer src="https://t.mosaicdeck.com/s.js" data-site="site_01J9XEXAMPLE00000000000000"></script>
</head>

WordPress

Paste the snippet into your theme's header through a “header and footer code” plugin, or add it from a child theme:

// In your child theme's functions.php, or a code-snippets plugin
add_action('wp_head', function () {
    echo '<script defer src="https://t.mosaicdeck.com/s.js" data-site="site_01J9XEXAMPLE00000000000000"></script>';
});

React and other single-page apps

Add the snippet to index.html. Route changes made with the History API (React Router, TanStack Router, Vue Router and similar) are tracked as page views with no extra code.

<!-- index.html (Vite, Create React App, Vue, Svelte, …) -->
<head>
  <script defer src="https://t.mosaicdeck.com/s.js" data-site="site_01J9XEXAMPLE00000000000000"></script>
</head>

Webflow, Squarespace and site builders

Paste the snippet into the site-wide custom code (head) setting.

First-party proxy

Some ad blockers block third-party analytics. You can send events through your own domain instead: the script posts to your proxy, and the proxy forwards the request to Mosaicdeck.

A proxy hides your visitors' real IP addresses, which Mosaicdeck needs (in memory only) for rate limits, countries and the daily visitor hash. So the proxy passes them on with a proxy token that proves the request came from you:

  1. On the site's Connections tab, create a proxy token. It's shown once. Store it as a secret namedMOSAICDECK_PROXY_TOKEN on your host.
  2. Deploy one of the proxies below. Each forwards the body unchanged, plus the visitor's User-Agent and these headers:
    • X-Mosaicdeck-Proxy-Token: the proxy token
    • X-Mosaicdeck-Client-IP: the visitor's IP address
    • X-Mosaicdeck-Country: the visitor's country code, if your host knows it
    • X-Mosaicdeck-Origin: the visitor's Origin
  3. Point the script at your proxy with data-api. The script adds /e to it:
    <script defer src="https://t.mosaicdeck.com/s.js" data-site="site_01J9XEXAMPLE00000000000000" data-api="https://example.com/relay"></script>

Without a valid token, every visitor behind the proxy looks like one person: visitor counts will be wrong and the per-visitor rate limit applies to all of them together. The Connections tab warns you if it sees proxy headers without a valid token.

Cloudflare Workers

Set the secret with wrangler secret put MOSAICDECK_PROXY_TOKEN and add a route forexample.com/relay/*.

// Cloudflare Worker on your own domain, routed to example.com/relay/*
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    if (request.method !== "POST" || url.pathname !== "/relay/e") {
      return new Response("Not found", { status: 404 });
    }
    const headers = {
      "Content-Type": "text/plain",
      "User-Agent": request.headers.get("User-Agent") ?? "",
      "X-Mosaicdeck-Proxy-Token": env.MOSAICDECK_PROXY_TOKEN,
      "X-Mosaicdeck-Client-IP": request.headers.get("CF-Connecting-IP") ?? "",
      "X-Mosaicdeck-Origin": request.headers.get("Origin") ?? url.origin,
    };
    if (request.cf?.country) headers["X-Mosaicdeck-Country"] = request.cf.country;
    const res = await fetch("https://t.mosaicdeck.com/e", { method: "POST", headers, body: await request.text() });
    return new Response(null, { status: res.status });
  },
};

Next.js (route handler)

Works on Vercel, Netlify, Cloudflare and self-hosted Node. Add the token as an environment variable.

// app/relay/e/route.ts (Next.js App Router, any host)
export async function POST(request: Request) {
  const h = request.headers;
  const headers: Record<string, string> = {
    "Content-Type": "text/plain",
    "User-Agent": h.get("user-agent") ?? "",
    "X-Mosaicdeck-Proxy-Token": process.env.MOSAICDECK_PROXY_TOKEN!,
    "X-Mosaicdeck-Client-IP": (h.get("x-forwarded-for") ?? "").split(",")[0].trim() || (h.get("x-real-ip") ?? ""),
    "X-Mosaicdeck-Origin": h.get("origin") ?? new URL(request.url).origin,
  };
  const country = h.get("x-vercel-ip-country") ?? h.get("cf-ipcountry");
  if (country) headers["X-Mosaicdeck-Country"] = country;
  const res = await fetch("https://t.mosaicdeck.com/e", { method: "POST", headers, body: await request.text() });
  return new Response(null, { status: res.status });
}

Vercel Functions (without Next.js)

This file serves /api/relay/e, so use data-api="https://example.com/api/relay".

// api/relay/e.ts (Vercel Functions without Next.js)
export async function POST(request: Request) {
  const h = request.headers;
  const headers: Record<string, string> = {
    "Content-Type": "text/plain",
    "User-Agent": h.get("user-agent") ?? "",
    "X-Mosaicdeck-Proxy-Token": process.env.MOSAICDECK_PROXY_TOKEN!,
    "X-Mosaicdeck-Client-IP": (h.get("x-forwarded-for") ?? "").split(",")[0].trim(),
    "X-Mosaicdeck-Origin": h.get("origin") ?? new URL(request.url).origin,
  };
  const country = h.get("x-vercel-ip-country");
  if (country) headers["X-Mosaicdeck-Country"] = country;
  const res = await fetch("https://t.mosaicdeck.com/e", { method: "POST", headers, body: await request.text() });
  return new Response(null, { status: res.status });
}

Netlify Edge Functions

// netlify/edge-functions/relay.ts (Netlify Edge Functions)
import type { Config, Context } from "@netlify/edge-functions";

export default async (request: Request, context: Context) => {
  if (request.method !== "POST") return new Response(null, { status: 405 });
  const headers: Record<string, string> = {
    "Content-Type": "text/plain",
    "User-Agent": request.headers.get("user-agent") ?? "",
    "X-Mosaicdeck-Proxy-Token": Netlify.env.get("MOSAICDECK_PROXY_TOKEN") ?? "",
    "X-Mosaicdeck-Client-IP": context.ip,
    "X-Mosaicdeck-Origin": request.headers.get("origin") ?? new URL(request.url).origin,
  };
  if (context.geo?.country?.code) headers["X-Mosaicdeck-Country"] = context.geo.country.code;
  const res = await fetch("https://t.mosaicdeck.com/e", { method: "POST", headers, body: await request.text() });
  return new Response(null, { status: res.status });
};

export const config: Config = { path: "/relay/e" };

Troubleshooting

  • No data? Check that the page isn't on localhost, that the site's hostname is listed in site settings, and that an ad blocker isn't blocking the script (try the proxy).
  • Counts look low? Bots and requests without a browser user agent are filtered out.
  • Seeing an unlisted domain warning? Add the hostname in the site's settings, or ignore it if it isn't yours.