Quick start
Canopy takes about a minute to install. Add your site, paste one script tag (or mount one React component), and pageviews start flowing to your own domain — no cookies, no consent banner, no third-party requests. This page covers the three install paths and how to verify data is arriving.
1Add your site
Open the dashboard and create a site for the domain you want to measure — for example example.com. Canopy normalizes the domain (lowercase, no protocol, no www.) and derives your site ID from it:
example.com → canopy_example_com
The site ID is the only identifier the tracker needs. You can find it any time in your site settings, and it appears in the install snippets the dashboard generates for you.
2HTML script — any site
This is the universal install. It works on plain HTML, WordPress, Webflow, Ghost, Rails, Astro, or any SPA framework — anywhere you can add a tag to the <head>. The script is under 2kb gzipped, loads async, and never blocks rendering:
<script src="https://your-canopy.app/p.js" data-site-id="canopy_example_com" async ></script>
Replace your-canopy.app with the host where Canopy runs and canopy_example_com with your site ID. Once loaded, the script initializes itself: it sends a pageview on load, keeps sending pageviews as visitors navigate (including SPA route changes), and tracks clicks on outbound links. If Canopy runs on a different origin than your site, point events at it explicitly:
<script src="https://canopy.your-app.com/p.js" data-site-id="canopy_example_com" data-endpoint="https://canopy.your-app.com/api/canopy/event" async ></script>
Attributes
The script reads its configuration from data-* attributes on the tag:
| Attribute | Required | Description |
|---|---|---|
| data-site-id | yes | Your site ID, e.g. canopy_example_com. Without it the script does nothing. |
| data-endpoint | no | Where events are POSTed. Defaults to /api/canopy/event on the serving origin. |
| data-env | no | Optional label attached to every event, e.g. production or staging. |
| data-respect-dnt | no | Set to false to track visitors with Do Not Track enabled. DNT is respected by default. |
| data-disable-outbound | no | Presence of the attribute disables automatic outbound click tracking. |
3Next.js — @wnstn/canopy-nextjs
On Next.js, prefer the React SDK over the script tag. It ships as its own package (it depends on the core tracker for you) and adds a context provider, hooks, and SPA-aware pageviews wired into the App Router:
npm i @wnstn/canopy-nextjs
Wrap your root layout once — that single mount replaces the script tag entirely:
import { CanopyProvider } from "@wnstn/canopy-nextjs";
export default function RootLayout({ children }) {
return (
<html>
<body>
<CanopyProvider siteId={process.env.NEXT_PUBLIC_CANOPY_SITE_ID!} />
{children}
</body>
</html>
);
}The provider initializes the tracker on the client, sends the first pageview automatically, and keeps pageviews in sync as users navigate. From any client component, track events with the hook:
'use client';
import { useCanopyTrack } from "@wnstn/canopy-nextjs";
export function PricingButton() {
const { track } = useCanopyTrack();
return <button onClick={() => track("signup", { plan: "pro" })}>Upgrade</button>;
}'use client or event handlers. Full provider options and the hook list live in the SDK reference.4Vanilla JS — framework agnostic
If you bundle your own JavaScript and don't want the script tag, install the core tracker and initialize it directly. Calls made before initialization are queued and replayed once the tracker starts, so import order never matters:
import { createCanopy, track } from "@wnstn/canopy";
createCanopy({ siteId: "canopy_example_com" });
// anywhere in your app — even before createCanopy runs
track("checkout", { value: 49, currency: "USD" });5Verify it works
Data should appear within seconds of loading a page. Three quick checks:
/api/canopy/event returning 202.404 in production, or an extension is blocking requests. The API error table maps each status code to its cause.6Add custom events when ready
Pageviews and outbound clicks are automatic — everything else is one call away. Events carry arbitrary JSON properties that land in your database as JSONB, ready to filter in the event log:
track("signup", { plan: "pro", source: "header" });
track("checkout", { value: 49, currency: "USD" });
track("feature_used", { name: "export", format: "csv" });Recipes, naming rules, and server-side events are covered in the custom events guide.