@customyai/site
v0.2.0
Published
Load the tags and pixels a Customy workspace configured for your site (GA4, Tag Manager, Meta, Clarity, Hotjar…) with consent gating, CSP helpers and server-side dedupe identifiers. Zero dependencies.
Maintainers
Readme
@customyai/site
Load the tags and pixels your Customy workspace configured for your site — Google Analytics 4, Tag Manager, Meta Pixel, Microsoft Clarity, Hotjar, LinkedIn Insight, TikTok, Plausible, or any external script — with consent gating, CSP helpers and the identifiers Customy needs to forward the same events server-side without double counting.
Zero dependencies. Works in any browser bundle; the manifest fetch also runs in Node 18+ for server-side rendering.
npm install @customyai/site1. Fetch the manifest (server-side)
The manifest is compiled by Customy Data for your source and read with its write key. Fetch it where the key lives — a server component, middleware, an API route — and cache it for a minute.
import { fetchTagManifest } from "@customyai/site";
const manifest = await fetchTagManifest({
url: "https://data.customy.ai/v1/collect/tags",
writeKey: process.env.CUSTOMY_DATA_WRITE_KEY!,
});fetchTagManifest honours etag (pass the previous revision) and returns
null on 304.
2. Keep your CSP strict
The manifest lists the origins each tag needs. Merge them into your policy instead of loosening it:
import { mergeCsp } from "@customyai/site";
response.headers.set(
"Content-Security-Policy",
mergeCsp("default-src 'self'; script-src 'self' 'nonce-…'; connect-src 'self'", manifest),
);3. Mount in the browser
import { createSiteTags } from "@customyai/site";
const tags = createSiteTags({
manifest, // from step 1, passed to the client
nonce, // your CSP nonce, if any
consent: () => ({ analytics: consent === "granted", marketing: false, functional: true }),
});
await tags.mount(); // loads only what is consented
tags.page(); // on every route change
tags.track("lead_submitted", { value: 1 }); // gtag / dataLayer / fbq / ttq / plausible…
await tags.updateConsent(); // after the visitor changes their choiceNothing is injected inline: vendor bootstraps (gtag, dataLayer, fbq,
ttq, clarity, hj, lintrk) are implemented in this package, and every
<script src> carries your nonce.
React
import { useSiteTags } from "@customyai/site/react";
const tags = useSiteTags({ manifest, nonce, consent: () => consent, consentKey: JSON.stringify(consent) });
useEffect(() => { tags?.page(); }, [tags, pathname]);4. Let Customy forward events server-side
When a tag runs in cloud mode, Customy Data forwards your collected events
to GA4 (Measurement Protocol) and Meta (Conversions API). Add the browser
identifiers to each event's context so both sides deduplicate:
sdk.send({
type: "track",
event: "lead_submitted",
messageId, // Meta uses it as event_id
anonymousId,
context: { ...tags.context(), application: "my-site" },
consent: { analytics: true, marketing: false },
});tags.context() returns { ga: { clientId, sessionId }, meta: { fbp, fbc }, tiktok: { ttclid, ttp }, linkedin: { liFatId }, google: { gclid }, page: { url, title, referrer } }.
Customy Data forwards to GA4, Meta, TikTok (Events API) and LinkedIn
(Conversions API); each forwarder reads its own identifiers from there.
Consent categories
Every tag has a category — analytics, marketing or functional — chosen
in the workspace. A tag mounts only when its category is true; GA4 additionally
receives Consent Mode v2 defaults and updates. Cloud forwarding applies the
same rule to the consent object of each event.
Withdrawal
updateConsent() after the visitor withdraws a category does what they asked:
the vendor is told to stop (ga-disable-<id>, fbq('consent','revoke'),
clarity('consent', false), ttq.disableCookie()), page()/track() stop
bridging to it, and its cookies on your domain are expired on the host and every
parent domain. The result lists them under revoked. Set
purgeCookiesOnRevoke: false if you purge yourself.
Regional defaults (Consent Mode v2)
import { EEA_REGIONS } from "@customyai/site";
createSiteTags({
manifest,
consent,
consentDefaults: {
regions: [{ regions: EEA_REGIONS, consent: {} }], // denied in EEA/UK/CH until the visitor decides
waitForUpdate: 500,
},
});The global default is whatever the visitor already granted (denied otherwise);
each regions entry becomes another gtag('consent','default', { region }).
ads_data_redaction follows the marketing decision and url_passthrough is on.
5. Serve vendor loaders from your own origin (optional)
// app/tags/js/route.ts (Next.js) — any Fetch-API runtime works
import { createTagScriptProxy } from "@customyai/site";
export const GET = createTagScriptProxy({ manifest: () => loadManifest() });
// browser
createSiteTags({ manifest, consent, scriptProxy: scriptProxyVia("/tags/js") });Only URLs that appear in the manifest are proxied (404 otherwise), no cookies
travel either way, and the response is cacheable. Vendor scripts still talk to
their own endpoints afterwards, so keep the connect-src origins from
mergeCsp.
License
MIT
