@jwiedeman/gtm-kit-next 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,343 @@
1
+ # @react-gtm-kit/next
2
+
3
+ [![CI](https://github.com/jwiedeman/react-gtm-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/jwiedeman/react-gtm-kit/actions/workflows/ci.yml)
4
+ [![Coverage](https://codecov.io/gh/jwiedeman/react-gtm-kit/graph/badge.svg?flag=next)](https://codecov.io/gh/jwiedeman/react-gtm-kit)
5
+ [![npm version](https://img.shields.io/npm/v/@react-gtm-kit/next.svg)](https://www.npmjs.com/package/@react-gtm-kit/next)
6
+ [![Bundle Size](https://img.shields.io/bundlephobia/minzip/@react-gtm-kit/next)](https://bundlephobia.com/package/@react-gtm-kit/next)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
+ [![Next.js](https://img.shields.io/badge/Next.js-13+-000000.svg?logo=next.js)](https://nextjs.org/)
10
+
11
+ **Next.js App Router components for Google Tag Manager. Server components ready.**
12
+
13
+ The Next.js adapter for GTM Kit - provides server components and route tracking hooks.
14
+
15
+ ---
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install @react-gtm-kit/core @react-gtm-kit/next
21
+ ```
22
+
23
+ ```bash
24
+ yarn add @react-gtm-kit/core @react-gtm-kit/next
25
+ ```
26
+
27
+ ```bash
28
+ pnpm add @react-gtm-kit/core @react-gtm-kit/next
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Quick Start
34
+
35
+ ### Step 1: Add to Layout
36
+
37
+ ```tsx
38
+ // app/layout.tsx
39
+ import { GtmHeadScript, GtmNoScript } from '@react-gtm-kit/next';
40
+
41
+ export default function RootLayout({ children }) {
42
+ return (
43
+ <html>
44
+ <head>
45
+ <GtmHeadScript containers="GTM-XXXXXX" />
46
+ </head>
47
+ <body>
48
+ <GtmNoScript containers="GTM-XXXXXX" />
49
+ {children}
50
+ </body>
51
+ </html>
52
+ );
53
+ }
54
+ ```
55
+
56
+ ### Step 2: Create Client Provider
57
+
58
+ ```tsx
59
+ // app/providers/gtm.tsx
60
+ 'use client';
61
+ import { createGtmClient } from '@react-gtm-kit/core';
62
+ import { useTrackPageViews } from '@react-gtm-kit/next';
63
+
64
+ const client = createGtmClient({ containers: 'GTM-XXXXXX' });
65
+ client.init();
66
+
67
+ export function GtmProvider({ children }) {
68
+ useTrackPageViews({ client }); // Auto-tracks route changes
69
+ return children;
70
+ }
71
+ ```
72
+
73
+ ### Step 3: Push Events
74
+
75
+ ```tsx
76
+ 'use client';
77
+ import { pushEvent } from '@react-gtm-kit/core';
78
+
79
+ // In any client component
80
+ function BuyButton({ client }) {
81
+ return <button onClick={() => pushEvent(client, 'purchase', { value: 49.99 })}>Buy Now</button>;
82
+ }
83
+ ```
84
+
85
+ ---
86
+
87
+ ## Features
88
+
89
+ | Feature | Description |
90
+ | ---------------------- | ------------------------------------------------------- |
91
+ | **Server Components** | `GtmHeadScript` and `GtmNoScript` are server components |
92
+ | **App Router** | Built for Next.js 13+ App Router |
93
+ | **Auto Page Tracking** | `useTrackPageViews` hook for route changes |
94
+ | **CSP Support** | Nonce support for Content Security Policy |
95
+ | **TypeScript** | Full type definitions included |
96
+ | **Lightweight** | Only what you need for Next.js |
97
+
98
+ ---
99
+
100
+ ## Server Components
101
+
102
+ ### `<GtmHeadScript />`
103
+
104
+ Renders the GTM script tag. Place in your `<head>`.
105
+
106
+ ```tsx
107
+ import { GtmHeadScript } from '@react-gtm-kit/next';
108
+
109
+ <GtmHeadScript
110
+ containers="GTM-XXXXXX"
111
+ scriptAttributes={{ nonce: 'your-csp-nonce' }} // Optional
112
+ />;
113
+ ```
114
+
115
+ ### `<GtmNoScript />`
116
+
117
+ Renders the noscript fallback iframe. Place at the start of `<body>`.
118
+
119
+ ```tsx
120
+ import { GtmNoScript } from '@react-gtm-kit/next';
121
+
122
+ <GtmNoScript containers="GTM-XXXXXX" />;
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Client Hooks
128
+
129
+ ### `useTrackPageViews()`
130
+
131
+ Automatically tracks page views on route changes.
132
+
133
+ ```tsx
134
+ 'use client';
135
+ import { useTrackPageViews } from '@react-gtm-kit/next';
136
+
137
+ export function GtmProvider({ children, client }) {
138
+ useTrackPageViews({ client });
139
+ return children;
140
+ }
141
+ ```
142
+
143
+ ---
144
+
145
+ ## Full Setup Example
146
+
147
+ ### 1. Root Layout
148
+
149
+ ```tsx
150
+ // app/layout.tsx
151
+ import { GtmHeadScript, GtmNoScript } from '@react-gtm-kit/next';
152
+ import { GtmProvider } from './providers/gtm';
153
+
154
+ export default function RootLayout({ children }) {
155
+ return (
156
+ <html>
157
+ <head>
158
+ <GtmHeadScript containers="GTM-XXXXXX" />
159
+ </head>
160
+ <body>
161
+ <GtmNoScript containers="GTM-XXXXXX" />
162
+ <GtmProvider>{children}</GtmProvider>
163
+ </body>
164
+ </html>
165
+ );
166
+ }
167
+ ```
168
+
169
+ ### 2. GTM Provider
170
+
171
+ ```tsx
172
+ // app/providers/gtm.tsx
173
+ 'use client';
174
+ import { createGtmClient } from '@react-gtm-kit/core';
175
+ import { useTrackPageViews } from '@react-gtm-kit/next';
176
+ import { createContext, useContext } from 'react';
177
+
178
+ const client = createGtmClient({ containers: 'GTM-XXXXXX' });
179
+ client.init();
180
+
181
+ const GtmContext = createContext(client);
182
+
183
+ export function GtmProvider({ children }) {
184
+ useTrackPageViews({ client });
185
+ return <GtmContext.Provider value={client}>{children}</GtmContext.Provider>;
186
+ }
187
+
188
+ export function useGtmClient() {
189
+ return useContext(GtmContext);
190
+ }
191
+ ```
192
+
193
+ ### 3. Use in Components
194
+
195
+ ```tsx
196
+ // app/components/BuyButton.tsx
197
+ 'use client';
198
+ import { pushEvent } from '@react-gtm-kit/core';
199
+ import { useGtmClient } from '../providers/gtm';
200
+
201
+ export function BuyButton() {
202
+ const client = useGtmClient();
203
+
204
+ return <button onClick={() => pushEvent(client, 'purchase', { value: 49.99 })}>Buy Now</button>;
205
+ }
206
+ ```
207
+
208
+ ---
209
+
210
+ ## Consent Mode v2 (GDPR)
211
+
212
+ ```tsx
213
+ // app/providers/gtm.tsx
214
+ 'use client';
215
+ import { createGtmClient, consentPresets } from '@react-gtm-kit/core';
216
+ import { useTrackPageViews } from '@react-gtm-kit/next';
217
+
218
+ const client = createGtmClient({ containers: 'GTM-XXXXXX' });
219
+
220
+ // Set consent defaults BEFORE init
221
+ client.setConsentDefaults(consentPresets.eeaDefault, { region: ['EEA'] });
222
+ client.init();
223
+
224
+ export function GtmProvider({ children }) {
225
+ useTrackPageViews({ client });
226
+ return children;
227
+ }
228
+
229
+ // Export for consent updates
230
+ export { client };
231
+ ```
232
+
233
+ ```tsx
234
+ // app/components/CookieBanner.tsx
235
+ 'use client';
236
+ import { client } from '../providers/gtm';
237
+ import { consentPresets } from '@react-gtm-kit/core';
238
+
239
+ export function CookieBanner() {
240
+ // Accept all tracking
241
+ const acceptAll = () => client.updateConsent(consentPresets.allGranted);
242
+
243
+ // Reject all tracking
244
+ const rejectAll = () => client.updateConsent(consentPresets.eeaDefault);
245
+
246
+ // Analytics only (mixed consent)
247
+ const analyticsOnly = () => client.updateConsent(consentPresets.analyticsOnly);
248
+
249
+ // Partial update - only change specific categories
250
+ const customChoice = () =>
251
+ client.updateConsent({
252
+ analytics_storage: 'granted',
253
+ ad_storage: 'denied'
254
+ });
255
+
256
+ return (
257
+ <div>
258
+ <button onClick={acceptAll}>Accept All</button>
259
+ <button onClick={rejectAll}>Reject All</button>
260
+ <button onClick={analyticsOnly}>Analytics Only</button>
261
+ </div>
262
+ );
263
+ }
264
+ ```
265
+
266
+ **Granular Updates** - Update individual categories without affecting others:
267
+
268
+ ```tsx
269
+ // User later changes ad preferences from settings page
270
+ client.updateConsent({ ad_storage: 'granted', ad_user_data: 'granted' });
271
+ // analytics_storage and ad_personalization remain unchanged
272
+ ```
273
+
274
+ ---
275
+
276
+ ## CSP (Content Security Policy)
277
+
278
+ For strict CSP configurations, pass a nonce:
279
+
280
+ ```tsx
281
+ // app/layout.tsx
282
+ import { headers } from 'next/headers';
283
+ import { GtmHeadScript, GtmNoScript } from '@react-gtm-kit/next';
284
+
285
+ export default function RootLayout({ children }) {
286
+ const nonce = headers().get('x-nonce') || '';
287
+
288
+ return (
289
+ <html>
290
+ <head>
291
+ <GtmHeadScript containers="GTM-XXXXXX" scriptAttributes={{ nonce }} />
292
+ </head>
293
+ <body>
294
+ <GtmNoScript containers="GTM-XXXXXX" />
295
+ {children}
296
+ </body>
297
+ </html>
298
+ );
299
+ }
300
+ ```
301
+
302
+ ---
303
+
304
+ ## Multiple Containers
305
+
306
+ ```tsx
307
+ <GtmHeadScript
308
+ containers={[{ id: 'GTM-MAIN' }, { id: 'GTM-ADS', queryParams: { gtm_auth: 'abc', gtm_preview: 'env-1' } }]}
309
+ />
310
+ ```
311
+
312
+ ---
313
+
314
+ ## Pages Router (Legacy)
315
+
316
+ For Next.js Pages Router, use `@react-gtm-kit/react-modern` instead:
317
+
318
+ ```tsx
319
+ // pages/_app.tsx
320
+ import { GtmProvider } from '@react-gtm-kit/react-modern';
321
+
322
+ export default function App({ Component, pageProps }) {
323
+ return (
324
+ <GtmProvider config={{ containers: 'GTM-XXXXXX' }}>
325
+ <Component {...pageProps} />
326
+ </GtmProvider>
327
+ );
328
+ }
329
+ ```
330
+
331
+ ---
332
+
333
+ ## Requirements
334
+
335
+ - Next.js 13.4+ (App Router)
336
+ - React 18+
337
+ - `@react-gtm-kit/core` (peer dependency)
338
+
339
+ ---
340
+
341
+ ## License
342
+
343
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var navigation = require('next/navigation');
5
+ var gtmKit = require('@jwiedeman/gtm-kit');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+
8
+ var Q="page_view",W=({pagePath:t,url:e,title:n})=>{let r={page_path:t,page_location:e};return n&&(r.page_title=n),r},K=(t,e)=>{var r;let n=typeof window!="undefined"&&((r=window.location)!=null&&r.origin)?window.location.origin:"";return n?`${n}${t}${e}`:`${t}${e}`},J=t=>t&&t.startsWith("#")?t:t?`#${t}`:"",X=({client:t,eventName:e=Q,buildPayload:n=W,includeSearchParams:r=!0,trackHash:i=!1,trackOnMount:l=!0,skipSamePath:o=!0,pushEventFn:f=gtmKit.pushEvent,waitForReady:d=!1,readyPromise:s})=>{if(!t)throw new Error("A GTM client is required to track page views.");let u=navigation.usePathname(),m=navigation.useSearchParams(),p=react.useMemo(()=>!r||!m?"":m.toString(),[r,m]),a=react.useRef(null),h=react.useRef(!1),S=react.useRef(null),k=react.useRef(d?s!=null?s:t.whenReady():null),T=react.useRef(!0);react.useEffect(()=>(T.current=!0,()=>{T.current=!1;}),[]),react.useEffect(()=>{k.current=d?s!=null?s:t.whenReady():null;},[t,s,d]);let D=react.useCallback(g=>{let c=g.filter(w=>w.status==="failed");if(!c.length)return;let v=c.map(w=>w.containerId).join(", ");console.error(`[react-gtm-kit] Failed to load GTM container script(s): ${v}`,c);},[]),E=react.useCallback((g,c,v)=>{if(!g)return;let w=i?J(v):"",C=c?`${g}?${c}`:g,R=`${C}${w}`,G=K(C,w);if(!l&&!h.current){a.current={key:R,pathname:g,search:c,hash:w,pagePath:C,url:G},h.current=!0;return}if(o&&a.current&&a.current.key===R)return;let O=typeof document!="undefined"?document.title:void 0,V=a.current?{pathname:a.current.pathname,search:a.current.search,hash:a.current.hash,pagePath:a.current.pagePath,url:a.current.url}:void 0,F={pathname:g,search:c,hash:w,pagePath:C,url:G,title:O,previous:V},M=()=>{let y=n(F);f(t,e,y),a.current={key:R,pathname:g,search:c,hash:w,pagePath:C,url:G},h.current=!0;};if(d&&k.current){S.current=R,k.current.then(y=>{!T.current||S.current!==R||(D(y),M());}).catch(y=>{!T.current||S.current!==R||(console.error("[react-gtm-kit] Error while waiting for GTM readiness.",y),M());});return}M();},[n,t,e,D,f,o,i,l,d]);react.useEffect(()=>{var c;if(typeof window=="undefined")return;let g=i&&(c=window.location.hash)!=null?c:"";E(u,p,g);},[E,u,p,i]),react.useEffect(()=>{if(!i||typeof window=="undefined")return;let g=()=>{var c;E(u,p,(c=window.location.hash)!=null?c:"");};return window.addEventListener("hashchange",g),()=>{window.removeEventListener("hashchange",g);}},[E,u,p,i]);};var L="https://www.googletagmanager.com",Z=t=>typeof t=="string",I=t=>Z(t)?{id:t}:t,_=t=>Array.isArray(t)?t.map(I):[I(t)],tt=t=>t?Object.entries(t).reduce((e,[n,r])=>(e[n]=String(r),e),{}):{},et=t=>t.endsWith("/")?t.slice(0,-1):t,N=(t,e,n,r,i=gtmKit.DEFAULT_DATA_LAYER_NAME)=>{let l=et(e),o=new URLSearchParams({id:n}),f=tt(r);i!==gtmKit.DEFAULT_DATA_LAYER_NAME&&f.l===void 0&&(f.l=i);for(let[s,u]of Object.entries(f))s!=="id"&&o.set(s,u);return `${l}/${t==="gtm"?"gtm.js":"ns.html"}?${o.toString()}`},$=(t,e,n,r=gtmKit.DEFAULT_DATA_LAYER_NAME)=>N("gtm",t,e,n,r),H=(t,e,n,r=gtmKit.DEFAULT_DATA_LAYER_NAME)=>N("ns",t,e,n,r);var nt=!0,ot=({containers:t,host:e=L,defaultQueryParams:n,scriptAttributes:r,dataLayerName:i=gtmKit.DEFAULT_DATA_LAYER_NAME})=>{let l=_(t);if(!l.length)throw new Error("At least one GTM container is required to render script tags.");return jsxRuntime.jsx(jsxRuntime.Fragment,{children:l.map(o=>{if(!o.id)throw new Error("Container id is required to render GTM script tags.");let f={...n,...o.queryParams},d=$(e,o.id,f,i),{async:s,defer:u,nonce:m,...p}=r!=null?r:{},a={src:d,async:s!=null?s:nt};u!==void 0&&(a.defer=u),m&&(a.nonce=m);for(let[h,S]of Object.entries(p))h==="async"||h==="defer"||h==="nonce"||h==="src"||S!=null&&(a[h]=S);return jsxRuntime.jsx("script",{"data-gtm-container-id":o.id,...a},o.id)})})};var ct=t=>String(t),ut=t=>t.split(";").map(e=>e.trim()).filter(Boolean).reduce((e,n)=>{let[r,i]=n.split(":");if(!r||i===void 0)return e;let l=r.trim().replace(/-([a-z])/g,(f,d)=>d.toUpperCase()),o=i.trim();return !l||!o||(e[l]=o),e},{}),pt=({containers:t,host:e=L,defaultQueryParams:n,iframeAttributes:r,dataLayerName:i=gtmKit.DEFAULT_DATA_LAYER_NAME})=>{let l=_(t);if(!l.length)throw new Error("At least one GTM container is required to render noscript markup.");return jsxRuntime.jsx(jsxRuntime.Fragment,{children:l.map(o=>{if(!o.id)throw new Error("Container id is required to render GTM noscript markup.");let f={...n,...o.queryParams},d=H(e,o.id,f,i),s={...gtmKit.DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,...r},u={src:d};for(let[m,p]of Object.entries(s))if(m!=="src"&&p!=null){if(m==="style"){typeof p=="string"?u.style=ut(p):typeof p=="object"&&(u.style=p);continue}u[m]=ct(p);}return jsxRuntime.jsx("noscript",{children:jsxRuntime.jsx("iframe",{...u})},o.id)})})};
9
+
10
+ exports.GtmHeadScript = ot;
11
+ exports.GtmNoScript = pt;
12
+ exports.useTrackPageViews = X;
13
+ //# sourceMappingURL=out.js.map
14
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/route-listener.ts","../src/head-script.tsx","../src/internal/container-helpers.ts","../src/noscript.tsx"],"names":["useCallback","useEffect","useMemo","useRef","usePathname","useSearchParams","pushEvent","DEFAULT_EVENT_NAME","defaultBuildPayload","pagePath","url","title","payload","buildUrl","hash","_a","origin","sanitizeHash","useTrackPageViews","client","eventName","buildPayload","includeSearchParams","trackHash","trackOnMount","skipSamePath","pushEventFn","waitForReady","readyPromise","pathname","searchParams","search","previousRef","hasTrackedRef","pendingKeyRef","readinessRef","isMountedRef","logFailures","states","failed","state","details","handleRouteChange","nextPathname","searchValue","rawHash","normalizedHash","key","previous","pushPayload","error","listener","DEFAULT_DATA_LAYER_NAME","DEFAULT_GTM_HOST","isString","value","normalizeContainer","input","normalizeContainers","containers","toRecord","params","acc","normalizeHost","host","kind","containerId","queryParams","dataLayerName","normalizedHost","buildScriptUrl","buildNoscriptUrl","Fragment","jsx","DEFAULT_ASYNC","GtmHeadScript","defaultQueryParams","scriptAttributes","normalized","container","src","asyncAttr","defer","nonce","restAttributes","scriptProps","attribute","DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES","toStringValue","parseStyle","style","chunk","declaration","property","rawValue","_","char","GtmNoScript","iframeAttributes","attributes","iframeProps"],"mappings":"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","sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useMemo, useRef } from 'react';\nimport { usePathname, useSearchParams } from 'next/navigation';\nimport type { GtmClient, PageViewPayload, ScriptLoadState } from '@jwiedeman/gtm-kit';\nimport { pushEvent } from '@jwiedeman/gtm-kit';\n\nconst DEFAULT_EVENT_NAME = 'page_view';\n\nexport interface RouteLocationSnapshot {\n pathname: string;\n search: string;\n hash: string;\n pagePath: string;\n url: string;\n}\n\nexport interface RouteChangeEventDetails extends RouteLocationSnapshot {\n title?: string;\n previous?: RouteLocationSnapshot;\n}\n\nexport type PageViewPayloadBuilder = (details: RouteChangeEventDetails) => PageViewPayload;\n\nexport interface UseTrackPageViewsOptions {\n client: Pick<GtmClient, 'push' | 'whenReady'>;\n eventName?: string;\n buildPayload?: PageViewPayloadBuilder;\n includeSearchParams?: boolean;\n trackHash?: boolean;\n trackOnMount?: boolean;\n skipSamePath?: boolean;\n pushEventFn?: typeof pushEvent;\n waitForReady?: boolean;\n readyPromise?: Promise<ScriptLoadState[]>;\n}\n\ninterface RouteSnapshot extends RouteLocationSnapshot {\n key: string;\n}\n\nconst defaultBuildPayload: PageViewPayloadBuilder = ({ pagePath, url, title }) => {\n const payload: PageViewPayload = {\n page_path: pagePath,\n page_location: url\n };\n\n if (title) {\n payload.page_title = title;\n }\n\n return payload;\n};\n\nconst buildUrl = (pagePath: string, hash: string): string => {\n const origin = typeof window !== 'undefined' && window.location?.origin ? window.location.origin : '';\n if (!origin) {\n return `${pagePath}${hash}`;\n }\n\n return `${origin}${pagePath}${hash}`;\n};\n\nconst sanitizeHash = (hash: string): string => (hash && hash.startsWith('#') ? hash : hash ? `#${hash}` : '');\n\nexport const useTrackPageViews = ({\n client,\n eventName = DEFAULT_EVENT_NAME,\n buildPayload = defaultBuildPayload,\n includeSearchParams = true,\n trackHash = false,\n trackOnMount = true,\n skipSamePath = true,\n pushEventFn = pushEvent,\n waitForReady = false,\n readyPromise\n}: UseTrackPageViewsOptions): void => {\n if (!client) {\n throw new Error('A GTM client is required to track page views.');\n }\n\n const pathname = usePathname();\n const searchParams = useSearchParams();\n\n const search = useMemo(() => {\n if (!includeSearchParams || !searchParams) {\n return '';\n }\n\n return searchParams.toString();\n }, [includeSearchParams, searchParams]);\n\n const previousRef = useRef<RouteSnapshot | null>(null);\n const hasTrackedRef = useRef(false);\n const pendingKeyRef = useRef<string | null>(null);\n const readinessRef = useRef<Promise<ScriptLoadState[]> | null>(\n waitForReady ? (readyPromise ?? client.whenReady()) : null\n );\n const isMountedRef = useRef(true);\n\n useEffect(() => {\n isMountedRef.current = true;\n return () => {\n isMountedRef.current = false;\n };\n }, []);\n\n useEffect(() => {\n readinessRef.current = waitForReady ? (readyPromise ?? client.whenReady()) : null;\n }, [client, readyPromise, waitForReady]);\n\n const logFailures = useCallback((states: ScriptLoadState[]) => {\n const failed = states.filter((state) => state.status === 'failed');\n if (!failed.length) {\n return;\n }\n\n const details = failed.map((state) => state.containerId).join(', ');\n // eslint-disable-next-line no-console\n console.error(`[react-gtm-kit] Failed to load GTM container script(s): ${details}`, failed);\n }, []);\n\n const handleRouteChange = useCallback(\n (nextPathname: string | null, searchValue: string, rawHash: string) => {\n if (!nextPathname) {\n return;\n }\n\n const normalizedHash = trackHash ? sanitizeHash(rawHash) : '';\n const pagePath = searchValue ? `${nextPathname}?${searchValue}` : nextPathname;\n const key = `${pagePath}${normalizedHash}`;\n const url = buildUrl(pagePath, normalizedHash);\n\n if (!trackOnMount && !hasTrackedRef.current) {\n previousRef.current = {\n key,\n pathname: nextPathname,\n search: searchValue,\n hash: normalizedHash,\n pagePath,\n url\n };\n hasTrackedRef.current = true;\n return;\n }\n\n if (skipSamePath && previousRef.current && previousRef.current.key === key) {\n return;\n }\n\n const title = typeof document !== 'undefined' ? document.title : undefined;\n const previous = previousRef.current\n ? {\n pathname: previousRef.current.pathname,\n search: previousRef.current.search,\n hash: previousRef.current.hash,\n pagePath: previousRef.current.pagePath,\n url: previousRef.current.url\n }\n : undefined;\n\n const details: RouteChangeEventDetails = {\n pathname: nextPathname,\n search: searchValue,\n hash: normalizedHash,\n pagePath,\n url,\n title,\n previous\n };\n\n const pushPayload = (): void => {\n const payload = buildPayload(details);\n pushEventFn(client, eventName, payload);\n\n previousRef.current = {\n key,\n pathname: nextPathname,\n search: searchValue,\n hash: normalizedHash,\n pagePath,\n url\n };\n hasTrackedRef.current = true;\n };\n\n if (waitForReady && readinessRef.current) {\n pendingKeyRef.current = key;\n readinessRef.current\n .then((states) => {\n if (!isMountedRef.current || pendingKeyRef.current !== key) {\n return;\n }\n\n logFailures(states);\n pushPayload();\n })\n .catch((error) => {\n if (!isMountedRef.current || pendingKeyRef.current !== key) {\n return;\n }\n // eslint-disable-next-line no-console\n console.error('[react-gtm-kit] Error while waiting for GTM readiness.', error);\n pushPayload();\n });\n\n return;\n }\n\n pushPayload();\n },\n [buildPayload, client, eventName, logFailures, pushEventFn, skipSamePath, trackHash, trackOnMount, waitForReady]\n );\n\n useEffect(() => {\n if (typeof window === 'undefined') {\n return;\n }\n\n const hash = trackHash ? (window.location.hash ?? '') : '';\n handleRouteChange(pathname, search, hash);\n }, [handleRouteChange, pathname, search, trackHash]);\n\n useEffect(() => {\n if (!trackHash || typeof window === 'undefined') {\n return;\n }\n\n const listener = (): void => {\n handleRouteChange(pathname, search, window.location.hash ?? '');\n };\n\n window.addEventListener('hashchange', listener);\n return () => {\n window.removeEventListener('hashchange', listener);\n };\n }, [handleRouteChange, pathname, search, trackHash]);\n};\n","import type React from 'react';\nimport { DEFAULT_DATA_LAYER_NAME } from '@jwiedeman/gtm-kit';\nimport type { ContainerConfigInput, ScriptAttributes } from '@jwiedeman/gtm-kit';\nimport { buildScriptUrl, DEFAULT_GTM_HOST, normalizeContainers } from './internal/container-helpers';\n\nexport interface GtmHeadScriptProps {\n containers: ContainerConfigInput | ContainerConfigInput[];\n host?: string;\n defaultQueryParams?: Record<string, string | number | boolean>;\n scriptAttributes?: ScriptAttributes;\n dataLayerName?: string;\n}\n\nconst DEFAULT_ASYNC = true;\n\nexport const GtmHeadScript = ({\n containers,\n host = DEFAULT_GTM_HOST,\n defaultQueryParams,\n scriptAttributes,\n dataLayerName = DEFAULT_DATA_LAYER_NAME\n}: GtmHeadScriptProps): React.ReactElement => {\n const normalized = normalizeContainers(containers);\n\n if (!normalized.length) {\n throw new Error('At least one GTM container is required to render script tags.');\n }\n\n return (\n <>\n {normalized.map((container) => {\n if (!container.id) {\n throw new Error('Container id is required to render GTM script tags.');\n }\n\n const params = {\n ...defaultQueryParams,\n ...container.queryParams\n };\n\n const src = buildScriptUrl(host, container.id, params, dataLayerName);\n const { async: asyncAttr, defer, nonce, ...restAttributes } = scriptAttributes ?? {};\n\n const scriptProps: React.ScriptHTMLAttributes<HTMLScriptElement> = {\n src,\n async: asyncAttr ?? DEFAULT_ASYNC\n };\n\n if (defer !== undefined) {\n scriptProps.defer = defer;\n }\n\n if (nonce) {\n scriptProps.nonce = nonce;\n }\n\n for (const [attribute, value] of Object.entries(restAttributes)) {\n if (attribute === 'async' || attribute === 'defer' || attribute === 'nonce' || attribute === 'src') {\n continue;\n }\n\n if (value === undefined || value === null) {\n continue;\n }\n\n (scriptProps as Record<string, unknown>)[attribute] = value;\n }\n\n return <script key={container.id} data-gtm-container-id={container.id} {...scriptProps} />;\n })}\n </>\n );\n};\n","import { DEFAULT_DATA_LAYER_NAME } from '@jwiedeman/gtm-kit';\nimport type { ContainerConfigInput, ContainerDescriptor } from '@jwiedeman/gtm-kit';\n\nexport const DEFAULT_GTM_HOST = 'https://www.googletagmanager.com';\n\nconst isString = (value: unknown): value is string => typeof value === 'string';\n\nexport const normalizeContainer = (input: ContainerConfigInput): ContainerDescriptor => {\n if (isString(input)) {\n return { id: input };\n }\n\n return input;\n};\n\nexport const normalizeContainers = (\n containers: ContainerConfigInput | ContainerConfigInput[]\n): ContainerDescriptor[] => {\n if (Array.isArray(containers)) {\n return containers.map(normalizeContainer);\n }\n\n return [normalizeContainer(containers)];\n};\n\nconst toRecord = (params?: Record<string, string | number | boolean>): Record<string, string> => {\n if (!params) {\n return {};\n }\n\n return Object.entries(params).reduce<Record<string, string>>((acc, [key, value]) => {\n acc[key] = String(value);\n return acc;\n }, {});\n};\n\nconst normalizeHost = (host: string): string => (host.endsWith('/') ? host.slice(0, -1) : host);\n\nconst buildUrl = (\n kind: 'gtm' | 'ns',\n host: string,\n containerId: string,\n queryParams?: Record<string, string | number | boolean>,\n dataLayerName: string = DEFAULT_DATA_LAYER_NAME\n): string => {\n const normalizedHost = normalizeHost(host);\n const searchParams = new URLSearchParams({ id: containerId });\n\n const params = toRecord(queryParams);\n if (dataLayerName !== DEFAULT_DATA_LAYER_NAME && params.l === undefined) {\n params.l = dataLayerName;\n }\n\n for (const [key, value] of Object.entries(params)) {\n if (key === 'id') {\n continue;\n }\n searchParams.set(key, value);\n }\n\n const suffix = kind === 'gtm' ? 'gtm.js' : 'ns.html';\n return `${normalizedHost}/${suffix}?${searchParams.toString()}`;\n};\n\nexport const buildScriptUrl = (\n host: string,\n containerId: string,\n queryParams?: Record<string, string | number | boolean>,\n dataLayerName: string = DEFAULT_DATA_LAYER_NAME\n): string => buildUrl('gtm', host, containerId, queryParams, dataLayerName);\n\nexport const buildNoscriptUrl = (\n host: string,\n containerId: string,\n queryParams?: Record<string, string | number | boolean>,\n dataLayerName: string = DEFAULT_DATA_LAYER_NAME\n): string => buildUrl('ns', host, containerId, queryParams, dataLayerName);\n","import type React from 'react';\nimport { DEFAULT_DATA_LAYER_NAME } from '@jwiedeman/gtm-kit';\nimport type { ContainerConfigInput } from '@jwiedeman/gtm-kit';\nimport { DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES } from '@jwiedeman/gtm-kit';\nimport { buildNoscriptUrl, DEFAULT_GTM_HOST, normalizeContainers } from './internal/container-helpers';\n\nexport interface GtmNoScriptProps {\n containers: ContainerConfigInput | ContainerConfigInput[];\n host?: string;\n defaultQueryParams?: Record<string, string | number | boolean>;\n iframeAttributes?: Record<string, string | number | boolean>;\n dataLayerName?: string;\n}\n\nconst toStringValue = (value: string | number | boolean): string => String(value);\n\nconst parseStyle = (style: string): React.CSSProperties => {\n return style\n .split(';')\n .map((chunk) => chunk.trim())\n .filter(Boolean)\n .reduce<React.CSSProperties>((acc, declaration) => {\n const [property, rawValue] = declaration.split(':');\n if (!property || rawValue === undefined) {\n return acc;\n }\n\n const key = property.trim().replace(/-([a-z])/g, (_, char: string) => char.toUpperCase());\n const value = rawValue.trim();\n if (!key || !value) {\n return acc;\n }\n\n (acc as Record<string, string>)[key] = value;\n return acc;\n }, {});\n};\n\nexport const GtmNoScript = ({\n containers,\n host = DEFAULT_GTM_HOST,\n defaultQueryParams,\n iframeAttributes,\n dataLayerName = DEFAULT_DATA_LAYER_NAME\n}: GtmNoScriptProps): React.ReactElement => {\n const normalized = normalizeContainers(containers);\n\n if (!normalized.length) {\n throw new Error('At least one GTM container is required to render noscript markup.');\n }\n\n return (\n <>\n {normalized.map((container) => {\n if (!container.id) {\n throw new Error('Container id is required to render GTM noscript markup.');\n }\n\n const params = {\n ...defaultQueryParams,\n ...container.queryParams\n };\n\n const src = buildNoscriptUrl(host, container.id, params, dataLayerName);\n const attributes = {\n ...DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,\n ...iframeAttributes\n };\n\n const iframeProps: React.IframeHTMLAttributes<HTMLIFrameElement> = {\n src\n };\n\n for (const [attribute, value] of Object.entries(attributes)) {\n if (attribute === 'src') {\n continue;\n }\n\n if (value === undefined || value === null) {\n continue;\n }\n\n if (attribute === 'style') {\n if (typeof value === 'string') {\n iframeProps.style = parseStyle(value);\n } else if (typeof value === 'object') {\n iframeProps.style = value as React.CSSProperties;\n }\n continue;\n }\n\n (iframeProps as Record<string, unknown>)[attribute] = toStringValue(value as string | number | boolean);\n }\n\n return (\n <noscript key={container.id}>\n <iframe {...iframeProps} />\n </noscript>\n );\n })}\n </>\n );\n};\n"]}
@@ -0,0 +1,48 @@
1
+ import { PageViewPayload, GtmClient, pushEvent, ScriptLoadState, ContainerConfigInput, ScriptAttributes } from '@jwiedeman/gtm-kit';
2
+ import React from 'react';
3
+
4
+ interface RouteLocationSnapshot {
5
+ pathname: string;
6
+ search: string;
7
+ hash: string;
8
+ pagePath: string;
9
+ url: string;
10
+ }
11
+ interface RouteChangeEventDetails extends RouteLocationSnapshot {
12
+ title?: string;
13
+ previous?: RouteLocationSnapshot;
14
+ }
15
+ type PageViewPayloadBuilder = (details: RouteChangeEventDetails) => PageViewPayload;
16
+ interface UseTrackPageViewsOptions {
17
+ client: Pick<GtmClient, 'push' | 'whenReady'>;
18
+ eventName?: string;
19
+ buildPayload?: PageViewPayloadBuilder;
20
+ includeSearchParams?: boolean;
21
+ trackHash?: boolean;
22
+ trackOnMount?: boolean;
23
+ skipSamePath?: boolean;
24
+ pushEventFn?: typeof pushEvent;
25
+ waitForReady?: boolean;
26
+ readyPromise?: Promise<ScriptLoadState[]>;
27
+ }
28
+ declare const useTrackPageViews: ({ client, eventName, buildPayload, includeSearchParams, trackHash, trackOnMount, skipSamePath, pushEventFn, waitForReady, readyPromise }: UseTrackPageViewsOptions) => void;
29
+
30
+ interface GtmHeadScriptProps {
31
+ containers: ContainerConfigInput | ContainerConfigInput[];
32
+ host?: string;
33
+ defaultQueryParams?: Record<string, string | number | boolean>;
34
+ scriptAttributes?: ScriptAttributes;
35
+ dataLayerName?: string;
36
+ }
37
+ declare const GtmHeadScript: ({ containers, host, defaultQueryParams, scriptAttributes, dataLayerName }: GtmHeadScriptProps) => React.ReactElement;
38
+
39
+ interface GtmNoScriptProps {
40
+ containers: ContainerConfigInput | ContainerConfigInput[];
41
+ host?: string;
42
+ defaultQueryParams?: Record<string, string | number | boolean>;
43
+ iframeAttributes?: Record<string, string | number | boolean>;
44
+ dataLayerName?: string;
45
+ }
46
+ declare const GtmNoScript: ({ containers, host, defaultQueryParams, iframeAttributes, dataLayerName }: GtmNoScriptProps) => React.ReactElement;
47
+
48
+ export { GtmHeadScript, GtmHeadScriptProps, GtmNoScript, GtmNoScriptProps, PageViewPayloadBuilder, RouteChangeEventDetails, RouteLocationSnapshot, UseTrackPageViewsOptions, useTrackPageViews };
@@ -0,0 +1,48 @@
1
+ import { PageViewPayload, GtmClient, pushEvent, ScriptLoadState, ContainerConfigInput, ScriptAttributes } from '@jwiedeman/gtm-kit';
2
+ import React from 'react';
3
+
4
+ interface RouteLocationSnapshot {
5
+ pathname: string;
6
+ search: string;
7
+ hash: string;
8
+ pagePath: string;
9
+ url: string;
10
+ }
11
+ interface RouteChangeEventDetails extends RouteLocationSnapshot {
12
+ title?: string;
13
+ previous?: RouteLocationSnapshot;
14
+ }
15
+ type PageViewPayloadBuilder = (details: RouteChangeEventDetails) => PageViewPayload;
16
+ interface UseTrackPageViewsOptions {
17
+ client: Pick<GtmClient, 'push' | 'whenReady'>;
18
+ eventName?: string;
19
+ buildPayload?: PageViewPayloadBuilder;
20
+ includeSearchParams?: boolean;
21
+ trackHash?: boolean;
22
+ trackOnMount?: boolean;
23
+ skipSamePath?: boolean;
24
+ pushEventFn?: typeof pushEvent;
25
+ waitForReady?: boolean;
26
+ readyPromise?: Promise<ScriptLoadState[]>;
27
+ }
28
+ declare const useTrackPageViews: ({ client, eventName, buildPayload, includeSearchParams, trackHash, trackOnMount, skipSamePath, pushEventFn, waitForReady, readyPromise }: UseTrackPageViewsOptions) => void;
29
+
30
+ interface GtmHeadScriptProps {
31
+ containers: ContainerConfigInput | ContainerConfigInput[];
32
+ host?: string;
33
+ defaultQueryParams?: Record<string, string | number | boolean>;
34
+ scriptAttributes?: ScriptAttributes;
35
+ dataLayerName?: string;
36
+ }
37
+ declare const GtmHeadScript: ({ containers, host, defaultQueryParams, scriptAttributes, dataLayerName }: GtmHeadScriptProps) => React.ReactElement;
38
+
39
+ interface GtmNoScriptProps {
40
+ containers: ContainerConfigInput | ContainerConfigInput[];
41
+ host?: string;
42
+ defaultQueryParams?: Record<string, string | number | boolean>;
43
+ iframeAttributes?: Record<string, string | number | boolean>;
44
+ dataLayerName?: string;
45
+ }
46
+ declare const GtmNoScript: ({ containers, host, defaultQueryParams, iframeAttributes, dataLayerName }: GtmNoScriptProps) => React.ReactElement;
47
+
48
+ export { GtmHeadScript, GtmHeadScriptProps, GtmNoScript, GtmNoScriptProps, PageViewPayloadBuilder, RouteChangeEventDetails, RouteLocationSnapshot, UseTrackPageViewsOptions, useTrackPageViews };
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ import { useMemo, useRef, useEffect, useCallback } from 'react';
2
+ import { usePathname, useSearchParams } from 'next/navigation';
3
+ import { pushEvent, DEFAULT_DATA_LAYER_NAME, DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES } from '@jwiedeman/gtm-kit';
4
+ import { jsx, Fragment } from 'react/jsx-runtime';
5
+
6
+ var Q="page_view",W=({pagePath:t,url:e,title:n})=>{let r={page_path:t,page_location:e};return n&&(r.page_title=n),r},K=(t,e)=>{var r;let n=typeof window!="undefined"&&((r=window.location)!=null&&r.origin)?window.location.origin:"";return n?`${n}${t}${e}`:`${t}${e}`},J=t=>t&&t.startsWith("#")?t:t?`#${t}`:"",X=({client:t,eventName:e=Q,buildPayload:n=W,includeSearchParams:r=!0,trackHash:i=!1,trackOnMount:l=!0,skipSamePath:o=!0,pushEventFn:f=pushEvent,waitForReady:d=!1,readyPromise:s})=>{if(!t)throw new Error("A GTM client is required to track page views.");let u=usePathname(),m=useSearchParams(),p=useMemo(()=>!r||!m?"":m.toString(),[r,m]),a=useRef(null),h=useRef(!1),S=useRef(null),k=useRef(d?s!=null?s:t.whenReady():null),T=useRef(!0);useEffect(()=>(T.current=!0,()=>{T.current=!1;}),[]),useEffect(()=>{k.current=d?s!=null?s:t.whenReady():null;},[t,s,d]);let D=useCallback(g=>{let c=g.filter(w=>w.status==="failed");if(!c.length)return;let v=c.map(w=>w.containerId).join(", ");console.error(`[react-gtm-kit] Failed to load GTM container script(s): ${v}`,c);},[]),E=useCallback((g,c,v)=>{if(!g)return;let w=i?J(v):"",C=c?`${g}?${c}`:g,R=`${C}${w}`,G=K(C,w);if(!l&&!h.current){a.current={key:R,pathname:g,search:c,hash:w,pagePath:C,url:G},h.current=!0;return}if(o&&a.current&&a.current.key===R)return;let O=typeof document!="undefined"?document.title:void 0,V=a.current?{pathname:a.current.pathname,search:a.current.search,hash:a.current.hash,pagePath:a.current.pagePath,url:a.current.url}:void 0,F={pathname:g,search:c,hash:w,pagePath:C,url:G,title:O,previous:V},M=()=>{let y=n(F);f(t,e,y),a.current={key:R,pathname:g,search:c,hash:w,pagePath:C,url:G},h.current=!0;};if(d&&k.current){S.current=R,k.current.then(y=>{!T.current||S.current!==R||(D(y),M());}).catch(y=>{!T.current||S.current!==R||(console.error("[react-gtm-kit] Error while waiting for GTM readiness.",y),M());});return}M();},[n,t,e,D,f,o,i,l,d]);useEffect(()=>{var c;if(typeof window=="undefined")return;let g=i&&(c=window.location.hash)!=null?c:"";E(u,p,g);},[E,u,p,i]),useEffect(()=>{if(!i||typeof window=="undefined")return;let g=()=>{var c;E(u,p,(c=window.location.hash)!=null?c:"");};return window.addEventListener("hashchange",g),()=>{window.removeEventListener("hashchange",g);}},[E,u,p,i]);};var L="https://www.googletagmanager.com",Z=t=>typeof t=="string",I=t=>Z(t)?{id:t}:t,_=t=>Array.isArray(t)?t.map(I):[I(t)],tt=t=>t?Object.entries(t).reduce((e,[n,r])=>(e[n]=String(r),e),{}):{},et=t=>t.endsWith("/")?t.slice(0,-1):t,N=(t,e,n,r,i=DEFAULT_DATA_LAYER_NAME)=>{let l=et(e),o=new URLSearchParams({id:n}),f=tt(r);i!==DEFAULT_DATA_LAYER_NAME&&f.l===void 0&&(f.l=i);for(let[s,u]of Object.entries(f))s!=="id"&&o.set(s,u);return `${l}/${t==="gtm"?"gtm.js":"ns.html"}?${o.toString()}`},$=(t,e,n,r=DEFAULT_DATA_LAYER_NAME)=>N("gtm",t,e,n,r),H=(t,e,n,r=DEFAULT_DATA_LAYER_NAME)=>N("ns",t,e,n,r);var nt=!0,ot=({containers:t,host:e=L,defaultQueryParams:n,scriptAttributes:r,dataLayerName:i=DEFAULT_DATA_LAYER_NAME})=>{let l=_(t);if(!l.length)throw new Error("At least one GTM container is required to render script tags.");return jsx(Fragment,{children:l.map(o=>{if(!o.id)throw new Error("Container id is required to render GTM script tags.");let f={...n,...o.queryParams},d=$(e,o.id,f,i),{async:s,defer:u,nonce:m,...p}=r!=null?r:{},a={src:d,async:s!=null?s:nt};u!==void 0&&(a.defer=u),m&&(a.nonce=m);for(let[h,S]of Object.entries(p))h==="async"||h==="defer"||h==="nonce"||h==="src"||S!=null&&(a[h]=S);return jsx("script",{"data-gtm-container-id":o.id,...a},o.id)})})};var ct=t=>String(t),ut=t=>t.split(";").map(e=>e.trim()).filter(Boolean).reduce((e,n)=>{let[r,i]=n.split(":");if(!r||i===void 0)return e;let l=r.trim().replace(/-([a-z])/g,(f,d)=>d.toUpperCase()),o=i.trim();return !l||!o||(e[l]=o),e},{}),pt=({containers:t,host:e=L,defaultQueryParams:n,iframeAttributes:r,dataLayerName:i=DEFAULT_DATA_LAYER_NAME})=>{let l=_(t);if(!l.length)throw new Error("At least one GTM container is required to render noscript markup.");return jsx(Fragment,{children:l.map(o=>{if(!o.id)throw new Error("Container id is required to render GTM noscript markup.");let f={...n,...o.queryParams},d=H(e,o.id,f,i),s={...DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,...r},u={src:d};for(let[m,p]of Object.entries(s))if(m!=="src"&&p!=null){if(m==="style"){typeof p=="string"?u.style=ut(p):typeof p=="object"&&(u.style=p);continue}u[m]=ct(p);}return jsx("noscript",{children:jsx("iframe",{...u})},o.id)})})};
7
+
8
+ export { ot as GtmHeadScript, pt as GtmNoScript, X as useTrackPageViews };
9
+ //# sourceMappingURL=out.js.map
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/route-listener.ts","../src/head-script.tsx","../src/internal/container-helpers.ts","../src/noscript.tsx"],"names":["useCallback","useEffect","useMemo","useRef","usePathname","useSearchParams","pushEvent","DEFAULT_EVENT_NAME","defaultBuildPayload","pagePath","url","title","payload","buildUrl","hash","_a","origin","sanitizeHash","useTrackPageViews","client","eventName","buildPayload","includeSearchParams","trackHash","trackOnMount","skipSamePath","pushEventFn","waitForReady","readyPromise","pathname","searchParams","search","previousRef","hasTrackedRef","pendingKeyRef","readinessRef","isMountedRef","logFailures","states","failed","state","details","handleRouteChange","nextPathname","searchValue","rawHash","normalizedHash","key","previous","pushPayload","error","listener","DEFAULT_DATA_LAYER_NAME","DEFAULT_GTM_HOST","isString","value","normalizeContainer","input","normalizeContainers","containers","toRecord","params","acc","normalizeHost","host","kind","containerId","queryParams","dataLayerName","normalizedHost","buildScriptUrl","buildNoscriptUrl","Fragment","jsx","DEFAULT_ASYNC","GtmHeadScript","defaultQueryParams","scriptAttributes","normalized","container","src","asyncAttr","defer","nonce","restAttributes","scriptProps","attribute","DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES","toStringValue","parseStyle","style","chunk","declaration","property","rawValue","_","char","GtmNoScript","iframeAttributes","attributes","iframeProps"],"mappings":"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","sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useMemo, useRef } from 'react';\nimport { usePathname, useSearchParams } from 'next/navigation';\nimport type { GtmClient, PageViewPayload, ScriptLoadState } from '@jwiedeman/gtm-kit';\nimport { pushEvent } from '@jwiedeman/gtm-kit';\n\nconst DEFAULT_EVENT_NAME = 'page_view';\n\nexport interface RouteLocationSnapshot {\n pathname: string;\n search: string;\n hash: string;\n pagePath: string;\n url: string;\n}\n\nexport interface RouteChangeEventDetails extends RouteLocationSnapshot {\n title?: string;\n previous?: RouteLocationSnapshot;\n}\n\nexport type PageViewPayloadBuilder = (details: RouteChangeEventDetails) => PageViewPayload;\n\nexport interface UseTrackPageViewsOptions {\n client: Pick<GtmClient, 'push' | 'whenReady'>;\n eventName?: string;\n buildPayload?: PageViewPayloadBuilder;\n includeSearchParams?: boolean;\n trackHash?: boolean;\n trackOnMount?: boolean;\n skipSamePath?: boolean;\n pushEventFn?: typeof pushEvent;\n waitForReady?: boolean;\n readyPromise?: Promise<ScriptLoadState[]>;\n}\n\ninterface RouteSnapshot extends RouteLocationSnapshot {\n key: string;\n}\n\nconst defaultBuildPayload: PageViewPayloadBuilder = ({ pagePath, url, title }) => {\n const payload: PageViewPayload = {\n page_path: pagePath,\n page_location: url\n };\n\n if (title) {\n payload.page_title = title;\n }\n\n return payload;\n};\n\nconst buildUrl = (pagePath: string, hash: string): string => {\n const origin = typeof window !== 'undefined' && window.location?.origin ? window.location.origin : '';\n if (!origin) {\n return `${pagePath}${hash}`;\n }\n\n return `${origin}${pagePath}${hash}`;\n};\n\nconst sanitizeHash = (hash: string): string => (hash && hash.startsWith('#') ? hash : hash ? `#${hash}` : '');\n\nexport const useTrackPageViews = ({\n client,\n eventName = DEFAULT_EVENT_NAME,\n buildPayload = defaultBuildPayload,\n includeSearchParams = true,\n trackHash = false,\n trackOnMount = true,\n skipSamePath = true,\n pushEventFn = pushEvent,\n waitForReady = false,\n readyPromise\n}: UseTrackPageViewsOptions): void => {\n if (!client) {\n throw new Error('A GTM client is required to track page views.');\n }\n\n const pathname = usePathname();\n const searchParams = useSearchParams();\n\n const search = useMemo(() => {\n if (!includeSearchParams || !searchParams) {\n return '';\n }\n\n return searchParams.toString();\n }, [includeSearchParams, searchParams]);\n\n const previousRef = useRef<RouteSnapshot | null>(null);\n const hasTrackedRef = useRef(false);\n const pendingKeyRef = useRef<string | null>(null);\n const readinessRef = useRef<Promise<ScriptLoadState[]> | null>(\n waitForReady ? (readyPromise ?? client.whenReady()) : null\n );\n const isMountedRef = useRef(true);\n\n useEffect(() => {\n isMountedRef.current = true;\n return () => {\n isMountedRef.current = false;\n };\n }, []);\n\n useEffect(() => {\n readinessRef.current = waitForReady ? (readyPromise ?? client.whenReady()) : null;\n }, [client, readyPromise, waitForReady]);\n\n const logFailures = useCallback((states: ScriptLoadState[]) => {\n const failed = states.filter((state) => state.status === 'failed');\n if (!failed.length) {\n return;\n }\n\n const details = failed.map((state) => state.containerId).join(', ');\n // eslint-disable-next-line no-console\n console.error(`[react-gtm-kit] Failed to load GTM container script(s): ${details}`, failed);\n }, []);\n\n const handleRouteChange = useCallback(\n (nextPathname: string | null, searchValue: string, rawHash: string) => {\n if (!nextPathname) {\n return;\n }\n\n const normalizedHash = trackHash ? sanitizeHash(rawHash) : '';\n const pagePath = searchValue ? `${nextPathname}?${searchValue}` : nextPathname;\n const key = `${pagePath}${normalizedHash}`;\n const url = buildUrl(pagePath, normalizedHash);\n\n if (!trackOnMount && !hasTrackedRef.current) {\n previousRef.current = {\n key,\n pathname: nextPathname,\n search: searchValue,\n hash: normalizedHash,\n pagePath,\n url\n };\n hasTrackedRef.current = true;\n return;\n }\n\n if (skipSamePath && previousRef.current && previousRef.current.key === key) {\n return;\n }\n\n const title = typeof document !== 'undefined' ? document.title : undefined;\n const previous = previousRef.current\n ? {\n pathname: previousRef.current.pathname,\n search: previousRef.current.search,\n hash: previousRef.current.hash,\n pagePath: previousRef.current.pagePath,\n url: previousRef.current.url\n }\n : undefined;\n\n const details: RouteChangeEventDetails = {\n pathname: nextPathname,\n search: searchValue,\n hash: normalizedHash,\n pagePath,\n url,\n title,\n previous\n };\n\n const pushPayload = (): void => {\n const payload = buildPayload(details);\n pushEventFn(client, eventName, payload);\n\n previousRef.current = {\n key,\n pathname: nextPathname,\n search: searchValue,\n hash: normalizedHash,\n pagePath,\n url\n };\n hasTrackedRef.current = true;\n };\n\n if (waitForReady && readinessRef.current) {\n pendingKeyRef.current = key;\n readinessRef.current\n .then((states) => {\n if (!isMountedRef.current || pendingKeyRef.current !== key) {\n return;\n }\n\n logFailures(states);\n pushPayload();\n })\n .catch((error) => {\n if (!isMountedRef.current || pendingKeyRef.current !== key) {\n return;\n }\n // eslint-disable-next-line no-console\n console.error('[react-gtm-kit] Error while waiting for GTM readiness.', error);\n pushPayload();\n });\n\n return;\n }\n\n pushPayload();\n },\n [buildPayload, client, eventName, logFailures, pushEventFn, skipSamePath, trackHash, trackOnMount, waitForReady]\n );\n\n useEffect(() => {\n if (typeof window === 'undefined') {\n return;\n }\n\n const hash = trackHash ? (window.location.hash ?? '') : '';\n handleRouteChange(pathname, search, hash);\n }, [handleRouteChange, pathname, search, trackHash]);\n\n useEffect(() => {\n if (!trackHash || typeof window === 'undefined') {\n return;\n }\n\n const listener = (): void => {\n handleRouteChange(pathname, search, window.location.hash ?? '');\n };\n\n window.addEventListener('hashchange', listener);\n return () => {\n window.removeEventListener('hashchange', listener);\n };\n }, [handleRouteChange, pathname, search, trackHash]);\n};\n","import type React from 'react';\nimport { DEFAULT_DATA_LAYER_NAME } from '@jwiedeman/gtm-kit';\nimport type { ContainerConfigInput, ScriptAttributes } from '@jwiedeman/gtm-kit';\nimport { buildScriptUrl, DEFAULT_GTM_HOST, normalizeContainers } from './internal/container-helpers';\n\nexport interface GtmHeadScriptProps {\n containers: ContainerConfigInput | ContainerConfigInput[];\n host?: string;\n defaultQueryParams?: Record<string, string | number | boolean>;\n scriptAttributes?: ScriptAttributes;\n dataLayerName?: string;\n}\n\nconst DEFAULT_ASYNC = true;\n\nexport const GtmHeadScript = ({\n containers,\n host = DEFAULT_GTM_HOST,\n defaultQueryParams,\n scriptAttributes,\n dataLayerName = DEFAULT_DATA_LAYER_NAME\n}: GtmHeadScriptProps): React.ReactElement => {\n const normalized = normalizeContainers(containers);\n\n if (!normalized.length) {\n throw new Error('At least one GTM container is required to render script tags.');\n }\n\n return (\n <>\n {normalized.map((container) => {\n if (!container.id) {\n throw new Error('Container id is required to render GTM script tags.');\n }\n\n const params = {\n ...defaultQueryParams,\n ...container.queryParams\n };\n\n const src = buildScriptUrl(host, container.id, params, dataLayerName);\n const { async: asyncAttr, defer, nonce, ...restAttributes } = scriptAttributes ?? {};\n\n const scriptProps: React.ScriptHTMLAttributes<HTMLScriptElement> = {\n src,\n async: asyncAttr ?? DEFAULT_ASYNC\n };\n\n if (defer !== undefined) {\n scriptProps.defer = defer;\n }\n\n if (nonce) {\n scriptProps.nonce = nonce;\n }\n\n for (const [attribute, value] of Object.entries(restAttributes)) {\n if (attribute === 'async' || attribute === 'defer' || attribute === 'nonce' || attribute === 'src') {\n continue;\n }\n\n if (value === undefined || value === null) {\n continue;\n }\n\n (scriptProps as Record<string, unknown>)[attribute] = value;\n }\n\n return <script key={container.id} data-gtm-container-id={container.id} {...scriptProps} />;\n })}\n </>\n );\n};\n","import { DEFAULT_DATA_LAYER_NAME } from '@jwiedeman/gtm-kit';\nimport type { ContainerConfigInput, ContainerDescriptor } from '@jwiedeman/gtm-kit';\n\nexport const DEFAULT_GTM_HOST = 'https://www.googletagmanager.com';\n\nconst isString = (value: unknown): value is string => typeof value === 'string';\n\nexport const normalizeContainer = (input: ContainerConfigInput): ContainerDescriptor => {\n if (isString(input)) {\n return { id: input };\n }\n\n return input;\n};\n\nexport const normalizeContainers = (\n containers: ContainerConfigInput | ContainerConfigInput[]\n): ContainerDescriptor[] => {\n if (Array.isArray(containers)) {\n return containers.map(normalizeContainer);\n }\n\n return [normalizeContainer(containers)];\n};\n\nconst toRecord = (params?: Record<string, string | number | boolean>): Record<string, string> => {\n if (!params) {\n return {};\n }\n\n return Object.entries(params).reduce<Record<string, string>>((acc, [key, value]) => {\n acc[key] = String(value);\n return acc;\n }, {});\n};\n\nconst normalizeHost = (host: string): string => (host.endsWith('/') ? host.slice(0, -1) : host);\n\nconst buildUrl = (\n kind: 'gtm' | 'ns',\n host: string,\n containerId: string,\n queryParams?: Record<string, string | number | boolean>,\n dataLayerName: string = DEFAULT_DATA_LAYER_NAME\n): string => {\n const normalizedHost = normalizeHost(host);\n const searchParams = new URLSearchParams({ id: containerId });\n\n const params = toRecord(queryParams);\n if (dataLayerName !== DEFAULT_DATA_LAYER_NAME && params.l === undefined) {\n params.l = dataLayerName;\n }\n\n for (const [key, value] of Object.entries(params)) {\n if (key === 'id') {\n continue;\n }\n searchParams.set(key, value);\n }\n\n const suffix = kind === 'gtm' ? 'gtm.js' : 'ns.html';\n return `${normalizedHost}/${suffix}?${searchParams.toString()}`;\n};\n\nexport const buildScriptUrl = (\n host: string,\n containerId: string,\n queryParams?: Record<string, string | number | boolean>,\n dataLayerName: string = DEFAULT_DATA_LAYER_NAME\n): string => buildUrl('gtm', host, containerId, queryParams, dataLayerName);\n\nexport const buildNoscriptUrl = (\n host: string,\n containerId: string,\n queryParams?: Record<string, string | number | boolean>,\n dataLayerName: string = DEFAULT_DATA_LAYER_NAME\n): string => buildUrl('ns', host, containerId, queryParams, dataLayerName);\n","import type React from 'react';\nimport { DEFAULT_DATA_LAYER_NAME } from '@jwiedeman/gtm-kit';\nimport type { ContainerConfigInput } from '@jwiedeman/gtm-kit';\nimport { DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES } from '@jwiedeman/gtm-kit';\nimport { buildNoscriptUrl, DEFAULT_GTM_HOST, normalizeContainers } from './internal/container-helpers';\n\nexport interface GtmNoScriptProps {\n containers: ContainerConfigInput | ContainerConfigInput[];\n host?: string;\n defaultQueryParams?: Record<string, string | number | boolean>;\n iframeAttributes?: Record<string, string | number | boolean>;\n dataLayerName?: string;\n}\n\nconst toStringValue = (value: string | number | boolean): string => String(value);\n\nconst parseStyle = (style: string): React.CSSProperties => {\n return style\n .split(';')\n .map((chunk) => chunk.trim())\n .filter(Boolean)\n .reduce<React.CSSProperties>((acc, declaration) => {\n const [property, rawValue] = declaration.split(':');\n if (!property || rawValue === undefined) {\n return acc;\n }\n\n const key = property.trim().replace(/-([a-z])/g, (_, char: string) => char.toUpperCase());\n const value = rawValue.trim();\n if (!key || !value) {\n return acc;\n }\n\n (acc as Record<string, string>)[key] = value;\n return acc;\n }, {});\n};\n\nexport const GtmNoScript = ({\n containers,\n host = DEFAULT_GTM_HOST,\n defaultQueryParams,\n iframeAttributes,\n dataLayerName = DEFAULT_DATA_LAYER_NAME\n}: GtmNoScriptProps): React.ReactElement => {\n const normalized = normalizeContainers(containers);\n\n if (!normalized.length) {\n throw new Error('At least one GTM container is required to render noscript markup.');\n }\n\n return (\n <>\n {normalized.map((container) => {\n if (!container.id) {\n throw new Error('Container id is required to render GTM noscript markup.');\n }\n\n const params = {\n ...defaultQueryParams,\n ...container.queryParams\n };\n\n const src = buildNoscriptUrl(host, container.id, params, dataLayerName);\n const attributes = {\n ...DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,\n ...iframeAttributes\n };\n\n const iframeProps: React.IframeHTMLAttributes<HTMLIFrameElement> = {\n src\n };\n\n for (const [attribute, value] of Object.entries(attributes)) {\n if (attribute === 'src') {\n continue;\n }\n\n if (value === undefined || value === null) {\n continue;\n }\n\n if (attribute === 'style') {\n if (typeof value === 'string') {\n iframeProps.style = parseStyle(value);\n } else if (typeof value === 'object') {\n iframeProps.style = value as React.CSSProperties;\n }\n continue;\n }\n\n (iframeProps as Record<string, unknown>)[attribute] = toStringValue(value as string | number | boolean);\n }\n\n return (\n <noscript key={container.id}>\n <iframe {...iframeProps} />\n </noscript>\n );\n })}\n </>\n );\n};\n"]}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@jwiedeman/gtm-kit-next",
3
+ "version": "1.0.1",
4
+ "description": "Next.js App Router helpers for GTM Kit - Google Tag Manager integration.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/jwiedeman/GTM-Kit.git",
8
+ "directory": "packages/next"
9
+ },
10
+ "author": "jwiedeman",
11
+ "keywords": [
12
+ "gtm",
13
+ "google-tag-manager",
14
+ "nextjs",
15
+ "next",
16
+ "app-router"
17
+ ],
18
+ "license": "MIT",
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "type": "module",
23
+ "main": "dist/index.cjs",
24
+ "module": "dist/index.js",
25
+ "types": "dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js",
30
+ "require": "./dist/index.cjs"
31
+ }
32
+ },
33
+ "files": [
34
+ "dist"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsup",
38
+ "clean": "rm -rf dist",
39
+ "lint": "eslint --max-warnings=0 \"src/**/*.{ts,tsx}\"",
40
+ "test": "jest --config ./jest.config.cjs --runInBand",
41
+ "typecheck": "tsc --noEmit"
42
+ },
43
+ "dependencies": {
44
+ "@jwiedeman/gtm-kit": "^1.0.1"
45
+ },
46
+ "peerDependencies": {
47
+ "next": "^13.4.0 || ^14.0.0 || ^15.0.0",
48
+ "react": "^18.0.0 || ^19.0.0"
49
+ },
50
+ "devDependencies": {
51
+ "@testing-library/jest-dom": "^6.4.2",
52
+ "@testing-library/react": "^14.2.1",
53
+ "@types/react": "^18.3.0",
54
+ "@types/react-dom": "^18.3.0",
55
+ "react": "^18.3.1",
56
+ "react-dom": "^18.3.1",
57
+ "tslib": "^2.6.2"
58
+ }
59
+ }