@jwiedeman/gtm-kit-astro 1.2.0 → 1.3.0
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/dist/components/index.cjs +41 -1
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.d.cts +31 -1
- package/dist/components/index.d.ts +31 -1
- package/dist/components/index.js +42 -3
- package/dist/components/index.js.map +1 -1
- package/dist/index.cjs +11 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -3
- package/src/components/GtmHead.astro +0 -128
- package/src/components/GtmNoScript.astro +0 -56
- package/src/components/GtmScript.astro +0 -70
|
@@ -6,6 +6,9 @@ var gtmKit = require('@jwiedeman/gtm-kit');
|
|
|
6
6
|
var isValidJsIdentifier = (value) => {
|
|
7
7
|
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(value);
|
|
8
8
|
};
|
|
9
|
+
var escapeJsString = (value) => {
|
|
10
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/</g, "\\x3c").replace(/>/g, "\\x3e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
11
|
+
};
|
|
9
12
|
var filterNullish = (obj) => Object.fromEntries(Object.entries(obj).filter(([, v]) => v != null));
|
|
10
13
|
var generateScriptTags = (config) => {
|
|
11
14
|
const {
|
|
@@ -19,6 +22,11 @@ var generateScriptTags = (config) => {
|
|
|
19
22
|
if (!normalized.length) {
|
|
20
23
|
throw new Error("At least one GTM container is required.");
|
|
21
24
|
}
|
|
25
|
+
if (!isValidJsIdentifier(dataLayerName)) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`Invalid dataLayerName: "${dataLayerName}". Must be a valid JavaScript identifier.`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
22
30
|
return normalized.map((container) => {
|
|
23
31
|
if (!container.id) {
|
|
24
32
|
throw new Error("Container id is required.");
|
|
@@ -29,16 +37,47 @@ var generateScriptTags = (config) => {
|
|
|
29
37
|
};
|
|
30
38
|
const src = gtmKit.buildGtmScriptUrl(host, container.id, params, dataLayerName);
|
|
31
39
|
const { async: asyncAttr, defer, nonce, ...restAttributes } = scriptAttributes != null ? scriptAttributes : {};
|
|
40
|
+
escapeJsString(container.id);
|
|
41
|
+
const initScript = `window.${dataLayerName}=window.${dataLayerName}||[];window.${dataLayerName}.push({'gtm.start':new Date().getTime(),event:'gtm.js'});`;
|
|
32
42
|
return {
|
|
33
43
|
id: container.id,
|
|
34
44
|
src,
|
|
35
45
|
async: asyncAttr != null ? asyncAttr : true,
|
|
36
46
|
defer,
|
|
37
47
|
nonce,
|
|
38
|
-
attributes: filterNullish(restAttributes)
|
|
48
|
+
attributes: filterNullish(restAttributes),
|
|
49
|
+
initScript
|
|
39
50
|
};
|
|
40
51
|
});
|
|
41
52
|
};
|
|
53
|
+
var generateGtmScript = (config) => {
|
|
54
|
+
const {
|
|
55
|
+
containers,
|
|
56
|
+
host = gtmKit.DEFAULT_GTM_HOST,
|
|
57
|
+
defaultQueryParams,
|
|
58
|
+
dataLayerName = gtmKit.DEFAULT_DATA_LAYER_NAME
|
|
59
|
+
} = config;
|
|
60
|
+
const normalized = gtmKit.normalizeContainers(containers);
|
|
61
|
+
if (!normalized.length) {
|
|
62
|
+
throw new Error("At least one GTM container is required.");
|
|
63
|
+
}
|
|
64
|
+
if (!isValidJsIdentifier(dataLayerName)) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Invalid dataLayerName: "${dataLayerName}". Must be a valid JavaScript identifier.`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return normalized.map((container) => {
|
|
70
|
+
if (!container.id) {
|
|
71
|
+
throw new Error("Container id is required.");
|
|
72
|
+
}
|
|
73
|
+
const escapedId = escapeJsString(container.id);
|
|
74
|
+
dataLayerName !== gtmKit.DEFAULT_DATA_LAYER_NAME ? `,'${escapeJsString(dataLayerName)}'` : "";
|
|
75
|
+
const mergedParams = { ...defaultQueryParams, ...container.queryParams };
|
|
76
|
+
const extraParams = Object.entries(mergedParams).filter(([, v]) => v != null).map(([k, v]) => `&${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("");
|
|
77
|
+
const hostUrl = (host != null ? host : gtmKit.DEFAULT_GTM_HOST).replace(/\/+$/, "");
|
|
78
|
+
return `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='${escapeJsString(hostUrl)}/gtm.js?id='+i+dl${extraParams ? `+'${escapeJsString(extraParams)}'` : ""};f.parentNode.insertBefore(j,f);})(window,document,'script','${escapeJsString(dataLayerName)}','${escapedId}');`;
|
|
79
|
+
}).join("\n");
|
|
80
|
+
};
|
|
42
81
|
var generateNoscriptTags = (config) => {
|
|
43
82
|
const {
|
|
44
83
|
containers,
|
|
@@ -104,6 +143,7 @@ Object.defineProperty(exports, 'normalizeContainers', {
|
|
|
104
143
|
get: function () { return gtmKit.normalizeContainers; }
|
|
105
144
|
});
|
|
106
145
|
exports.generateDataLayerScript = generateDataLayerScript;
|
|
146
|
+
exports.generateGtmScript = generateGtmScript;
|
|
107
147
|
exports.generateNoscriptTags = generateNoscriptTags;
|
|
108
148
|
exports.generateScriptTags = generateScriptTags;
|
|
109
149
|
//# sourceMappingURL=out.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/helpers.ts","../../src/components/index.ts"],"names":["normalizeContainers","normalizeContainer","buildGtmScriptUrl","buildGtmNoscriptUrl"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAiBA,IAAM,sBAAsB,CAAC,UAA2B;AAE7D,SAAO,6BAA6B,KAAK,KAAK;AAChD;AAoBA,IAAM,gBAAgB,CAAoC,QACxD,OAAO,YAAY,OAAO,QAAQ,GAAG,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,MAAM,KAAK,IAAI,CAAC;AAuB9D,IAAM,qBAAqB,CAAC,WAA6C;AAC9E,QAAM;AAAA,IACJ;AAAA,IACA,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA,gBAAgB;AAAA,EAClB,IAAI;AAEJ,QAAM,aAAa,oBAAoB,UAAU;AAEjD,MAAI,CAAC,WAAW,QAAQ;AACtB,UAAM,IAAI,MAAM,yCAAyC;AAAA,EAC3D;AAEA,SAAO,WAAW,IAAI,CAAC,cAAc;AACnC,QAAI,CAAC,UAAU,IAAI;AACjB,YAAM,IAAI,MAAM,2BAA2B;AAAA,IAC7C;AAEA,UAAM,SAAS;AAAA,MACb,GAAG;AAAA,MACH,GAAG,UAAU;AAAA,IACf;AAEA,UAAM,MAAM,kBAAkB,MAAM,UAAU,IAAI,QAAQ,aAAa;AACvE,UAAM,EAAE,OAAO,WAAW,OAAO,OAAO,GAAG,eAAe,IAAI,8CAAoB,CAAC;AAEnF,WAAO;AAAA,MACL,IAAI,UAAU;AAAA,MACd;AAAA,MACA,OAAO,gCAAa;AAAA,MACpB;AAAA,MACA;AAAA,MACA,YAAY,cAAc,cAAc;AAAA,IAC1C;AAAA,EACF,CAAC;AACH;AAYO,IAAM,uBAAuB,CAClC,WAGsB;AACtB,QAAM;AAAA,IACJ;AAAA,IACA,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA,gBAAgB;AAAA,EAClB,IAAI;AAEJ,QAAM,aAAa,oBAAoB,UAAU;AAEjD,MAAI,CAAC,WAAW,QAAQ;AACtB,UAAM,IAAI,MAAM,yCAAyC;AAAA,EAC3D;AAEA,SAAO,WAAW,IAAI,CAAC,cAAc;AACnC,QAAI,CAAC,UAAU,IAAI;AACjB,YAAM,IAAI,MAAM,2BAA2B;AAAA,IAC7C;AAEA,UAAM,SAAS;AAAA,MACb,GAAG;AAAA,MACH,GAAG,UAAU;AAAA,IACf;AAEA,UAAM,MAAM,oBAAoB,MAAM,UAAU,IAAI,QAAQ,aAAa;AACzE,UAAM,mBAAmB;AAAA,MACvB,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAGA,UAAM,aAAa,OAAO;AAAA,MACxB,OAAO,QAAQ,gBAAgB,EAC5B,OAAO,CAAC,CAAC,EAAE,CAAC,MAAM,KAAK,IAAI,EAC3B,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;AAAA,IACnC;AAEA,WAAO;AAAA,MACL,IAAI,UAAU;AAAA,MACd;AAAA,MACA;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAMO,IAAM,0BAA0B,CAAC,gBAAwB,4BAAoC;AAClG,MAAI,CAAC,oBAAoB,aAAa,GAAG;AACvC,UAAM,IAAI;AAAA,MACR,2BAA2B,aAAa;AAAA,IAC1C;AAAA,EACF;AACA,SAAO,UAAU,aAAa,WAAW,aAAa;AACxD;;;ACnLA;AAAA,EACE,uBAAAA;AAAA,EACA,sBAAAC;AAAA,EACqB,qBAArBC;AAAA,EACuB,uBAAvBC;AAAA,OACK","sourcesContent":["import {\n DEFAULT_DATA_LAYER_NAME,\n DEFAULT_GTM_HOST,\n DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,\n normalizeContainer,\n normalizeContainers,\n buildGtmScriptUrl,\n buildGtmNoscriptUrl\n} from '@jwiedeman/gtm-kit';\nimport type { ContainerConfigInput, ScriptAttributes } from '@jwiedeman/gtm-kit';\n\n// Re-export for convenience\nexport {\n DEFAULT_GTM_HOST,\n DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,\n normalizeContainer,\n normalizeContainers,\n buildGtmScriptUrl as buildScriptUrl,\n buildGtmNoscriptUrl as buildNoscriptUrl\n};\n\n/**\n * Validate that a string is a valid JavaScript identifier.\n * This prevents XSS attacks through dataLayerName injection.\n */\nexport const isValidJsIdentifier = (value: string): boolean => {\n // Must be a valid JS identifier: starts with letter/$/_, followed by letters/digits/$/_\n return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(value);\n};\n\n/**\n * Escape a string for safe use in JavaScript string literals.\n * Prevents XSS when interpolating user-provided values into inline scripts.\n */\nexport const escapeJsString = (value: string): string => {\n return value\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/'/g, \"\\\\'\")\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, '\\\\n')\n .replace(/\\r/g, '\\\\r')\n .replace(/</g, '\\\\x3c')\n .replace(/>/g, '\\\\x3e')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029');\n};\n\n/** Filter out null/undefined values from an object */\nconst filterNullish = <T extends Record<string, unknown>>(obj: T): T =>\n Object.fromEntries(Object.entries(obj).filter(([, v]) => v != null)) as T;\n\nexport interface GtmScriptConfig {\n containers: ContainerConfigInput | ContainerConfigInput[];\n host?: string;\n defaultQueryParams?: Record<string, string | number | boolean>;\n scriptAttributes?: ScriptAttributes;\n dataLayerName?: string;\n}\n\nexport interface ScriptTagData {\n id: string;\n src: string;\n async: boolean;\n defer?: boolean;\n nonce?: string;\n attributes: Record<string, string | boolean>;\n}\n\n/**\n * Generate script tag data for GTM containers.\n * Used by Astro components to render script tags.\n */\nexport const generateScriptTags = (config: GtmScriptConfig): ScriptTagData[] => {\n const {\n containers,\n host = DEFAULT_GTM_HOST,\n defaultQueryParams,\n scriptAttributes,\n dataLayerName = DEFAULT_DATA_LAYER_NAME\n } = config;\n\n const normalized = normalizeContainers(containers);\n\n if (!normalized.length) {\n throw new Error('At least one GTM container is required.');\n }\n\n return normalized.map((container) => {\n if (!container.id) {\n throw new Error('Container id is required.');\n }\n\n const params = {\n ...defaultQueryParams,\n ...container.queryParams\n };\n\n const src = buildGtmScriptUrl(host, container.id, params, dataLayerName);\n const { async: asyncAttr, defer, nonce, ...restAttributes } = scriptAttributes ?? {};\n\n return {\n id: container.id,\n src,\n async: asyncAttr ?? true,\n defer,\n nonce,\n attributes: filterNullish(restAttributes) as Record<string, string | boolean>\n };\n });\n};\n\nexport interface NoscriptTagData {\n id: string;\n src: string;\n attributes: Record<string, string>;\n}\n\n/**\n * Generate noscript iframe data for GTM containers.\n * Used by Astro components to render noscript fallbacks.\n */\nexport const generateNoscriptTags = (\n config: Omit<GtmScriptConfig, 'scriptAttributes'> & {\n iframeAttributes?: Record<string, string | number | boolean>;\n }\n): NoscriptTagData[] => {\n const {\n containers,\n host = DEFAULT_GTM_HOST,\n defaultQueryParams,\n iframeAttributes,\n dataLayerName = DEFAULT_DATA_LAYER_NAME\n } = config;\n\n const normalized = normalizeContainers(containers);\n\n if (!normalized.length) {\n throw new Error('At least one GTM container is required.');\n }\n\n return normalized.map((container) => {\n if (!container.id) {\n throw new Error('Container id is required.');\n }\n\n const params = {\n ...defaultQueryParams,\n ...container.queryParams\n };\n\n const src = buildGtmNoscriptUrl(host, container.id, params, dataLayerName);\n const mergedAttributes = {\n ...DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,\n ...iframeAttributes\n };\n\n // Filter nullish values and convert to strings\n const attributes = Object.fromEntries(\n Object.entries(mergedAttributes)\n .filter(([, v]) => v != null)\n .map(([k, v]) => [k, String(v)])\n ) as Record<string, string>;\n\n return {\n id: container.id,\n src,\n attributes\n };\n });\n};\n\n/**\n * Generate the dataLayer initialization script.\n * @throws {Error} If dataLayerName is not a valid JavaScript identifier\n */\nexport const generateDataLayerScript = (dataLayerName: string = DEFAULT_DATA_LAYER_NAME): string => {\n if (!isValidJsIdentifier(dataLayerName)) {\n throw new Error(\n `Invalid dataLayerName: \"${dataLayerName}\". Must be a valid JavaScript identifier (letters, digits, $, _ only, cannot start with a digit).`\n );\n }\n return `window.${dataLayerName}=window.${dataLayerName}||[];`;\n};\n","export { generateScriptTags, generateNoscriptTags, generateDataLayerScript, DEFAULT_GTM_HOST } from './helpers';\n\n// Re-export URL utilities from core for backwards compatibility\nexport {\n normalizeContainers,\n normalizeContainer,\n buildGtmScriptUrl as buildScriptUrl,\n buildGtmNoscriptUrl as buildNoscriptUrl\n} from '@jwiedeman/gtm-kit';\n\nexport type { GtmScriptConfig, ScriptTagData, NoscriptTagData } from './helpers';\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/helpers.ts","../../src/components/index.ts"],"names":["normalizeContainers","normalizeContainer","buildGtmScriptUrl","buildGtmNoscriptUrl"],"mappings":"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","sourcesContent":["import {\n DEFAULT_DATA_LAYER_NAME,\n DEFAULT_GTM_HOST,\n DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,\n normalizeContainer,\n normalizeContainers,\n buildGtmScriptUrl,\n buildGtmNoscriptUrl\n} from '@jwiedeman/gtm-kit';\nimport type { ContainerConfigInput, ScriptAttributes } from '@jwiedeman/gtm-kit';\n\n// Re-export for convenience\nexport {\n DEFAULT_GTM_HOST,\n DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,\n normalizeContainer,\n normalizeContainers,\n buildGtmScriptUrl as buildScriptUrl,\n buildGtmNoscriptUrl as buildNoscriptUrl\n};\n\n/**\n * Validate that a string is a valid JavaScript identifier.\n * This prevents XSS attacks through dataLayerName injection.\n */\nexport const isValidJsIdentifier = (value: string): boolean => {\n // Must be a valid JS identifier: starts with letter/$/_, followed by letters/digits/$/_\n return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(value);\n};\n\n/**\n * Escape a string for safe use in JavaScript string literals.\n * Prevents XSS when interpolating user-provided values into inline scripts.\n */\nexport const escapeJsString = (value: string): string => {\n return value\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/'/g, \"\\\\'\")\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, '\\\\n')\n .replace(/\\r/g, '\\\\r')\n .replace(/</g, '\\\\x3c')\n .replace(/>/g, '\\\\x3e')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029');\n};\n\n/** Filter out null/undefined values from an object */\nconst filterNullish = <T extends Record<string, unknown>>(obj: T): T =>\n Object.fromEntries(Object.entries(obj).filter(([, v]) => v != null)) as T;\n\nexport interface GtmScriptConfig {\n containers: ContainerConfigInput | ContainerConfigInput[];\n host?: string;\n defaultQueryParams?: Record<string, string | number | boolean>;\n scriptAttributes?: ScriptAttributes;\n dataLayerName?: string;\n}\n\nexport interface ScriptTagData {\n id: string;\n src: string;\n async: boolean;\n defer?: boolean;\n nonce?: string;\n attributes: Record<string, string | boolean>;\n /**\n * Inline JavaScript that must be executed before the GTM script loads.\n * Initializes the dataLayer and pushes the gtm.start event, which is\n * required for GTM's built-in \"All Pages\" trigger to fire.\n *\n * Render this as a `<script>` tag (or `<script is:inline>` in Astro)\n * immediately before the GTM `<script async src=\"...\">` tag.\n */\n initScript: string;\n}\n\n/**\n * Generate script tag data for GTM containers.\n * Used by Astro components to render script tags.\n *\n * Each tag includes an `initScript` property containing the dataLayer\n * initialization and gtm.start event push. This MUST be rendered as an\n * inline script before the GTM script tag, or GTM's \"All Pages\" trigger\n * will not fire.\n */\nexport const generateScriptTags = (config: GtmScriptConfig): ScriptTagData[] => {\n const {\n containers,\n host = DEFAULT_GTM_HOST,\n defaultQueryParams,\n scriptAttributes,\n dataLayerName = DEFAULT_DATA_LAYER_NAME\n } = config;\n\n const normalized = normalizeContainers(containers);\n\n if (!normalized.length) {\n throw new Error('At least one GTM container is required.');\n }\n\n if (!isValidJsIdentifier(dataLayerName)) {\n throw new Error(\n `Invalid dataLayerName: \"${dataLayerName}\". Must be a valid JavaScript identifier.`\n );\n }\n\n return normalized.map((container) => {\n if (!container.id) {\n throw new Error('Container id is required.');\n }\n\n const params = {\n ...defaultQueryParams,\n ...container.queryParams\n };\n\n const src = buildGtmScriptUrl(host, container.id, params, dataLayerName);\n const { async: asyncAttr, defer, nonce, ...restAttributes } = scriptAttributes ?? {};\n\n // Generate the standard GTM initialization script.\n // This pushes the gtm.start event to the dataLayer, which is required\n // for GTM to trigger its built-in \"All Pages\" / \"Container Loaded\" trigger.\n const escapedId = escapeJsString(container.id);\n const dlParam = dataLayerName !== DEFAULT_DATA_LAYER_NAME ? `'&l='+l` : `''`;\n const initScript =\n `window.${dataLayerName}=window.${dataLayerName}||[];` +\n `window.${dataLayerName}.push({'gtm.start':new Date().getTime(),event:'gtm.js'});`;\n\n return {\n id: container.id,\n src,\n async: asyncAttr ?? true,\n defer,\n nonce,\n attributes: filterNullish(restAttributes) as Record<string, string | boolean>,\n initScript\n };\n });\n};\n\n/**\n * Generate the complete standard GTM snippet as an inline script string.\n *\n * This produces the Google-recommended GTM installation code that:\n * 1. Initializes the dataLayer array\n * 2. Pushes the gtm.start event (required for \"All Pages\" trigger)\n * 3. Dynamically creates and inserts the GTM script element\n *\n * Usage in Astro:\n * ```astro\n * <script is:inline set:html={generateGtmScript({ containers: 'GTM-XXXX' })} />\n * ```\n *\n * For the noscript fallback, use `generateNoscriptTags()` separately.\n */\nexport const generateGtmScript = (config: Omit<GtmScriptConfig, 'scriptAttributes'>): string => {\n const {\n containers,\n host = DEFAULT_GTM_HOST,\n defaultQueryParams,\n dataLayerName = DEFAULT_DATA_LAYER_NAME\n } = config;\n\n const normalized = normalizeContainers(containers);\n\n if (!normalized.length) {\n throw new Error('At least one GTM container is required.');\n }\n\n if (!isValidJsIdentifier(dataLayerName)) {\n throw new Error(\n `Invalid dataLayerName: \"${dataLayerName}\". Must be a valid JavaScript identifier.`\n );\n }\n\n // Generate one standard snippet per container\n return normalized.map((container) => {\n if (!container.id) {\n throw new Error('Container id is required.');\n }\n\n const escapedId = escapeJsString(container.id);\n const dlArg = dataLayerName !== DEFAULT_DATA_LAYER_NAME\n ? `,'${escapeJsString(dataLayerName)}'`\n : '';\n\n // Build query params string for additional params (auth, preview, etc.)\n const mergedParams = { ...defaultQueryParams, ...container.queryParams };\n const extraParams = Object.entries(mergedParams)\n .filter(([, v]) => v != null)\n .map(([k, v]) => `&${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)\n .join('');\n\n const hostUrl = (host ?? DEFAULT_GTM_HOST).replace(/\\/+$/, '');\n\n // Standard GTM snippet — matches Google's recommended installation\n return `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':` +\n `new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],` +\n `j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=` +\n `'${escapeJsString(hostUrl)}/gtm.js?id='+i+dl${extraParams ? `+'${escapeJsString(extraParams)}'` : ''};f.parentNode.insertBefore(j,f);` +\n `})(window,document,'script','${escapeJsString(dataLayerName)}','${escapedId}');`;\n }).join('\\n');\n};\n\nexport interface NoscriptTagData {\n id: string;\n src: string;\n attributes: Record<string, string>;\n}\n\n/**\n * Generate noscript iframe data for GTM containers.\n * Used by Astro components to render noscript fallbacks.\n */\nexport const generateNoscriptTags = (\n config: Omit<GtmScriptConfig, 'scriptAttributes'> & {\n iframeAttributes?: Record<string, string | number | boolean>;\n }\n): NoscriptTagData[] => {\n const {\n containers,\n host = DEFAULT_GTM_HOST,\n defaultQueryParams,\n iframeAttributes,\n dataLayerName = DEFAULT_DATA_LAYER_NAME\n } = config;\n\n const normalized = normalizeContainers(containers);\n\n if (!normalized.length) {\n throw new Error('At least one GTM container is required.');\n }\n\n return normalized.map((container) => {\n if (!container.id) {\n throw new Error('Container id is required.');\n }\n\n const params = {\n ...defaultQueryParams,\n ...container.queryParams\n };\n\n const src = buildGtmNoscriptUrl(host, container.id, params, dataLayerName);\n const mergedAttributes = {\n ...DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,\n ...iframeAttributes\n };\n\n // Filter nullish values and convert to strings\n const attributes = Object.fromEntries(\n Object.entries(mergedAttributes)\n .filter(([, v]) => v != null)\n .map(([k, v]) => [k, String(v)])\n ) as Record<string, string>;\n\n return {\n id: container.id,\n src,\n attributes\n };\n });\n};\n\n/**\n * Generate the dataLayer initialization script.\n * @throws {Error} If dataLayerName is not a valid JavaScript identifier\n */\nexport const generateDataLayerScript = (dataLayerName: string = DEFAULT_DATA_LAYER_NAME): string => {\n if (!isValidJsIdentifier(dataLayerName)) {\n throw new Error(\n `Invalid dataLayerName: \"${dataLayerName}\". Must be a valid JavaScript identifier (letters, digits, $, _ only, cannot start with a digit).`\n );\n }\n return `window.${dataLayerName}=window.${dataLayerName}||[];`;\n};\n","export { generateScriptTags, generateNoscriptTags, generateDataLayerScript, generateGtmScript, DEFAULT_GTM_HOST } from './helpers';\n\n// Re-export URL utilities from core for backwards compatibility\nexport {\n normalizeContainers,\n normalizeContainer,\n buildGtmScriptUrl as buildScriptUrl,\n buildGtmNoscriptUrl as buildNoscriptUrl\n} from '@jwiedeman/gtm-kit';\n\nexport type { GtmScriptConfig, ScriptTagData, NoscriptTagData } from './helpers';\n"]}
|
|
@@ -15,12 +15,42 @@ interface ScriptTagData {
|
|
|
15
15
|
defer?: boolean;
|
|
16
16
|
nonce?: string;
|
|
17
17
|
attributes: Record<string, string | boolean>;
|
|
18
|
+
/**
|
|
19
|
+
* Inline JavaScript that must be executed before the GTM script loads.
|
|
20
|
+
* Initializes the dataLayer and pushes the gtm.start event, which is
|
|
21
|
+
* required for GTM's built-in "All Pages" trigger to fire.
|
|
22
|
+
*
|
|
23
|
+
* Render this as a `<script>` tag (or `<script is:inline>` in Astro)
|
|
24
|
+
* immediately before the GTM `<script async src="...">` tag.
|
|
25
|
+
*/
|
|
26
|
+
initScript: string;
|
|
18
27
|
}
|
|
19
28
|
/**
|
|
20
29
|
* Generate script tag data for GTM containers.
|
|
21
30
|
* Used by Astro components to render script tags.
|
|
31
|
+
*
|
|
32
|
+
* Each tag includes an `initScript` property containing the dataLayer
|
|
33
|
+
* initialization and gtm.start event push. This MUST be rendered as an
|
|
34
|
+
* inline script before the GTM script tag, or GTM's "All Pages" trigger
|
|
35
|
+
* will not fire.
|
|
22
36
|
*/
|
|
23
37
|
declare const generateScriptTags: (config: GtmScriptConfig) => ScriptTagData[];
|
|
38
|
+
/**
|
|
39
|
+
* Generate the complete standard GTM snippet as an inline script string.
|
|
40
|
+
*
|
|
41
|
+
* This produces the Google-recommended GTM installation code that:
|
|
42
|
+
* 1. Initializes the dataLayer array
|
|
43
|
+
* 2. Pushes the gtm.start event (required for "All Pages" trigger)
|
|
44
|
+
* 3. Dynamically creates and inserts the GTM script element
|
|
45
|
+
*
|
|
46
|
+
* Usage in Astro:
|
|
47
|
+
* ```astro
|
|
48
|
+
* <script is:inline set:html={generateGtmScript({ containers: 'GTM-XXXX' })} />
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* For the noscript fallback, use `generateNoscriptTags()` separately.
|
|
52
|
+
*/
|
|
53
|
+
declare const generateGtmScript: (config: Omit<GtmScriptConfig, 'scriptAttributes'>) => string;
|
|
24
54
|
interface NoscriptTagData {
|
|
25
55
|
id: string;
|
|
26
56
|
src: string;
|
|
@@ -39,4 +69,4 @@ declare const generateNoscriptTags: (config: Omit<GtmScriptConfig, 'scriptAttrib
|
|
|
39
69
|
*/
|
|
40
70
|
declare const generateDataLayerScript: (dataLayerName?: string) => string;
|
|
41
71
|
|
|
42
|
-
export { GtmScriptConfig, NoscriptTagData, ScriptTagData, generateDataLayerScript, generateNoscriptTags, generateScriptTags };
|
|
72
|
+
export { GtmScriptConfig, NoscriptTagData, ScriptTagData, generateDataLayerScript, generateGtmScript, generateNoscriptTags, generateScriptTags };
|
|
@@ -15,12 +15,42 @@ interface ScriptTagData {
|
|
|
15
15
|
defer?: boolean;
|
|
16
16
|
nonce?: string;
|
|
17
17
|
attributes: Record<string, string | boolean>;
|
|
18
|
+
/**
|
|
19
|
+
* Inline JavaScript that must be executed before the GTM script loads.
|
|
20
|
+
* Initializes the dataLayer and pushes the gtm.start event, which is
|
|
21
|
+
* required for GTM's built-in "All Pages" trigger to fire.
|
|
22
|
+
*
|
|
23
|
+
* Render this as a `<script>` tag (or `<script is:inline>` in Astro)
|
|
24
|
+
* immediately before the GTM `<script async src="...">` tag.
|
|
25
|
+
*/
|
|
26
|
+
initScript: string;
|
|
18
27
|
}
|
|
19
28
|
/**
|
|
20
29
|
* Generate script tag data for GTM containers.
|
|
21
30
|
* Used by Astro components to render script tags.
|
|
31
|
+
*
|
|
32
|
+
* Each tag includes an `initScript` property containing the dataLayer
|
|
33
|
+
* initialization and gtm.start event push. This MUST be rendered as an
|
|
34
|
+
* inline script before the GTM script tag, or GTM's "All Pages" trigger
|
|
35
|
+
* will not fire.
|
|
22
36
|
*/
|
|
23
37
|
declare const generateScriptTags: (config: GtmScriptConfig) => ScriptTagData[];
|
|
38
|
+
/**
|
|
39
|
+
* Generate the complete standard GTM snippet as an inline script string.
|
|
40
|
+
*
|
|
41
|
+
* This produces the Google-recommended GTM installation code that:
|
|
42
|
+
* 1. Initializes the dataLayer array
|
|
43
|
+
* 2. Pushes the gtm.start event (required for "All Pages" trigger)
|
|
44
|
+
* 3. Dynamically creates and inserts the GTM script element
|
|
45
|
+
*
|
|
46
|
+
* Usage in Astro:
|
|
47
|
+
* ```astro
|
|
48
|
+
* <script is:inline set:html={generateGtmScript({ containers: 'GTM-XXXX' })} />
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* For the noscript fallback, use `generateNoscriptTags()` separately.
|
|
52
|
+
*/
|
|
53
|
+
declare const generateGtmScript: (config: Omit<GtmScriptConfig, 'scriptAttributes'>) => string;
|
|
24
54
|
interface NoscriptTagData {
|
|
25
55
|
id: string;
|
|
26
56
|
src: string;
|
|
@@ -39,4 +69,4 @@ declare const generateNoscriptTags: (config: Omit<GtmScriptConfig, 'scriptAttrib
|
|
|
39
69
|
*/
|
|
40
70
|
declare const generateDataLayerScript: (dataLayerName?: string) => string;
|
|
41
71
|
|
|
42
|
-
export { GtmScriptConfig, NoscriptTagData, ScriptTagData, generateDataLayerScript, generateNoscriptTags, generateScriptTags };
|
|
72
|
+
export { GtmScriptConfig, NoscriptTagData, ScriptTagData, generateDataLayerScript, generateGtmScript, generateNoscriptTags, generateScriptTags };
|
package/dist/components/index.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { normalizeContainers, buildGtmScriptUrl,
|
|
1
|
+
import { normalizeContainers, buildGtmScriptUrl, DEFAULT_DATA_LAYER_NAME, DEFAULT_GTM_HOST, buildGtmNoscriptUrl, DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES } from '@jwiedeman/gtm-kit';
|
|
2
2
|
export { DEFAULT_GTM_HOST, buildGtmNoscriptUrl as buildNoscriptUrl, buildGtmScriptUrl as buildScriptUrl, normalizeContainer, normalizeContainers } from '@jwiedeman/gtm-kit';
|
|
3
3
|
|
|
4
4
|
// src/components/helpers.ts
|
|
5
5
|
var isValidJsIdentifier = (value) => {
|
|
6
6
|
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(value);
|
|
7
7
|
};
|
|
8
|
+
var escapeJsString = (value) => {
|
|
9
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/</g, "\\x3c").replace(/>/g, "\\x3e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
10
|
+
};
|
|
8
11
|
var filterNullish = (obj) => Object.fromEntries(Object.entries(obj).filter(([, v]) => v != null));
|
|
9
12
|
var generateScriptTags = (config) => {
|
|
10
13
|
const {
|
|
@@ -18,6 +21,11 @@ var generateScriptTags = (config) => {
|
|
|
18
21
|
if (!normalized.length) {
|
|
19
22
|
throw new Error("At least one GTM container is required.");
|
|
20
23
|
}
|
|
24
|
+
if (!isValidJsIdentifier(dataLayerName)) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Invalid dataLayerName: "${dataLayerName}". Must be a valid JavaScript identifier.`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
21
29
|
return normalized.map((container) => {
|
|
22
30
|
if (!container.id) {
|
|
23
31
|
throw new Error("Container id is required.");
|
|
@@ -28,16 +36,47 @@ var generateScriptTags = (config) => {
|
|
|
28
36
|
};
|
|
29
37
|
const src = buildGtmScriptUrl(host, container.id, params, dataLayerName);
|
|
30
38
|
const { async: asyncAttr, defer, nonce, ...restAttributes } = scriptAttributes != null ? scriptAttributes : {};
|
|
39
|
+
escapeJsString(container.id);
|
|
40
|
+
const initScript = `window.${dataLayerName}=window.${dataLayerName}||[];window.${dataLayerName}.push({'gtm.start':new Date().getTime(),event:'gtm.js'});`;
|
|
31
41
|
return {
|
|
32
42
|
id: container.id,
|
|
33
43
|
src,
|
|
34
44
|
async: asyncAttr != null ? asyncAttr : true,
|
|
35
45
|
defer,
|
|
36
46
|
nonce,
|
|
37
|
-
attributes: filterNullish(restAttributes)
|
|
47
|
+
attributes: filterNullish(restAttributes),
|
|
48
|
+
initScript
|
|
38
49
|
};
|
|
39
50
|
});
|
|
40
51
|
};
|
|
52
|
+
var generateGtmScript = (config) => {
|
|
53
|
+
const {
|
|
54
|
+
containers,
|
|
55
|
+
host = DEFAULT_GTM_HOST,
|
|
56
|
+
defaultQueryParams,
|
|
57
|
+
dataLayerName = DEFAULT_DATA_LAYER_NAME
|
|
58
|
+
} = config;
|
|
59
|
+
const normalized = normalizeContainers(containers);
|
|
60
|
+
if (!normalized.length) {
|
|
61
|
+
throw new Error("At least one GTM container is required.");
|
|
62
|
+
}
|
|
63
|
+
if (!isValidJsIdentifier(dataLayerName)) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Invalid dataLayerName: "${dataLayerName}". Must be a valid JavaScript identifier.`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return normalized.map((container) => {
|
|
69
|
+
if (!container.id) {
|
|
70
|
+
throw new Error("Container id is required.");
|
|
71
|
+
}
|
|
72
|
+
const escapedId = escapeJsString(container.id);
|
|
73
|
+
dataLayerName !== DEFAULT_DATA_LAYER_NAME ? `,'${escapeJsString(dataLayerName)}'` : "";
|
|
74
|
+
const mergedParams = { ...defaultQueryParams, ...container.queryParams };
|
|
75
|
+
const extraParams = Object.entries(mergedParams).filter(([, v]) => v != null).map(([k, v]) => `&${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("");
|
|
76
|
+
const hostUrl = (host != null ? host : DEFAULT_GTM_HOST).replace(/\/+$/, "");
|
|
77
|
+
return `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='${escapeJsString(hostUrl)}/gtm.js?id='+i+dl${extraParams ? `+'${escapeJsString(extraParams)}'` : ""};f.parentNode.insertBefore(j,f);})(window,document,'script','${escapeJsString(dataLayerName)}','${escapedId}');`;
|
|
78
|
+
}).join("\n");
|
|
79
|
+
};
|
|
41
80
|
var generateNoscriptTags = (config) => {
|
|
42
81
|
const {
|
|
43
82
|
containers,
|
|
@@ -82,6 +121,6 @@ var generateDataLayerScript = (dataLayerName = DEFAULT_DATA_LAYER_NAME) => {
|
|
|
82
121
|
return `window.${dataLayerName}=window.${dataLayerName}||[];`;
|
|
83
122
|
};
|
|
84
123
|
|
|
85
|
-
export { generateDataLayerScript, generateNoscriptTags, generateScriptTags };
|
|
124
|
+
export { generateDataLayerScript, generateGtmScript, generateNoscriptTags, generateScriptTags };
|
|
86
125
|
//# sourceMappingURL=out.js.map
|
|
87
126
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/helpers.ts","../../src/components/index.ts"],"names":["normalizeContainers","normalizeContainer","buildGtmScriptUrl","buildGtmNoscriptUrl"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAiBA,IAAM,sBAAsB,CAAC,UAA2B;AAE7D,SAAO,6BAA6B,KAAK,KAAK;AAChD;AAoBA,IAAM,gBAAgB,CAAoC,QACxD,OAAO,YAAY,OAAO,QAAQ,GAAG,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,MAAM,KAAK,IAAI,CAAC;AAuB9D,IAAM,qBAAqB,CAAC,WAA6C;AAC9E,QAAM;AAAA,IACJ;AAAA,IACA,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA,gBAAgB;AAAA,EAClB,IAAI;AAEJ,QAAM,aAAa,oBAAoB,UAAU;AAEjD,MAAI,CAAC,WAAW,QAAQ;AACtB,UAAM,IAAI,MAAM,yCAAyC;AAAA,EAC3D;AAEA,SAAO,WAAW,IAAI,CAAC,cAAc;AACnC,QAAI,CAAC,UAAU,IAAI;AACjB,YAAM,IAAI,MAAM,2BAA2B;AAAA,IAC7C;AAEA,UAAM,SAAS;AAAA,MACb,GAAG;AAAA,MACH,GAAG,UAAU;AAAA,IACf;AAEA,UAAM,MAAM,kBAAkB,MAAM,UAAU,IAAI,QAAQ,aAAa;AACvE,UAAM,EAAE,OAAO,WAAW,OAAO,OAAO,GAAG,eAAe,IAAI,8CAAoB,CAAC;AAEnF,WAAO;AAAA,MACL,IAAI,UAAU;AAAA,MACd;AAAA,MACA,OAAO,gCAAa;AAAA,MACpB;AAAA,MACA;AAAA,MACA,YAAY,cAAc,cAAc;AAAA,IAC1C;AAAA,EACF,CAAC;AACH;AAYO,IAAM,uBAAuB,CAClC,WAGsB;AACtB,QAAM;AAAA,IACJ;AAAA,IACA,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA,gBAAgB;AAAA,EAClB,IAAI;AAEJ,QAAM,aAAa,oBAAoB,UAAU;AAEjD,MAAI,CAAC,WAAW,QAAQ;AACtB,UAAM,IAAI,MAAM,yCAAyC;AAAA,EAC3D;AAEA,SAAO,WAAW,IAAI,CAAC,cAAc;AACnC,QAAI,CAAC,UAAU,IAAI;AACjB,YAAM,IAAI,MAAM,2BAA2B;AAAA,IAC7C;AAEA,UAAM,SAAS;AAAA,MACb,GAAG;AAAA,MACH,GAAG,UAAU;AAAA,IACf;AAEA,UAAM,MAAM,oBAAoB,MAAM,UAAU,IAAI,QAAQ,aAAa;AACzE,UAAM,mBAAmB;AAAA,MACvB,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAGA,UAAM,aAAa,OAAO;AAAA,MACxB,OAAO,QAAQ,gBAAgB,EAC5B,OAAO,CAAC,CAAC,EAAE,CAAC,MAAM,KAAK,IAAI,EAC3B,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;AAAA,IACnC;AAEA,WAAO;AAAA,MACL,IAAI,UAAU;AAAA,MACd;AAAA,MACA;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAMO,IAAM,0BAA0B,CAAC,gBAAwB,4BAAoC;AAClG,MAAI,CAAC,oBAAoB,aAAa,GAAG;AACvC,UAAM,IAAI;AAAA,MACR,2BAA2B,aAAa;AAAA,IAC1C;AAAA,EACF;AACA,SAAO,UAAU,aAAa,WAAW,aAAa;AACxD;;;ACnLA;AAAA,EACE,uBAAAA;AAAA,EACA,sBAAAC;AAAA,EACqB,qBAArBC;AAAA,EACuB,uBAAvBC;AAAA,OACK","sourcesContent":["import {\n DEFAULT_DATA_LAYER_NAME,\n DEFAULT_GTM_HOST,\n DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,\n normalizeContainer,\n normalizeContainers,\n buildGtmScriptUrl,\n buildGtmNoscriptUrl\n} from '@jwiedeman/gtm-kit';\nimport type { ContainerConfigInput, ScriptAttributes } from '@jwiedeman/gtm-kit';\n\n// Re-export for convenience\nexport {\n DEFAULT_GTM_HOST,\n DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,\n normalizeContainer,\n normalizeContainers,\n buildGtmScriptUrl as buildScriptUrl,\n buildGtmNoscriptUrl as buildNoscriptUrl\n};\n\n/**\n * Validate that a string is a valid JavaScript identifier.\n * This prevents XSS attacks through dataLayerName injection.\n */\nexport const isValidJsIdentifier = (value: string): boolean => {\n // Must be a valid JS identifier: starts with letter/$/_, followed by letters/digits/$/_\n return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(value);\n};\n\n/**\n * Escape a string for safe use in JavaScript string literals.\n * Prevents XSS when interpolating user-provided values into inline scripts.\n */\nexport const escapeJsString = (value: string): string => {\n return value\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/'/g, \"\\\\'\")\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, '\\\\n')\n .replace(/\\r/g, '\\\\r')\n .replace(/</g, '\\\\x3c')\n .replace(/>/g, '\\\\x3e')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029');\n};\n\n/** Filter out null/undefined values from an object */\nconst filterNullish = <T extends Record<string, unknown>>(obj: T): T =>\n Object.fromEntries(Object.entries(obj).filter(([, v]) => v != null)) as T;\n\nexport interface GtmScriptConfig {\n containers: ContainerConfigInput | ContainerConfigInput[];\n host?: string;\n defaultQueryParams?: Record<string, string | number | boolean>;\n scriptAttributes?: ScriptAttributes;\n dataLayerName?: string;\n}\n\nexport interface ScriptTagData {\n id: string;\n src: string;\n async: boolean;\n defer?: boolean;\n nonce?: string;\n attributes: Record<string, string | boolean>;\n}\n\n/**\n * Generate script tag data for GTM containers.\n * Used by Astro components to render script tags.\n */\nexport const generateScriptTags = (config: GtmScriptConfig): ScriptTagData[] => {\n const {\n containers,\n host = DEFAULT_GTM_HOST,\n defaultQueryParams,\n scriptAttributes,\n dataLayerName = DEFAULT_DATA_LAYER_NAME\n } = config;\n\n const normalized = normalizeContainers(containers);\n\n if (!normalized.length) {\n throw new Error('At least one GTM container is required.');\n }\n\n return normalized.map((container) => {\n if (!container.id) {\n throw new Error('Container id is required.');\n }\n\n const params = {\n ...defaultQueryParams,\n ...container.queryParams\n };\n\n const src = buildGtmScriptUrl(host, container.id, params, dataLayerName);\n const { async: asyncAttr, defer, nonce, ...restAttributes } = scriptAttributes ?? {};\n\n return {\n id: container.id,\n src,\n async: asyncAttr ?? true,\n defer,\n nonce,\n attributes: filterNullish(restAttributes) as Record<string, string | boolean>\n };\n });\n};\n\nexport interface NoscriptTagData {\n id: string;\n src: string;\n attributes: Record<string, string>;\n}\n\n/**\n * Generate noscript iframe data for GTM containers.\n * Used by Astro components to render noscript fallbacks.\n */\nexport const generateNoscriptTags = (\n config: Omit<GtmScriptConfig, 'scriptAttributes'> & {\n iframeAttributes?: Record<string, string | number | boolean>;\n }\n): NoscriptTagData[] => {\n const {\n containers,\n host = DEFAULT_GTM_HOST,\n defaultQueryParams,\n iframeAttributes,\n dataLayerName = DEFAULT_DATA_LAYER_NAME\n } = config;\n\n const normalized = normalizeContainers(containers);\n\n if (!normalized.length) {\n throw new Error('At least one GTM container is required.');\n }\n\n return normalized.map((container) => {\n if (!container.id) {\n throw new Error('Container id is required.');\n }\n\n const params = {\n ...defaultQueryParams,\n ...container.queryParams\n };\n\n const src = buildGtmNoscriptUrl(host, container.id, params, dataLayerName);\n const mergedAttributes = {\n ...DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,\n ...iframeAttributes\n };\n\n // Filter nullish values and convert to strings\n const attributes = Object.fromEntries(\n Object.entries(mergedAttributes)\n .filter(([, v]) => v != null)\n .map(([k, v]) => [k, String(v)])\n ) as Record<string, string>;\n\n return {\n id: container.id,\n src,\n attributes\n };\n });\n};\n\n/**\n * Generate the dataLayer initialization script.\n * @throws {Error} If dataLayerName is not a valid JavaScript identifier\n */\nexport const generateDataLayerScript = (dataLayerName: string = DEFAULT_DATA_LAYER_NAME): string => {\n if (!isValidJsIdentifier(dataLayerName)) {\n throw new Error(\n `Invalid dataLayerName: \"${dataLayerName}\". Must be a valid JavaScript identifier (letters, digits, $, _ only, cannot start with a digit).`\n );\n }\n return `window.${dataLayerName}=window.${dataLayerName}||[];`;\n};\n","export { generateScriptTags, generateNoscriptTags, generateDataLayerScript, DEFAULT_GTM_HOST } from './helpers';\n\n// Re-export URL utilities from core for backwards compatibility\nexport {\n normalizeContainers,\n normalizeContainer,\n buildGtmScriptUrl as buildScriptUrl,\n buildGtmNoscriptUrl as buildNoscriptUrl\n} from '@jwiedeman/gtm-kit';\n\nexport type { GtmScriptConfig, ScriptTagData, NoscriptTagData } from './helpers';\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/helpers.ts","../../src/components/index.ts"],"names":["normalizeContainers","normalizeContainer","buildGtmScriptUrl","buildGtmNoscriptUrl"],"mappings":"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","sourcesContent":["import {\n DEFAULT_DATA_LAYER_NAME,\n DEFAULT_GTM_HOST,\n DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,\n normalizeContainer,\n normalizeContainers,\n buildGtmScriptUrl,\n buildGtmNoscriptUrl\n} from '@jwiedeman/gtm-kit';\nimport type { ContainerConfigInput, ScriptAttributes } from '@jwiedeman/gtm-kit';\n\n// Re-export for convenience\nexport {\n DEFAULT_GTM_HOST,\n DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,\n normalizeContainer,\n normalizeContainers,\n buildGtmScriptUrl as buildScriptUrl,\n buildGtmNoscriptUrl as buildNoscriptUrl\n};\n\n/**\n * Validate that a string is a valid JavaScript identifier.\n * This prevents XSS attacks through dataLayerName injection.\n */\nexport const isValidJsIdentifier = (value: string): boolean => {\n // Must be a valid JS identifier: starts with letter/$/_, followed by letters/digits/$/_\n return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(value);\n};\n\n/**\n * Escape a string for safe use in JavaScript string literals.\n * Prevents XSS when interpolating user-provided values into inline scripts.\n */\nexport const escapeJsString = (value: string): string => {\n return value\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/'/g, \"\\\\'\")\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, '\\\\n')\n .replace(/\\r/g, '\\\\r')\n .replace(/</g, '\\\\x3c')\n .replace(/>/g, '\\\\x3e')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029');\n};\n\n/** Filter out null/undefined values from an object */\nconst filterNullish = <T extends Record<string, unknown>>(obj: T): T =>\n Object.fromEntries(Object.entries(obj).filter(([, v]) => v != null)) as T;\n\nexport interface GtmScriptConfig {\n containers: ContainerConfigInput | ContainerConfigInput[];\n host?: string;\n defaultQueryParams?: Record<string, string | number | boolean>;\n scriptAttributes?: ScriptAttributes;\n dataLayerName?: string;\n}\n\nexport interface ScriptTagData {\n id: string;\n src: string;\n async: boolean;\n defer?: boolean;\n nonce?: string;\n attributes: Record<string, string | boolean>;\n /**\n * Inline JavaScript that must be executed before the GTM script loads.\n * Initializes the dataLayer and pushes the gtm.start event, which is\n * required for GTM's built-in \"All Pages\" trigger to fire.\n *\n * Render this as a `<script>` tag (or `<script is:inline>` in Astro)\n * immediately before the GTM `<script async src=\"...\">` tag.\n */\n initScript: string;\n}\n\n/**\n * Generate script tag data for GTM containers.\n * Used by Astro components to render script tags.\n *\n * Each tag includes an `initScript` property containing the dataLayer\n * initialization and gtm.start event push. This MUST be rendered as an\n * inline script before the GTM script tag, or GTM's \"All Pages\" trigger\n * will not fire.\n */\nexport const generateScriptTags = (config: GtmScriptConfig): ScriptTagData[] => {\n const {\n containers,\n host = DEFAULT_GTM_HOST,\n defaultQueryParams,\n scriptAttributes,\n dataLayerName = DEFAULT_DATA_LAYER_NAME\n } = config;\n\n const normalized = normalizeContainers(containers);\n\n if (!normalized.length) {\n throw new Error('At least one GTM container is required.');\n }\n\n if (!isValidJsIdentifier(dataLayerName)) {\n throw new Error(\n `Invalid dataLayerName: \"${dataLayerName}\". Must be a valid JavaScript identifier.`\n );\n }\n\n return normalized.map((container) => {\n if (!container.id) {\n throw new Error('Container id is required.');\n }\n\n const params = {\n ...defaultQueryParams,\n ...container.queryParams\n };\n\n const src = buildGtmScriptUrl(host, container.id, params, dataLayerName);\n const { async: asyncAttr, defer, nonce, ...restAttributes } = scriptAttributes ?? {};\n\n // Generate the standard GTM initialization script.\n // This pushes the gtm.start event to the dataLayer, which is required\n // for GTM to trigger its built-in \"All Pages\" / \"Container Loaded\" trigger.\n const escapedId = escapeJsString(container.id);\n const dlParam = dataLayerName !== DEFAULT_DATA_LAYER_NAME ? `'&l='+l` : `''`;\n const initScript =\n `window.${dataLayerName}=window.${dataLayerName}||[];` +\n `window.${dataLayerName}.push({'gtm.start':new Date().getTime(),event:'gtm.js'});`;\n\n return {\n id: container.id,\n src,\n async: asyncAttr ?? true,\n defer,\n nonce,\n attributes: filterNullish(restAttributes) as Record<string, string | boolean>,\n initScript\n };\n });\n};\n\n/**\n * Generate the complete standard GTM snippet as an inline script string.\n *\n * This produces the Google-recommended GTM installation code that:\n * 1. Initializes the dataLayer array\n * 2. Pushes the gtm.start event (required for \"All Pages\" trigger)\n * 3. Dynamically creates and inserts the GTM script element\n *\n * Usage in Astro:\n * ```astro\n * <script is:inline set:html={generateGtmScript({ containers: 'GTM-XXXX' })} />\n * ```\n *\n * For the noscript fallback, use `generateNoscriptTags()` separately.\n */\nexport const generateGtmScript = (config: Omit<GtmScriptConfig, 'scriptAttributes'>): string => {\n const {\n containers,\n host = DEFAULT_GTM_HOST,\n defaultQueryParams,\n dataLayerName = DEFAULT_DATA_LAYER_NAME\n } = config;\n\n const normalized = normalizeContainers(containers);\n\n if (!normalized.length) {\n throw new Error('At least one GTM container is required.');\n }\n\n if (!isValidJsIdentifier(dataLayerName)) {\n throw new Error(\n `Invalid dataLayerName: \"${dataLayerName}\". Must be a valid JavaScript identifier.`\n );\n }\n\n // Generate one standard snippet per container\n return normalized.map((container) => {\n if (!container.id) {\n throw new Error('Container id is required.');\n }\n\n const escapedId = escapeJsString(container.id);\n const dlArg = dataLayerName !== DEFAULT_DATA_LAYER_NAME\n ? `,'${escapeJsString(dataLayerName)}'`\n : '';\n\n // Build query params string for additional params (auth, preview, etc.)\n const mergedParams = { ...defaultQueryParams, ...container.queryParams };\n const extraParams = Object.entries(mergedParams)\n .filter(([, v]) => v != null)\n .map(([k, v]) => `&${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)\n .join('');\n\n const hostUrl = (host ?? DEFAULT_GTM_HOST).replace(/\\/+$/, '');\n\n // Standard GTM snippet — matches Google's recommended installation\n return `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':` +\n `new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],` +\n `j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=` +\n `'${escapeJsString(hostUrl)}/gtm.js?id='+i+dl${extraParams ? `+'${escapeJsString(extraParams)}'` : ''};f.parentNode.insertBefore(j,f);` +\n `})(window,document,'script','${escapeJsString(dataLayerName)}','${escapedId}');`;\n }).join('\\n');\n};\n\nexport interface NoscriptTagData {\n id: string;\n src: string;\n attributes: Record<string, string>;\n}\n\n/**\n * Generate noscript iframe data for GTM containers.\n * Used by Astro components to render noscript fallbacks.\n */\nexport const generateNoscriptTags = (\n config: Omit<GtmScriptConfig, 'scriptAttributes'> & {\n iframeAttributes?: Record<string, string | number | boolean>;\n }\n): NoscriptTagData[] => {\n const {\n containers,\n host = DEFAULT_GTM_HOST,\n defaultQueryParams,\n iframeAttributes,\n dataLayerName = DEFAULT_DATA_LAYER_NAME\n } = config;\n\n const normalized = normalizeContainers(containers);\n\n if (!normalized.length) {\n throw new Error('At least one GTM container is required.');\n }\n\n return normalized.map((container) => {\n if (!container.id) {\n throw new Error('Container id is required.');\n }\n\n const params = {\n ...defaultQueryParams,\n ...container.queryParams\n };\n\n const src = buildGtmNoscriptUrl(host, container.id, params, dataLayerName);\n const mergedAttributes = {\n ...DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,\n ...iframeAttributes\n };\n\n // Filter nullish values and convert to strings\n const attributes = Object.fromEntries(\n Object.entries(mergedAttributes)\n .filter(([, v]) => v != null)\n .map(([k, v]) => [k, String(v)])\n ) as Record<string, string>;\n\n return {\n id: container.id,\n src,\n attributes\n };\n });\n};\n\n/**\n * Generate the dataLayer initialization script.\n * @throws {Error} If dataLayerName is not a valid JavaScript identifier\n */\nexport const generateDataLayerScript = (dataLayerName: string = DEFAULT_DATA_LAYER_NAME): string => {\n if (!isValidJsIdentifier(dataLayerName)) {\n throw new Error(\n `Invalid dataLayerName: \"${dataLayerName}\". Must be a valid JavaScript identifier (letters, digits, $, _ only, cannot start with a digit).`\n );\n }\n return `window.${dataLayerName}=window.${dataLayerName}||[];`;\n};\n","export { generateScriptTags, generateNoscriptTags, generateDataLayerScript, generateGtmScript, DEFAULT_GTM_HOST } from './helpers';\n\n// Re-export URL utilities from core for backwards compatibility\nexport {\n normalizeContainers,\n normalizeContainer,\n buildGtmScriptUrl as buildScriptUrl,\n buildGtmNoscriptUrl as buildNoscriptUrl\n} from '@jwiedeman/gtm-kit';\n\nexport type { GtmScriptConfig, ScriptTagData, NoscriptTagData } from './helpers';\n"]}
|
package/dist/index.cjs
CHANGED
|
@@ -7,6 +7,11 @@ var clientInstance = null;
|
|
|
7
7
|
var clientConfig = null;
|
|
8
8
|
var initGtm = (options) => {
|
|
9
9
|
if (clientInstance) {
|
|
10
|
+
if (process.env.NODE_ENV === "development" && clientConfig && JSON.stringify(options) !== JSON.stringify(clientConfig)) {
|
|
11
|
+
console.warn(
|
|
12
|
+
"[gtm-kit/astro] initGtm() called again with different options. The original configuration is kept. Call teardown() first if you need to reinitialize with new options."
|
|
13
|
+
);
|
|
14
|
+
}
|
|
10
15
|
return clientInstance;
|
|
11
16
|
}
|
|
12
17
|
clientInstance = gtmKit.createGtmClient(options);
|
|
@@ -110,6 +115,7 @@ var trackPageView = (options = {}) => {
|
|
|
110
115
|
push(pageViewData);
|
|
111
116
|
};
|
|
112
117
|
var viewTransitionsSetup = false;
|
|
118
|
+
var pageTrackingSetup = false;
|
|
113
119
|
var lastTrackedPath = "";
|
|
114
120
|
var setupViewTransitions = (options = {}) => {
|
|
115
121
|
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
@@ -140,6 +146,10 @@ var setupPageTracking = (options = {}) => {
|
|
|
140
146
|
if (typeof window === "undefined") {
|
|
141
147
|
return noop;
|
|
142
148
|
}
|
|
149
|
+
if (pageTrackingSetup) {
|
|
150
|
+
return noop;
|
|
151
|
+
}
|
|
152
|
+
pageTrackingSetup = true;
|
|
143
153
|
lastTrackedPath = window.location.pathname + window.location.search;
|
|
144
154
|
trackPageView(options);
|
|
145
155
|
const handlePopState = () => {
|
|
@@ -153,6 +163,7 @@ var setupPageTracking = (options = {}) => {
|
|
|
153
163
|
window.addEventListener("popstate", handlePopState);
|
|
154
164
|
return () => {
|
|
155
165
|
window.removeEventListener("popstate", handlePopState);
|
|
166
|
+
pageTrackingSetup = false;
|
|
156
167
|
lastTrackedPath = "";
|
|
157
168
|
};
|
|
158
169
|
};
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts","../src/page-tracking.ts","../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n createGtmClient,\n type ConsentRegionOptions,\n type ConsentState,\n type CreateGtmClientOptions,\n type DataLayerValue,\n type GtmClient,\n type ScriptLoadState\n} from '@jwiedeman/gtm-kit';\n\nlet clientInstance: GtmClient | null = null;\nlet clientConfig: CreateGtmClientOptions | null = null;\n\n/**\n * Initialize the GTM client for use in Astro.\n * Should be called once when the page loads.\n *\n * @example\n * ```ts\n * // In a client-side script\n * import { initGtm } from '@jwiedeman/gtm-kit-astro';\n *\n * initGtm({ containers: 'GTM-XXXXXX' });\n * ```\n */\nexport const initGtm = (options: CreateGtmClientOptions): GtmClient => {\n // If already initialized, return existing instance\n // (common in Astro with view transitions where initGtm may be called multiple times)\n if (clientInstance) {\n return clientInstance;\n }\n\n clientInstance = createGtmClient(options);\n clientConfig = options;\n clientInstance.init();\n\n return clientInstance;\n};\n\n/**\n * Get the current GTM client instance.\n * Returns null if not initialized.\n *\n * @example\n * ```ts\n * import { getGtmClient } from '@jwiedeman/gtm-kit-astro';\n *\n * const client = getGtmClient();\n * if (client) {\n * client.push({ event: 'page_view' });\n * }\n * ```\n */\nexport const getGtmClient = (): GtmClient | null => clientInstance;\n\n/**\n * Get the GTM client or throw if not initialized.\n * Use this when you expect the client to be available.\n *\n * @throws Error if GTM client is not initialized\n */\nexport const requireGtmClient = (): GtmClient => {\n if (!clientInstance) {\n throw new Error(\n '[gtm-kit/astro] GTM client not initialized. Call initGtm() first or ensure the GTM script has loaded.'\n );\n }\n return clientInstance;\n};\n\n/**\n * Push a value to the GTM dataLayer.\n * This is a convenience function that handles the case where GTM isn't initialized.\n *\n * @returns true if the value was pushed successfully, false otherwise\n *\n * @example\n * ```ts\n * import { push } from '@jwiedeman/gtm-kit-astro';\n *\n * const success = push({ event: 'button_click', button_name: 'hero_cta' });\n * if (!success) {\n * console.warn('GTM not ready');\n * }\n * ```\n */\n/**\n * Type-safe access to window properties for dataLayer.\n */\nconst getWindowDataLayer = (name: string): DataLayerValue[] | undefined => {\n if (typeof window === 'undefined') {\n return undefined;\n }\n const win = window as typeof window & Record<string, unknown>;\n const layer = win[name];\n return Array.isArray(layer) ? (layer as DataLayerValue[]) : undefined;\n};\n\nexport const push = (value: DataLayerValue): boolean => {\n const client = getGtmClient();\n if (client) {\n client.push(value);\n return true;\n } else if (typeof window !== 'undefined') {\n // Fallback: push directly to dataLayer if it exists\n const dataLayerName = clientConfig?.dataLayerName ?? 'dataLayer';\n const dataLayer = getWindowDataLayer(dataLayerName);\n if (dataLayer) {\n dataLayer.push(value);\n return true;\n } else {\n console.warn('[gtm-kit/astro] GTM not initialized and dataLayer not found.');\n return false;\n }\n }\n return false;\n};\n\n/**\n * Set consent defaults (must be called before GTM loads).\n *\n * @returns true if consent defaults were set, false if GTM not initialized\n *\n * @example\n * ```ts\n * import { setConsentDefaults } from '@jwiedeman/gtm-kit-astro';\n *\n * const success = setConsentDefaults({\n * ad_storage: 'denied',\n * analytics_storage: 'denied'\n * });\n * ```\n */\nexport const setConsentDefaults = (state: ConsentState, options?: ConsentRegionOptions): boolean => {\n const client = getGtmClient();\n if (client) {\n client.setConsentDefaults(state, options);\n return true;\n } else {\n console.warn('[gtm-kit/astro] GTM not initialized. Consent defaults should be set before init.');\n return false;\n }\n};\n\n/**\n * Update consent state after user interaction.\n *\n * @returns true if consent was updated, false if GTM not initialized\n *\n * @example\n * ```ts\n * import { updateConsent } from '@jwiedeman/gtm-kit-astro';\n *\n * // When user accepts cookies\n * const success = updateConsent({\n * ad_storage: 'granted',\n * analytics_storage: 'granted',\n * ad_user_data: 'granted',\n * ad_personalization: 'granted'\n * });\n * ```\n */\nexport const updateConsent = (state: ConsentState, options?: ConsentRegionOptions): boolean => {\n const client = getGtmClient();\n if (client) {\n client.updateConsent(state, options);\n return true;\n } else {\n console.warn('[gtm-kit/astro] GTM not initialized. Cannot update consent.');\n return false;\n }\n};\n\n/**\n * Wait for GTM scripts to be ready.\n *\n * @example\n * ```ts\n * import { whenReady } from '@jwiedeman/gtm-kit-astro';\n *\n * whenReady().then((states) => {\n * console.log('GTM loaded:', states);\n * });\n * ```\n */\nexport const whenReady = (): Promise<ScriptLoadState[]> => {\n const client = getGtmClient();\n if (client) {\n return client.whenReady();\n }\n return Promise.reject(new Error('[gtm-kit/astro] GTM not initialized.'));\n};\n\n/**\n * Clean up the GTM client.\n * Call this when navigating away or cleaning up.\n */\nexport const teardown = (): void => {\n if (clientInstance) {\n clientInstance.teardown();\n clientInstance = null;\n clientConfig = null;\n }\n};\n","import { push } from './client';\n\n// No-op function for SSR/cleanup returns\nconst noop = (): void => {\n /* no-op */\n};\n\nexport interface PageViewData {\n event?: string;\n page_path?: string;\n page_location?: string;\n page_title?: string;\n page_referrer?: string;\n [key: string]: unknown;\n}\n\nexport interface TrackPageViewOptions {\n /**\n * The event name to use for page views.\n * @default 'page_view'\n */\n eventName?: string;\n\n /**\n * Whether to include query parameters in the page path.\n * @default true\n */\n includeQueryParams?: boolean;\n\n /**\n * Additional data to include with each page view.\n */\n additionalData?: Record<string, unknown> | (() => Record<string, unknown>);\n}\n\nconst getAdditionalData = (additionalData: TrackPageViewOptions['additionalData']): Record<string, unknown> => {\n if (typeof additionalData === 'function') {\n return additionalData();\n }\n return additionalData ?? {};\n};\n\n/**\n * Track a page view event.\n *\n * @example\n * ```ts\n * import { trackPageView } from '@jwiedeman/gtm-kit-astro';\n *\n * // Track current page\n * trackPageView();\n *\n * // Track with custom data\n * trackPageView({\n * additionalData: { user_type: 'guest' }\n * });\n * ```\n */\nexport const trackPageView = (options: TrackPageViewOptions = {}): void => {\n const { eventName = 'page_view', includeQueryParams = true, additionalData } = options;\n\n if (typeof window === 'undefined') {\n return;\n }\n\n const pagePath = includeQueryParams ? window.location.pathname + window.location.search : window.location.pathname;\n\n const pageViewData: PageViewData = {\n event: eventName,\n page_path: pagePath,\n page_location: window.location.href,\n page_title: document.title,\n ...getAdditionalData(additionalData)\n };\n\n push(pageViewData);\n};\n\nlet viewTransitionsSetup = false;\nlet lastTrackedPath = '';\n\n/**\n * Reset page tracking state (for testing only).\n * @internal\n */\nexport const _resetPageTrackingState = (): void => {\n viewTransitionsSetup = false;\n lastTrackedPath = '';\n};\n\n/**\n * Set up automatic page view tracking with Astro View Transitions.\n * Call this once when the page loads.\n *\n * @example\n * ```astro\n * ---\n * // src/layouts/Layout.astro\n * ---\n * <script>\n * import { initGtm, setupViewTransitions } from '@jwiedeman/gtm-kit-astro';\n *\n * initGtm({ containers: 'GTM-XXXXXX' });\n * setupViewTransitions();\n * </script>\n * ```\n */\nexport const setupViewTransitions = (options: TrackPageViewOptions = {}): (() => void) => {\n if (typeof window === 'undefined' || typeof document === 'undefined') {\n return noop;\n }\n\n // Prevent duplicate setup\n if (viewTransitionsSetup) {\n return noop;\n }\n\n viewTransitionsSetup = true;\n\n // Track initial page view\n lastTrackedPath = window.location.pathname + window.location.search;\n trackPageView(options);\n\n // Handle Astro View Transitions\n // The 'astro:page-load' event fires after every page load, including View Transitions\n const handlePageLoad = () => {\n const currentPath = window.location.pathname + window.location.search;\n\n // Avoid duplicate tracking for the same path\n if (currentPath === lastTrackedPath) {\n return;\n }\n\n lastTrackedPath = currentPath;\n trackPageView(options);\n };\n\n document.addEventListener('astro:page-load', handlePageLoad);\n\n // Cleanup function\n return () => {\n document.removeEventListener('astro:page-load', handlePageLoad);\n viewTransitionsSetup = false;\n lastTrackedPath = '';\n };\n};\n\n/**\n * Set up page view tracking for standard navigation (no View Transitions).\n * Tracks on popstate (back/forward) and initial load.\n *\n * @example\n * ```ts\n * import { setupPageTracking } from '@jwiedeman/gtm-kit-astro';\n *\n * setupPageTracking();\n * ```\n */\nexport const setupPageTracking = (options: TrackPageViewOptions = {}): (() => void) => {\n if (typeof window === 'undefined') {\n return noop;\n }\n\n // Track initial page view\n lastTrackedPath = window.location.pathname + window.location.search;\n trackPageView(options);\n\n // Handle browser back/forward navigation\n const handlePopState = () => {\n const currentPath = window.location.pathname + window.location.search;\n\n if (currentPath === lastTrackedPath) {\n return;\n }\n\n lastTrackedPath = currentPath;\n trackPageView(options);\n };\n\n window.addEventListener('popstate', handlePopState);\n\n return () => {\n window.removeEventListener('popstate', handlePopState);\n lastTrackedPath = '';\n };\n};\n","// Client-side GTM management\nexport {\n initGtm,\n getGtmClient,\n requireGtmClient,\n push,\n setConsentDefaults,\n updateConsent,\n whenReady,\n teardown\n} from './client';\n\n// Page tracking\nexport { trackPageView, setupViewTransitions, setupPageTracking } from './page-tracking';\nexport type { PageViewData, TrackPageViewOptions } from './page-tracking';\n\n// Re-export core types for convenience\nexport type {\n ConsentState,\n ConsentRegionOptions,\n CreateGtmClientOptions,\n DataLayerValue,\n GtmClient,\n ScriptLoadState,\n ContainerConfigInput,\n ContainerDescriptor,\n ScriptAttributes\n} from '@jwiedeman/gtm-kit';\n\n// Re-export consent helpers\nexport { consentPresets, getConsentPreset, eeaDefault, allGranted, analyticsOnly } from '@jwiedeman/gtm-kit';\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/page-tracking.ts","../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n createGtmClient,\n type ConsentRegionOptions,\n type ConsentState,\n type CreateGtmClientOptions,\n type DataLayerValue,\n type GtmClient,\n type ScriptLoadState\n} from '@jwiedeman/gtm-kit';\n\nlet clientInstance: GtmClient | null = null;\nlet clientConfig: CreateGtmClientOptions | null = null;\n\n/**\n * Initialize the GTM client for use in Astro.\n * Should be called once when the page loads.\n *\n * @example\n * ```ts\n * // In a client-side script\n * import { initGtm } from '@jwiedeman/gtm-kit-astro';\n *\n * initGtm({ containers: 'GTM-XXXXXX' });\n * ```\n */\nexport const initGtm = (options: CreateGtmClientOptions): GtmClient => {\n // If already initialized, return existing instance\n // (common in Astro with view transitions where initGtm may be called multiple times)\n if (clientInstance) {\n if (\n process.env.NODE_ENV === 'development' &&\n clientConfig &&\n JSON.stringify(options) !== JSON.stringify(clientConfig)\n ) {\n console.warn(\n '[gtm-kit/astro] initGtm() called again with different options. ' +\n 'The original configuration is kept. Call teardown() first if you need to reinitialize with new options.'\n );\n }\n return clientInstance;\n }\n\n clientInstance = createGtmClient(options);\n clientConfig = options;\n clientInstance.init();\n\n return clientInstance;\n};\n\n/**\n * Get the current GTM client instance.\n * Returns null if not initialized.\n *\n * @example\n * ```ts\n * import { getGtmClient } from '@jwiedeman/gtm-kit-astro';\n *\n * const client = getGtmClient();\n * if (client) {\n * client.push({ event: 'page_view' });\n * }\n * ```\n */\nexport const getGtmClient = (): GtmClient | null => clientInstance;\n\n/**\n * Get the GTM client or throw if not initialized.\n * Use this when you expect the client to be available.\n *\n * @throws Error if GTM client is not initialized\n */\nexport const requireGtmClient = (): GtmClient => {\n if (!clientInstance) {\n throw new Error(\n '[gtm-kit/astro] GTM client not initialized. Call initGtm() first or ensure the GTM script has loaded.'\n );\n }\n return clientInstance;\n};\n\n/**\n * Push a value to the GTM dataLayer.\n * This is a convenience function that handles the case where GTM isn't initialized.\n *\n * @returns true if the value was pushed successfully, false otherwise\n *\n * @example\n * ```ts\n * import { push } from '@jwiedeman/gtm-kit-astro';\n *\n * const success = push({ event: 'button_click', button_name: 'hero_cta' });\n * if (!success) {\n * console.warn('GTM not ready');\n * }\n * ```\n */\n/**\n * Type-safe access to window properties for dataLayer.\n */\nconst getWindowDataLayer = (name: string): DataLayerValue[] | undefined => {\n if (typeof window === 'undefined') {\n return undefined;\n }\n const win = window as typeof window & Record<string, unknown>;\n const layer = win[name];\n return Array.isArray(layer) ? (layer as DataLayerValue[]) : undefined;\n};\n\nexport const push = (value: DataLayerValue): boolean => {\n const client = getGtmClient();\n if (client) {\n client.push(value);\n return true;\n } else if (typeof window !== 'undefined') {\n // Fallback: push directly to dataLayer if it exists\n const dataLayerName = clientConfig?.dataLayerName ?? 'dataLayer';\n const dataLayer = getWindowDataLayer(dataLayerName);\n if (dataLayer) {\n dataLayer.push(value);\n return true;\n } else {\n console.warn('[gtm-kit/astro] GTM not initialized and dataLayer not found.');\n return false;\n }\n }\n return false;\n};\n\n/**\n * Set consent defaults (must be called before GTM loads).\n *\n * @returns true if consent defaults were set, false if GTM not initialized\n *\n * @example\n * ```ts\n * import { setConsentDefaults } from '@jwiedeman/gtm-kit-astro';\n *\n * const success = setConsentDefaults({\n * ad_storage: 'denied',\n * analytics_storage: 'denied'\n * });\n * ```\n */\nexport const setConsentDefaults = (state: ConsentState, options?: ConsentRegionOptions): boolean => {\n const client = getGtmClient();\n if (client) {\n client.setConsentDefaults(state, options);\n return true;\n } else {\n console.warn('[gtm-kit/astro] GTM not initialized. Consent defaults should be set before init.');\n return false;\n }\n};\n\n/**\n * Update consent state after user interaction.\n *\n * @returns true if consent was updated, false if GTM not initialized\n *\n * @example\n * ```ts\n * import { updateConsent } from '@jwiedeman/gtm-kit-astro';\n *\n * // When user accepts cookies\n * const success = updateConsent({\n * ad_storage: 'granted',\n * analytics_storage: 'granted',\n * ad_user_data: 'granted',\n * ad_personalization: 'granted'\n * });\n * ```\n */\nexport const updateConsent = (state: ConsentState, options?: ConsentRegionOptions): boolean => {\n const client = getGtmClient();\n if (client) {\n client.updateConsent(state, options);\n return true;\n } else {\n console.warn('[gtm-kit/astro] GTM not initialized. Cannot update consent.');\n return false;\n }\n};\n\n/**\n * Wait for GTM scripts to be ready.\n *\n * @example\n * ```ts\n * import { whenReady } from '@jwiedeman/gtm-kit-astro';\n *\n * whenReady().then((states) => {\n * console.log('GTM loaded:', states);\n * });\n * ```\n */\nexport const whenReady = (): Promise<ScriptLoadState[]> => {\n const client = getGtmClient();\n if (client) {\n return client.whenReady();\n }\n return Promise.reject(new Error('[gtm-kit/astro] GTM not initialized.'));\n};\n\n/**\n * Clean up the GTM client.\n * Call this when navigating away or cleaning up.\n */\nexport const teardown = (): void => {\n if (clientInstance) {\n clientInstance.teardown();\n clientInstance = null;\n clientConfig = null;\n }\n};\n","import { push } from './client';\n\n// No-op function for SSR/cleanup returns\nconst noop = (): void => {\n /* no-op */\n};\n\nexport interface PageViewData {\n event?: string;\n page_path?: string;\n page_location?: string;\n page_title?: string;\n page_referrer?: string;\n [key: string]: unknown;\n}\n\nexport interface TrackPageViewOptions {\n /**\n * The event name to use for page views.\n * @default 'page_view'\n */\n eventName?: string;\n\n /**\n * Whether to include query parameters in the page path.\n * @default true\n */\n includeQueryParams?: boolean;\n\n /**\n * Additional data to include with each page view.\n */\n additionalData?: Record<string, unknown> | (() => Record<string, unknown>);\n}\n\nconst getAdditionalData = (additionalData: TrackPageViewOptions['additionalData']): Record<string, unknown> => {\n if (typeof additionalData === 'function') {\n return additionalData();\n }\n return additionalData ?? {};\n};\n\n/**\n * Track a page view event.\n *\n * @example\n * ```ts\n * import { trackPageView } from '@jwiedeman/gtm-kit-astro';\n *\n * // Track current page\n * trackPageView();\n *\n * // Track with custom data\n * trackPageView({\n * additionalData: { user_type: 'guest' }\n * });\n * ```\n */\nexport const trackPageView = (options: TrackPageViewOptions = {}): void => {\n const { eventName = 'page_view', includeQueryParams = true, additionalData } = options;\n\n if (typeof window === 'undefined') {\n return;\n }\n\n const pagePath = includeQueryParams ? window.location.pathname + window.location.search : window.location.pathname;\n\n const pageViewData: PageViewData = {\n event: eventName,\n page_path: pagePath,\n page_location: window.location.href,\n page_title: document.title,\n ...getAdditionalData(additionalData)\n };\n\n push(pageViewData);\n};\n\nlet viewTransitionsSetup = false;\nlet pageTrackingSetup = false;\nlet lastTrackedPath = '';\n\n/**\n * Reset page tracking state (for testing only).\n * @internal\n */\nexport const _resetPageTrackingState = (): void => {\n viewTransitionsSetup = false;\n pageTrackingSetup = false;\n lastTrackedPath = '';\n};\n\n/**\n * Set up automatic page view tracking with Astro View Transitions.\n * Call this once when the page loads.\n *\n * @example\n * ```astro\n * ---\n * // src/layouts/Layout.astro\n * ---\n * <script>\n * import { initGtm, setupViewTransitions } from '@jwiedeman/gtm-kit-astro';\n *\n * initGtm({ containers: 'GTM-XXXXXX' });\n * setupViewTransitions();\n * </script>\n * ```\n */\nexport const setupViewTransitions = (options: TrackPageViewOptions = {}): (() => void) => {\n if (typeof window === 'undefined' || typeof document === 'undefined') {\n return noop;\n }\n\n // Prevent duplicate setup\n if (viewTransitionsSetup) {\n return noop;\n }\n\n viewTransitionsSetup = true;\n\n // Track initial page view\n lastTrackedPath = window.location.pathname + window.location.search;\n trackPageView(options);\n\n // Handle Astro View Transitions\n // The 'astro:page-load' event fires after every page load, including View Transitions\n const handlePageLoad = () => {\n const currentPath = window.location.pathname + window.location.search;\n\n // Avoid duplicate tracking for the same path\n if (currentPath === lastTrackedPath) {\n return;\n }\n\n lastTrackedPath = currentPath;\n trackPageView(options);\n };\n\n document.addEventListener('astro:page-load', handlePageLoad);\n\n // Cleanup function\n return () => {\n document.removeEventListener('astro:page-load', handlePageLoad);\n viewTransitionsSetup = false;\n lastTrackedPath = '';\n };\n};\n\n/**\n * Set up page view tracking for standard navigation (no View Transitions).\n * Tracks on popstate (back/forward) and initial load.\n *\n * @example\n * ```ts\n * import { setupPageTracking } from '@jwiedeman/gtm-kit-astro';\n *\n * setupPageTracking();\n * ```\n */\nexport const setupPageTracking = (options: TrackPageViewOptions = {}): (() => void) => {\n if (typeof window === 'undefined') {\n return noop;\n }\n\n // Prevent duplicate setup\n if (pageTrackingSetup) {\n return noop;\n }\n\n pageTrackingSetup = true;\n\n // Track initial page view\n lastTrackedPath = window.location.pathname + window.location.search;\n trackPageView(options);\n\n // Handle browser back/forward navigation\n const handlePopState = () => {\n const currentPath = window.location.pathname + window.location.search;\n\n if (currentPath === lastTrackedPath) {\n return;\n }\n\n lastTrackedPath = currentPath;\n trackPageView(options);\n };\n\n window.addEventListener('popstate', handlePopState);\n\n return () => {\n window.removeEventListener('popstate', handlePopState);\n pageTrackingSetup = false;\n lastTrackedPath = '';\n };\n};\n","// Client-side GTM management\nexport {\n initGtm,\n getGtmClient,\n requireGtmClient,\n push,\n setConsentDefaults,\n updateConsent,\n whenReady,\n teardown\n} from './client';\n\n// Page tracking\nexport { trackPageView, setupViewTransitions, setupPageTracking } from './page-tracking';\nexport type { PageViewData, TrackPageViewOptions } from './page-tracking';\n\n// Re-export core types for convenience\nexport type {\n ConsentState,\n ConsentRegionOptions,\n CreateGtmClientOptions,\n DataLayerValue,\n GtmClient,\n ScriptLoadState,\n ContainerConfigInput,\n ContainerDescriptor,\n ScriptAttributes\n} from '@jwiedeman/gtm-kit';\n\n// Re-export consent helpers\nexport { consentPresets, getConsentPreset, eeaDefault, allGranted, analyticsOnly } from '@jwiedeman/gtm-kit';\n"]}
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,11 @@ var clientInstance = null;
|
|
|
6
6
|
var clientConfig = null;
|
|
7
7
|
var initGtm = (options) => {
|
|
8
8
|
if (clientInstance) {
|
|
9
|
+
if (process.env.NODE_ENV === "development" && clientConfig && JSON.stringify(options) !== JSON.stringify(clientConfig)) {
|
|
10
|
+
console.warn(
|
|
11
|
+
"[gtm-kit/astro] initGtm() called again with different options. The original configuration is kept. Call teardown() first if you need to reinitialize with new options."
|
|
12
|
+
);
|
|
13
|
+
}
|
|
9
14
|
return clientInstance;
|
|
10
15
|
}
|
|
11
16
|
clientInstance = createGtmClient(options);
|
|
@@ -109,6 +114,7 @@ var trackPageView = (options = {}) => {
|
|
|
109
114
|
push(pageViewData);
|
|
110
115
|
};
|
|
111
116
|
var viewTransitionsSetup = false;
|
|
117
|
+
var pageTrackingSetup = false;
|
|
112
118
|
var lastTrackedPath = "";
|
|
113
119
|
var setupViewTransitions = (options = {}) => {
|
|
114
120
|
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
@@ -139,6 +145,10 @@ var setupPageTracking = (options = {}) => {
|
|
|
139
145
|
if (typeof window === "undefined") {
|
|
140
146
|
return noop;
|
|
141
147
|
}
|
|
148
|
+
if (pageTrackingSetup) {
|
|
149
|
+
return noop;
|
|
150
|
+
}
|
|
151
|
+
pageTrackingSetup = true;
|
|
142
152
|
lastTrackedPath = window.location.pathname + window.location.search;
|
|
143
153
|
trackPageView(options);
|
|
144
154
|
const handlePopState = () => {
|
|
@@ -152,6 +162,7 @@ var setupPageTracking = (options = {}) => {
|
|
|
152
162
|
window.addEventListener("popstate", handlePopState);
|
|
153
163
|
return () => {
|
|
154
164
|
window.removeEventListener("popstate", handlePopState);
|
|
165
|
+
pageTrackingSetup = false;
|
|
155
166
|
lastTrackedPath = "";
|
|
156
167
|
};
|
|
157
168
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts","../src/page-tracking.ts","../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n createGtmClient,\n type ConsentRegionOptions,\n type ConsentState,\n type CreateGtmClientOptions,\n type DataLayerValue,\n type GtmClient,\n type ScriptLoadState\n} from '@jwiedeman/gtm-kit';\n\nlet clientInstance: GtmClient | null = null;\nlet clientConfig: CreateGtmClientOptions | null = null;\n\n/**\n * Initialize the GTM client for use in Astro.\n * Should be called once when the page loads.\n *\n * @example\n * ```ts\n * // In a client-side script\n * import { initGtm } from '@jwiedeman/gtm-kit-astro';\n *\n * initGtm({ containers: 'GTM-XXXXXX' });\n * ```\n */\nexport const initGtm = (options: CreateGtmClientOptions): GtmClient => {\n // If already initialized, return existing instance\n // (common in Astro with view transitions where initGtm may be called multiple times)\n if (clientInstance) {\n return clientInstance;\n }\n\n clientInstance = createGtmClient(options);\n clientConfig = options;\n clientInstance.init();\n\n return clientInstance;\n};\n\n/**\n * Get the current GTM client instance.\n * Returns null if not initialized.\n *\n * @example\n * ```ts\n * import { getGtmClient } from '@jwiedeman/gtm-kit-astro';\n *\n * const client = getGtmClient();\n * if (client) {\n * client.push({ event: 'page_view' });\n * }\n * ```\n */\nexport const getGtmClient = (): GtmClient | null => clientInstance;\n\n/**\n * Get the GTM client or throw if not initialized.\n * Use this when you expect the client to be available.\n *\n * @throws Error if GTM client is not initialized\n */\nexport const requireGtmClient = (): GtmClient => {\n if (!clientInstance) {\n throw new Error(\n '[gtm-kit/astro] GTM client not initialized. Call initGtm() first or ensure the GTM script has loaded.'\n );\n }\n return clientInstance;\n};\n\n/**\n * Push a value to the GTM dataLayer.\n * This is a convenience function that handles the case where GTM isn't initialized.\n *\n * @returns true if the value was pushed successfully, false otherwise\n *\n * @example\n * ```ts\n * import { push } from '@jwiedeman/gtm-kit-astro';\n *\n * const success = push({ event: 'button_click', button_name: 'hero_cta' });\n * if (!success) {\n * console.warn('GTM not ready');\n * }\n * ```\n */\n/**\n * Type-safe access to window properties for dataLayer.\n */\nconst getWindowDataLayer = (name: string): DataLayerValue[] | undefined => {\n if (typeof window === 'undefined') {\n return undefined;\n }\n const win = window as typeof window & Record<string, unknown>;\n const layer = win[name];\n return Array.isArray(layer) ? (layer as DataLayerValue[]) : undefined;\n};\n\nexport const push = (value: DataLayerValue): boolean => {\n const client = getGtmClient();\n if (client) {\n client.push(value);\n return true;\n } else if (typeof window !== 'undefined') {\n // Fallback: push directly to dataLayer if it exists\n const dataLayerName = clientConfig?.dataLayerName ?? 'dataLayer';\n const dataLayer = getWindowDataLayer(dataLayerName);\n if (dataLayer) {\n dataLayer.push(value);\n return true;\n } else {\n console.warn('[gtm-kit/astro] GTM not initialized and dataLayer not found.');\n return false;\n }\n }\n return false;\n};\n\n/**\n * Set consent defaults (must be called before GTM loads).\n *\n * @returns true if consent defaults were set, false if GTM not initialized\n *\n * @example\n * ```ts\n * import { setConsentDefaults } from '@jwiedeman/gtm-kit-astro';\n *\n * const success = setConsentDefaults({\n * ad_storage: 'denied',\n * analytics_storage: 'denied'\n * });\n * ```\n */\nexport const setConsentDefaults = (state: ConsentState, options?: ConsentRegionOptions): boolean => {\n const client = getGtmClient();\n if (client) {\n client.setConsentDefaults(state, options);\n return true;\n } else {\n console.warn('[gtm-kit/astro] GTM not initialized. Consent defaults should be set before init.');\n return false;\n }\n};\n\n/**\n * Update consent state after user interaction.\n *\n * @returns true if consent was updated, false if GTM not initialized\n *\n * @example\n * ```ts\n * import { updateConsent } from '@jwiedeman/gtm-kit-astro';\n *\n * // When user accepts cookies\n * const success = updateConsent({\n * ad_storage: 'granted',\n * analytics_storage: 'granted',\n * ad_user_data: 'granted',\n * ad_personalization: 'granted'\n * });\n * ```\n */\nexport const updateConsent = (state: ConsentState, options?: ConsentRegionOptions): boolean => {\n const client = getGtmClient();\n if (client) {\n client.updateConsent(state, options);\n return true;\n } else {\n console.warn('[gtm-kit/astro] GTM not initialized. Cannot update consent.');\n return false;\n }\n};\n\n/**\n * Wait for GTM scripts to be ready.\n *\n * @example\n * ```ts\n * import { whenReady } from '@jwiedeman/gtm-kit-astro';\n *\n * whenReady().then((states) => {\n * console.log('GTM loaded:', states);\n * });\n * ```\n */\nexport const whenReady = (): Promise<ScriptLoadState[]> => {\n const client = getGtmClient();\n if (client) {\n return client.whenReady();\n }\n return Promise.reject(new Error('[gtm-kit/astro] GTM not initialized.'));\n};\n\n/**\n * Clean up the GTM client.\n * Call this when navigating away or cleaning up.\n */\nexport const teardown = (): void => {\n if (clientInstance) {\n clientInstance.teardown();\n clientInstance = null;\n clientConfig = null;\n }\n};\n","import { push } from './client';\n\n// No-op function for SSR/cleanup returns\nconst noop = (): void => {\n /* no-op */\n};\n\nexport interface PageViewData {\n event?: string;\n page_path?: string;\n page_location?: string;\n page_title?: string;\n page_referrer?: string;\n [key: string]: unknown;\n}\n\nexport interface TrackPageViewOptions {\n /**\n * The event name to use for page views.\n * @default 'page_view'\n */\n eventName?: string;\n\n /**\n * Whether to include query parameters in the page path.\n * @default true\n */\n includeQueryParams?: boolean;\n\n /**\n * Additional data to include with each page view.\n */\n additionalData?: Record<string, unknown> | (() => Record<string, unknown>);\n}\n\nconst getAdditionalData = (additionalData: TrackPageViewOptions['additionalData']): Record<string, unknown> => {\n if (typeof additionalData === 'function') {\n return additionalData();\n }\n return additionalData ?? {};\n};\n\n/**\n * Track a page view event.\n *\n * @example\n * ```ts\n * import { trackPageView } from '@jwiedeman/gtm-kit-astro';\n *\n * // Track current page\n * trackPageView();\n *\n * // Track with custom data\n * trackPageView({\n * additionalData: { user_type: 'guest' }\n * });\n * ```\n */\nexport const trackPageView = (options: TrackPageViewOptions = {}): void => {\n const { eventName = 'page_view', includeQueryParams = true, additionalData } = options;\n\n if (typeof window === 'undefined') {\n return;\n }\n\n const pagePath = includeQueryParams ? window.location.pathname + window.location.search : window.location.pathname;\n\n const pageViewData: PageViewData = {\n event: eventName,\n page_path: pagePath,\n page_location: window.location.href,\n page_title: document.title,\n ...getAdditionalData(additionalData)\n };\n\n push(pageViewData);\n};\n\nlet viewTransitionsSetup = false;\nlet lastTrackedPath = '';\n\n/**\n * Reset page tracking state (for testing only).\n * @internal\n */\nexport const _resetPageTrackingState = (): void => {\n viewTransitionsSetup = false;\n lastTrackedPath = '';\n};\n\n/**\n * Set up automatic page view tracking with Astro View Transitions.\n * Call this once when the page loads.\n *\n * @example\n * ```astro\n * ---\n * // src/layouts/Layout.astro\n * ---\n * <script>\n * import { initGtm, setupViewTransitions } from '@jwiedeman/gtm-kit-astro';\n *\n * initGtm({ containers: 'GTM-XXXXXX' });\n * setupViewTransitions();\n * </script>\n * ```\n */\nexport const setupViewTransitions = (options: TrackPageViewOptions = {}): (() => void) => {\n if (typeof window === 'undefined' || typeof document === 'undefined') {\n return noop;\n }\n\n // Prevent duplicate setup\n if (viewTransitionsSetup) {\n return noop;\n }\n\n viewTransitionsSetup = true;\n\n // Track initial page view\n lastTrackedPath = window.location.pathname + window.location.search;\n trackPageView(options);\n\n // Handle Astro View Transitions\n // The 'astro:page-load' event fires after every page load, including View Transitions\n const handlePageLoad = () => {\n const currentPath = window.location.pathname + window.location.search;\n\n // Avoid duplicate tracking for the same path\n if (currentPath === lastTrackedPath) {\n return;\n }\n\n lastTrackedPath = currentPath;\n trackPageView(options);\n };\n\n document.addEventListener('astro:page-load', handlePageLoad);\n\n // Cleanup function\n return () => {\n document.removeEventListener('astro:page-load', handlePageLoad);\n viewTransitionsSetup = false;\n lastTrackedPath = '';\n };\n};\n\n/**\n * Set up page view tracking for standard navigation (no View Transitions).\n * Tracks on popstate (back/forward) and initial load.\n *\n * @example\n * ```ts\n * import { setupPageTracking } from '@jwiedeman/gtm-kit-astro';\n *\n * setupPageTracking();\n * ```\n */\nexport const setupPageTracking = (options: TrackPageViewOptions = {}): (() => void) => {\n if (typeof window === 'undefined') {\n return noop;\n }\n\n // Track initial page view\n lastTrackedPath = window.location.pathname + window.location.search;\n trackPageView(options);\n\n // Handle browser back/forward navigation\n const handlePopState = () => {\n const currentPath = window.location.pathname + window.location.search;\n\n if (currentPath === lastTrackedPath) {\n return;\n }\n\n lastTrackedPath = currentPath;\n trackPageView(options);\n };\n\n window.addEventListener('popstate', handlePopState);\n\n return () => {\n window.removeEventListener('popstate', handlePopState);\n lastTrackedPath = '';\n };\n};\n","// Client-side GTM management\nexport {\n initGtm,\n getGtmClient,\n requireGtmClient,\n push,\n setConsentDefaults,\n updateConsent,\n whenReady,\n teardown\n} from './client';\n\n// Page tracking\nexport { trackPageView, setupViewTransitions, setupPageTracking } from './page-tracking';\nexport type { PageViewData, TrackPageViewOptions } from './page-tracking';\n\n// Re-export core types for convenience\nexport type {\n ConsentState,\n ConsentRegionOptions,\n CreateGtmClientOptions,\n DataLayerValue,\n GtmClient,\n ScriptLoadState,\n ContainerConfigInput,\n ContainerDescriptor,\n ScriptAttributes\n} from '@jwiedeman/gtm-kit';\n\n// Re-export consent helpers\nexport { consentPresets, getConsentPreset, eeaDefault, allGranted, analyticsOnly } from '@jwiedeman/gtm-kit';\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/page-tracking.ts","../src/index.ts"],"names":[],"mappings":";AAAA;AAAA,EACE;AAAA,OAOK;AAEP,IAAI,iBAAmC;AACvC,IAAI,eAA8C;AAc3C,IAAM,UAAU,CAAC,YAA+C;AAGrE,MAAI,gBAAgB;AAClB,QACE,QAAQ,IAAI,aAAa,iBACzB,gBACA,KAAK,UAAU,OAAO,MAAM,KAAK,UAAU,YAAY,GACvD;AACA,cAAQ;AAAA,QACN;AAAA,MAEF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,mBAAiB,gBAAgB,OAAO;AACxC,iBAAe;AACf,iBAAe,KAAK;AAEpB,SAAO;AACT;AAgBO,IAAM,eAAe,MAAwB;AAQ7C,IAAM,mBAAmB,MAAiB;AAC/C,MAAI,CAAC,gBAAgB;AACnB,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAqBA,IAAM,qBAAqB,CAAC,SAA+C;AACzE,MAAI,OAAO,WAAW,aAAa;AACjC,WAAO;AAAA,EACT;AACA,QAAM,MAAM;AACZ,QAAM,QAAQ,IAAI,IAAI;AACtB,SAAO,MAAM,QAAQ,KAAK,IAAK,QAA6B;AAC9D;AAEO,IAAM,OAAO,CAAC,UAAmC;AA5GxD;AA6GE,QAAM,SAAS,aAAa;AAC5B,MAAI,QAAQ;AACV,WAAO,KAAK,KAAK;AACjB,WAAO;AAAA,EACT,WAAW,OAAO,WAAW,aAAa;AAExC,UAAM,iBAAgB,kDAAc,kBAAd,YAA+B;AACrD,UAAM,YAAY,mBAAmB,aAAa;AAClD,QAAI,WAAW;AACb,gBAAU,KAAK,KAAK;AACpB,aAAO;AAAA,IACT,OAAO;AACL,cAAQ,KAAK,8DAA8D;AAC3E,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAiBO,IAAM,qBAAqB,CAAC,OAAqB,YAA4C;AAClG,QAAM,SAAS,aAAa;AAC5B,MAAI,QAAQ;AACV,WAAO,mBAAmB,OAAO,OAAO;AACxC,WAAO;AAAA,EACT,OAAO;AACL,YAAQ,KAAK,kFAAkF;AAC/F,WAAO;AAAA,EACT;AACF;AAoBO,IAAM,gBAAgB,CAAC,OAAqB,YAA4C;AAC7F,QAAM,SAAS,aAAa;AAC5B,MAAI,QAAQ;AACV,WAAO,cAAc,OAAO,OAAO;AACnC,WAAO;AAAA,EACT,OAAO;AACL,YAAQ,KAAK,6DAA6D;AAC1E,WAAO;AAAA,EACT;AACF;AAcO,IAAM,YAAY,MAAkC;AACzD,QAAM,SAAS,aAAa;AAC5B,MAAI,QAAQ;AACV,WAAO,OAAO,UAAU;AAAA,EAC1B;AACA,SAAO,QAAQ,OAAO,IAAI,MAAM,sCAAsC,CAAC;AACzE;AAMO,IAAM,WAAW,MAAY;AAClC,MAAI,gBAAgB;AAClB,mBAAe,SAAS;AACxB,qBAAiB;AACjB,mBAAe;AAAA,EACjB;AACF;;;AClNA,IAAM,OAAO,MAAY;AAEzB;AA8BA,IAAM,oBAAoB,CAAC,mBAAoF;AAC7G,MAAI,OAAO,mBAAmB,YAAY;AACxC,WAAO,eAAe;AAAA,EACxB;AACA,SAAO,0CAAkB,CAAC;AAC5B;AAkBO,IAAM,gBAAgB,CAAC,UAAgC,CAAC,MAAY;AACzE,QAAM,EAAE,YAAY,aAAa,qBAAqB,MAAM,eAAe,IAAI;AAE/E,MAAI,OAAO,WAAW,aAAa;AACjC;AAAA,EACF;AAEA,QAAM,WAAW,qBAAqB,OAAO,SAAS,WAAW,OAAO,SAAS,SAAS,OAAO,SAAS;AAE1G,QAAM,eAA6B;AAAA,IACjC,OAAO;AAAA,IACP,WAAW;AAAA,IACX,eAAe,OAAO,SAAS;AAAA,IAC/B,YAAY,SAAS;AAAA,IACrB,GAAG,kBAAkB,cAAc;AAAA,EACrC;AAEA,OAAK,YAAY;AACnB;AAEA,IAAI,uBAAuB;AAC3B,IAAI,oBAAoB;AACxB,IAAI,kBAAkB;AA6Bf,IAAM,uBAAuB,CAAC,UAAgC,CAAC,MAAoB;AACxF,MAAI,OAAO,WAAW,eAAe,OAAO,aAAa,aAAa;AACpE,WAAO;AAAA,EACT;AAGA,MAAI,sBAAsB;AACxB,WAAO;AAAA,EACT;AAEA,yBAAuB;AAGvB,oBAAkB,OAAO,SAAS,WAAW,OAAO,SAAS;AAC7D,gBAAc,OAAO;AAIrB,QAAM,iBAAiB,MAAM;AAC3B,UAAM,cAAc,OAAO,SAAS,WAAW,OAAO,SAAS;AAG/D,QAAI,gBAAgB,iBAAiB;AACnC;AAAA,IACF;AAEA,sBAAkB;AAClB,kBAAc,OAAO;AAAA,EACvB;AAEA,WAAS,iBAAiB,mBAAmB,cAAc;AAG3D,SAAO,MAAM;AACX,aAAS,oBAAoB,mBAAmB,cAAc;AAC9D,2BAAuB;AACvB,sBAAkB;AAAA,EACpB;AACF;AAaO,IAAM,oBAAoB,CAAC,UAAgC,CAAC,MAAoB;AACrF,MAAI,OAAO,WAAW,aAAa;AACjC,WAAO;AAAA,EACT;AAGA,MAAI,mBAAmB;AACrB,WAAO;AAAA,EACT;AAEA,sBAAoB;AAGpB,oBAAkB,OAAO,SAAS,WAAW,OAAO,SAAS;AAC7D,gBAAc,OAAO;AAGrB,QAAM,iBAAiB,MAAM;AAC3B,UAAM,cAAc,OAAO,SAAS,WAAW,OAAO,SAAS;AAE/D,QAAI,gBAAgB,iBAAiB;AACnC;AAAA,IACF;AAEA,sBAAkB;AAClB,kBAAc,OAAO;AAAA,EACvB;AAEA,SAAO,iBAAiB,YAAY,cAAc;AAElD,SAAO,MAAM;AACX,WAAO,oBAAoB,YAAY,cAAc;AACrD,wBAAoB;AACpB,sBAAkB;AAAA,EACpB;AACF;;;ACrKA,SAAS,gBAAgB,kBAAkB,YAAY,YAAY,qBAAqB","sourcesContent":["import {\n createGtmClient,\n type ConsentRegionOptions,\n type ConsentState,\n type CreateGtmClientOptions,\n type DataLayerValue,\n type GtmClient,\n type ScriptLoadState\n} from '@jwiedeman/gtm-kit';\n\nlet clientInstance: GtmClient | null = null;\nlet clientConfig: CreateGtmClientOptions | null = null;\n\n/**\n * Initialize the GTM client for use in Astro.\n * Should be called once when the page loads.\n *\n * @example\n * ```ts\n * // In a client-side script\n * import { initGtm } from '@jwiedeman/gtm-kit-astro';\n *\n * initGtm({ containers: 'GTM-XXXXXX' });\n * ```\n */\nexport const initGtm = (options: CreateGtmClientOptions): GtmClient => {\n // If already initialized, return existing instance\n // (common in Astro with view transitions where initGtm may be called multiple times)\n if (clientInstance) {\n if (\n process.env.NODE_ENV === 'development' &&\n clientConfig &&\n JSON.stringify(options) !== JSON.stringify(clientConfig)\n ) {\n console.warn(\n '[gtm-kit/astro] initGtm() called again with different options. ' +\n 'The original configuration is kept. Call teardown() first if you need to reinitialize with new options.'\n );\n }\n return clientInstance;\n }\n\n clientInstance = createGtmClient(options);\n clientConfig = options;\n clientInstance.init();\n\n return clientInstance;\n};\n\n/**\n * Get the current GTM client instance.\n * Returns null if not initialized.\n *\n * @example\n * ```ts\n * import { getGtmClient } from '@jwiedeman/gtm-kit-astro';\n *\n * const client = getGtmClient();\n * if (client) {\n * client.push({ event: 'page_view' });\n * }\n * ```\n */\nexport const getGtmClient = (): GtmClient | null => clientInstance;\n\n/**\n * Get the GTM client or throw if not initialized.\n * Use this when you expect the client to be available.\n *\n * @throws Error if GTM client is not initialized\n */\nexport const requireGtmClient = (): GtmClient => {\n if (!clientInstance) {\n throw new Error(\n '[gtm-kit/astro] GTM client not initialized. Call initGtm() first or ensure the GTM script has loaded.'\n );\n }\n return clientInstance;\n};\n\n/**\n * Push a value to the GTM dataLayer.\n * This is a convenience function that handles the case where GTM isn't initialized.\n *\n * @returns true if the value was pushed successfully, false otherwise\n *\n * @example\n * ```ts\n * import { push } from '@jwiedeman/gtm-kit-astro';\n *\n * const success = push({ event: 'button_click', button_name: 'hero_cta' });\n * if (!success) {\n * console.warn('GTM not ready');\n * }\n * ```\n */\n/**\n * Type-safe access to window properties for dataLayer.\n */\nconst getWindowDataLayer = (name: string): DataLayerValue[] | undefined => {\n if (typeof window === 'undefined') {\n return undefined;\n }\n const win = window as typeof window & Record<string, unknown>;\n const layer = win[name];\n return Array.isArray(layer) ? (layer as DataLayerValue[]) : undefined;\n};\n\nexport const push = (value: DataLayerValue): boolean => {\n const client = getGtmClient();\n if (client) {\n client.push(value);\n return true;\n } else if (typeof window !== 'undefined') {\n // Fallback: push directly to dataLayer if it exists\n const dataLayerName = clientConfig?.dataLayerName ?? 'dataLayer';\n const dataLayer = getWindowDataLayer(dataLayerName);\n if (dataLayer) {\n dataLayer.push(value);\n return true;\n } else {\n console.warn('[gtm-kit/astro] GTM not initialized and dataLayer not found.');\n return false;\n }\n }\n return false;\n};\n\n/**\n * Set consent defaults (must be called before GTM loads).\n *\n * @returns true if consent defaults were set, false if GTM not initialized\n *\n * @example\n * ```ts\n * import { setConsentDefaults } from '@jwiedeman/gtm-kit-astro';\n *\n * const success = setConsentDefaults({\n * ad_storage: 'denied',\n * analytics_storage: 'denied'\n * });\n * ```\n */\nexport const setConsentDefaults = (state: ConsentState, options?: ConsentRegionOptions): boolean => {\n const client = getGtmClient();\n if (client) {\n client.setConsentDefaults(state, options);\n return true;\n } else {\n console.warn('[gtm-kit/astro] GTM not initialized. Consent defaults should be set before init.');\n return false;\n }\n};\n\n/**\n * Update consent state after user interaction.\n *\n * @returns true if consent was updated, false if GTM not initialized\n *\n * @example\n * ```ts\n * import { updateConsent } from '@jwiedeman/gtm-kit-astro';\n *\n * // When user accepts cookies\n * const success = updateConsent({\n * ad_storage: 'granted',\n * analytics_storage: 'granted',\n * ad_user_data: 'granted',\n * ad_personalization: 'granted'\n * });\n * ```\n */\nexport const updateConsent = (state: ConsentState, options?: ConsentRegionOptions): boolean => {\n const client = getGtmClient();\n if (client) {\n client.updateConsent(state, options);\n return true;\n } else {\n console.warn('[gtm-kit/astro] GTM not initialized. Cannot update consent.');\n return false;\n }\n};\n\n/**\n * Wait for GTM scripts to be ready.\n *\n * @example\n * ```ts\n * import { whenReady } from '@jwiedeman/gtm-kit-astro';\n *\n * whenReady().then((states) => {\n * console.log('GTM loaded:', states);\n * });\n * ```\n */\nexport const whenReady = (): Promise<ScriptLoadState[]> => {\n const client = getGtmClient();\n if (client) {\n return client.whenReady();\n }\n return Promise.reject(new Error('[gtm-kit/astro] GTM not initialized.'));\n};\n\n/**\n * Clean up the GTM client.\n * Call this when navigating away or cleaning up.\n */\nexport const teardown = (): void => {\n if (clientInstance) {\n clientInstance.teardown();\n clientInstance = null;\n clientConfig = null;\n }\n};\n","import { push } from './client';\n\n// No-op function for SSR/cleanup returns\nconst noop = (): void => {\n /* no-op */\n};\n\nexport interface PageViewData {\n event?: string;\n page_path?: string;\n page_location?: string;\n page_title?: string;\n page_referrer?: string;\n [key: string]: unknown;\n}\n\nexport interface TrackPageViewOptions {\n /**\n * The event name to use for page views.\n * @default 'page_view'\n */\n eventName?: string;\n\n /**\n * Whether to include query parameters in the page path.\n * @default true\n */\n includeQueryParams?: boolean;\n\n /**\n * Additional data to include with each page view.\n */\n additionalData?: Record<string, unknown> | (() => Record<string, unknown>);\n}\n\nconst getAdditionalData = (additionalData: TrackPageViewOptions['additionalData']): Record<string, unknown> => {\n if (typeof additionalData === 'function') {\n return additionalData();\n }\n return additionalData ?? {};\n};\n\n/**\n * Track a page view event.\n *\n * @example\n * ```ts\n * import { trackPageView } from '@jwiedeman/gtm-kit-astro';\n *\n * // Track current page\n * trackPageView();\n *\n * // Track with custom data\n * trackPageView({\n * additionalData: { user_type: 'guest' }\n * });\n * ```\n */\nexport const trackPageView = (options: TrackPageViewOptions = {}): void => {\n const { eventName = 'page_view', includeQueryParams = true, additionalData } = options;\n\n if (typeof window === 'undefined') {\n return;\n }\n\n const pagePath = includeQueryParams ? window.location.pathname + window.location.search : window.location.pathname;\n\n const pageViewData: PageViewData = {\n event: eventName,\n page_path: pagePath,\n page_location: window.location.href,\n page_title: document.title,\n ...getAdditionalData(additionalData)\n };\n\n push(pageViewData);\n};\n\nlet viewTransitionsSetup = false;\nlet pageTrackingSetup = false;\nlet lastTrackedPath = '';\n\n/**\n * Reset page tracking state (for testing only).\n * @internal\n */\nexport const _resetPageTrackingState = (): void => {\n viewTransitionsSetup = false;\n pageTrackingSetup = false;\n lastTrackedPath = '';\n};\n\n/**\n * Set up automatic page view tracking with Astro View Transitions.\n * Call this once when the page loads.\n *\n * @example\n * ```astro\n * ---\n * // src/layouts/Layout.astro\n * ---\n * <script>\n * import { initGtm, setupViewTransitions } from '@jwiedeman/gtm-kit-astro';\n *\n * initGtm({ containers: 'GTM-XXXXXX' });\n * setupViewTransitions();\n * </script>\n * ```\n */\nexport const setupViewTransitions = (options: TrackPageViewOptions = {}): (() => void) => {\n if (typeof window === 'undefined' || typeof document === 'undefined') {\n return noop;\n }\n\n // Prevent duplicate setup\n if (viewTransitionsSetup) {\n return noop;\n }\n\n viewTransitionsSetup = true;\n\n // Track initial page view\n lastTrackedPath = window.location.pathname + window.location.search;\n trackPageView(options);\n\n // Handle Astro View Transitions\n // The 'astro:page-load' event fires after every page load, including View Transitions\n const handlePageLoad = () => {\n const currentPath = window.location.pathname + window.location.search;\n\n // Avoid duplicate tracking for the same path\n if (currentPath === lastTrackedPath) {\n return;\n }\n\n lastTrackedPath = currentPath;\n trackPageView(options);\n };\n\n document.addEventListener('astro:page-load', handlePageLoad);\n\n // Cleanup function\n return () => {\n document.removeEventListener('astro:page-load', handlePageLoad);\n viewTransitionsSetup = false;\n lastTrackedPath = '';\n };\n};\n\n/**\n * Set up page view tracking for standard navigation (no View Transitions).\n * Tracks on popstate (back/forward) and initial load.\n *\n * @example\n * ```ts\n * import { setupPageTracking } from '@jwiedeman/gtm-kit-astro';\n *\n * setupPageTracking();\n * ```\n */\nexport const setupPageTracking = (options: TrackPageViewOptions = {}): (() => void) => {\n if (typeof window === 'undefined') {\n return noop;\n }\n\n // Prevent duplicate setup\n if (pageTrackingSetup) {\n return noop;\n }\n\n pageTrackingSetup = true;\n\n // Track initial page view\n lastTrackedPath = window.location.pathname + window.location.search;\n trackPageView(options);\n\n // Handle browser back/forward navigation\n const handlePopState = () => {\n const currentPath = window.location.pathname + window.location.search;\n\n if (currentPath === lastTrackedPath) {\n return;\n }\n\n lastTrackedPath = currentPath;\n trackPageView(options);\n };\n\n window.addEventListener('popstate', handlePopState);\n\n return () => {\n window.removeEventListener('popstate', handlePopState);\n pageTrackingSetup = false;\n lastTrackedPath = '';\n };\n};\n","// Client-side GTM management\nexport {\n initGtm,\n getGtmClient,\n requireGtmClient,\n push,\n setConsentDefaults,\n updateConsent,\n whenReady,\n teardown\n} from './client';\n\n// Page tracking\nexport { trackPageView, setupViewTransitions, setupPageTracking } from './page-tracking';\nexport type { PageViewData, TrackPageViewOptions } from './page-tracking';\n\n// Re-export core types for convenience\nexport type {\n ConsentState,\n ConsentRegionOptions,\n CreateGtmClientOptions,\n DataLayerValue,\n GtmClient,\n ScriptLoadState,\n ContainerConfigInput,\n ContainerDescriptor,\n ScriptAttributes\n} from '@jwiedeman/gtm-kit';\n\n// Re-export consent helpers\nexport { consentPresets, getConsentPreset, eeaDefault, allGranted, analyticsOnly } from '@jwiedeman/gtm-kit';\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jwiedeman/gtm-kit-astro",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Astro components and helpers for GTM Kit - Google Tag Manager integration with View Transitions support.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -44,8 +44,7 @@
|
|
|
44
44
|
}
|
|
45
45
|
},
|
|
46
46
|
"files": [
|
|
47
|
-
"dist"
|
|
48
|
-
"src/components/*.astro"
|
|
47
|
+
"dist"
|
|
49
48
|
],
|
|
50
49
|
"scripts": {
|
|
51
50
|
"build": "tsup",
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* GTM Head Component
|
|
4
|
-
*
|
|
5
|
-
* A convenience component that sets up GTM in the document head
|
|
6
|
-
* with optional consent mode and auto page tracking.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* ```astro
|
|
10
|
-
* ---
|
|
11
|
-
* import { GtmHead } from '@jwiedeman/gtm-kit-astro/components';
|
|
12
|
-
* ---
|
|
13
|
-
* <head>
|
|
14
|
-
* <GtmHead
|
|
15
|
-
* containers="GTM-XXXXXX"
|
|
16
|
-
* enablePageTracking
|
|
17
|
-
* defaultConsent={{
|
|
18
|
-
* ad_storage: 'denied',
|
|
19
|
-
* analytics_storage: 'denied'
|
|
20
|
-
* }}
|
|
21
|
-
* />
|
|
22
|
-
* </head>
|
|
23
|
-
* ```
|
|
24
|
-
*/
|
|
25
|
-
import type { ContainerConfigInput, ScriptAttributes, ConsentState } from '@jwiedeman/gtm-kit';
|
|
26
|
-
import {
|
|
27
|
-
generateScriptTags,
|
|
28
|
-
generateDataLayerScript,
|
|
29
|
-
escapeJsString,
|
|
30
|
-
isValidJsIdentifier,
|
|
31
|
-
DEFAULT_GTM_HOST
|
|
32
|
-
} from './helpers';
|
|
33
|
-
|
|
34
|
-
export interface Props {
|
|
35
|
-
/** GTM container ID(s) - single string or array */
|
|
36
|
-
containers: ContainerConfigInput | ContainerConfigInput[];
|
|
37
|
-
/** GTM host URL */
|
|
38
|
-
host?: string;
|
|
39
|
-
/** Query parameters to add to all containers */
|
|
40
|
-
defaultQueryParams?: Record<string, string | number | boolean>;
|
|
41
|
-
/** Script tag attributes (async, defer, nonce, etc.) */
|
|
42
|
-
scriptAttributes?: ScriptAttributes;
|
|
43
|
-
/** Custom dataLayer name */
|
|
44
|
-
dataLayerName?: string;
|
|
45
|
-
/** Default consent state (set before GTM loads) */
|
|
46
|
-
defaultConsent?: ConsentState;
|
|
47
|
-
/** Enable automatic page view tracking with View Transitions */
|
|
48
|
-
enablePageTracking?: boolean;
|
|
49
|
-
/** Custom page view event name */
|
|
50
|
-
pageViewEventName?: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const {
|
|
54
|
-
containers,
|
|
55
|
-
host = DEFAULT_GTM_HOST,
|
|
56
|
-
defaultQueryParams,
|
|
57
|
-
scriptAttributes,
|
|
58
|
-
dataLayerName = 'dataLayer',
|
|
59
|
-
defaultConsent,
|
|
60
|
-
enablePageTracking = false,
|
|
61
|
-
pageViewEventName = 'page_view'
|
|
62
|
-
} = Astro.props;
|
|
63
|
-
|
|
64
|
-
const scripts = generateScriptTags({
|
|
65
|
-
containers,
|
|
66
|
-
host,
|
|
67
|
-
defaultQueryParams,
|
|
68
|
-
scriptAttributes,
|
|
69
|
-
dataLayerName
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// Build initialization script
|
|
73
|
-
let initScript = generateDataLayerScript(dataLayerName);
|
|
74
|
-
|
|
75
|
-
// Add consent defaults if provided
|
|
76
|
-
if (defaultConsent) {
|
|
77
|
-
// Validate consent keys are valid identifiers and escape values to prevent XSS
|
|
78
|
-
const consentEntries = Object.entries(defaultConsent)
|
|
79
|
-
.map(([key, value]) => {
|
|
80
|
-
// Consent keys should be valid identifiers (e.g., 'ad_storage', 'analytics_storage')
|
|
81
|
-
if (!isValidJsIdentifier(key)) {
|
|
82
|
-
throw new Error(`Invalid consent key: "${key}". Must be a valid JavaScript identifier.`);
|
|
83
|
-
}
|
|
84
|
-
// Escape the value to prevent XSS
|
|
85
|
-
const escapedValue = escapeJsString(String(value));
|
|
86
|
-
return `'${key}':'${escapedValue}'`;
|
|
87
|
-
})
|
|
88
|
-
.join(',');
|
|
89
|
-
initScript += `${dataLayerName}.push(['consent','default',{${consentEntries}}]);`;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Generate the containers config for client-side initialization
|
|
93
|
-
const containersArray = Array.isArray(containers) ? containers : [containers];
|
|
94
|
-
const containersJson = JSON.stringify(containersArray);
|
|
95
|
-
---
|
|
96
|
-
|
|
97
|
-
<script is:inline set:html={initScript} />
|
|
98
|
-
{scripts.map((script) => (
|
|
99
|
-
<script
|
|
100
|
-
src={script.src}
|
|
101
|
-
async={script.async}
|
|
102
|
-
defer={script.defer}
|
|
103
|
-
nonce={script.nonce}
|
|
104
|
-
data-gtm-container-id={script.id}
|
|
105
|
-
{...script.attributes}
|
|
106
|
-
/>
|
|
107
|
-
))}
|
|
108
|
-
|
|
109
|
-
{enablePageTracking && (
|
|
110
|
-
<script
|
|
111
|
-
define:vars={{ containersJson, dataLayerName, pageViewEventName }}
|
|
112
|
-
>
|
|
113
|
-
// Initialize GTM client and set up page tracking
|
|
114
|
-
import('@jwiedeman/gtm-kit-astro')
|
|
115
|
-
.then(({ initGtm, setupViewTransitions }) => {
|
|
116
|
-
try {
|
|
117
|
-
const containers = JSON.parse(containersJson);
|
|
118
|
-
initGtm({ containers, dataLayerName });
|
|
119
|
-
setupViewTransitions({ eventName: pageViewEventName });
|
|
120
|
-
} catch (error) {
|
|
121
|
-
console.error('[gtm-kit/astro] Failed to initialize GTM:', error);
|
|
122
|
-
}
|
|
123
|
-
})
|
|
124
|
-
.catch((error) => {
|
|
125
|
-
console.error('[gtm-kit/astro] Failed to load GTM module:', error);
|
|
126
|
-
});
|
|
127
|
-
</script>
|
|
128
|
-
)}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* GTM NoScript Component
|
|
4
|
-
*
|
|
5
|
-
* Renders the GTM noscript fallback iframe(s).
|
|
6
|
-
* Use this immediately after the opening <body> tag.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* ```astro
|
|
10
|
-
* ---
|
|
11
|
-
* import { GtmNoScript } from '@jwiedeman/gtm-kit-astro/components';
|
|
12
|
-
* ---
|
|
13
|
-
* <body>
|
|
14
|
-
* <GtmNoScript containers="GTM-XXXXXX" />
|
|
15
|
-
* <!-- rest of body -->
|
|
16
|
-
* </body>
|
|
17
|
-
* ```
|
|
18
|
-
*/
|
|
19
|
-
import type { ContainerConfigInput } from '@jwiedeman/gtm-kit';
|
|
20
|
-
import { generateNoscriptTags, DEFAULT_GTM_HOST } from './helpers';
|
|
21
|
-
|
|
22
|
-
export interface Props {
|
|
23
|
-
/** GTM container ID(s) - single string or array */
|
|
24
|
-
containers: ContainerConfigInput | ContainerConfigInput[];
|
|
25
|
-
/** GTM host URL */
|
|
26
|
-
host?: string;
|
|
27
|
-
/** Query parameters to add to all containers */
|
|
28
|
-
defaultQueryParams?: Record<string, string | number | boolean>;
|
|
29
|
-
/** Custom iframe attributes */
|
|
30
|
-
iframeAttributes?: Record<string, string | number | boolean>;
|
|
31
|
-
/** Custom dataLayer name */
|
|
32
|
-
dataLayerName?: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const {
|
|
36
|
-
containers,
|
|
37
|
-
host = DEFAULT_GTM_HOST,
|
|
38
|
-
defaultQueryParams,
|
|
39
|
-
iframeAttributes,
|
|
40
|
-
dataLayerName = 'dataLayer'
|
|
41
|
-
} = Astro.props;
|
|
42
|
-
|
|
43
|
-
const noscripts = generateNoscriptTags({
|
|
44
|
-
containers,
|
|
45
|
-
host,
|
|
46
|
-
defaultQueryParams,
|
|
47
|
-
iframeAttributes,
|
|
48
|
-
dataLayerName
|
|
49
|
-
});
|
|
50
|
-
---
|
|
51
|
-
|
|
52
|
-
{noscripts.map((ns) => (
|
|
53
|
-
<noscript>
|
|
54
|
-
<iframe src={ns.src} {...ns.attributes} />
|
|
55
|
-
</noscript>
|
|
56
|
-
))}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* GTM Script Component
|
|
4
|
-
*
|
|
5
|
-
* Renders the GTM script tag(s) in the document head.
|
|
6
|
-
* Use this in your <head> section.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* ```astro
|
|
10
|
-
* ---
|
|
11
|
-
* import { GtmScript } from '@jwiedeman/gtm-kit-astro/components';
|
|
12
|
-
* ---
|
|
13
|
-
* <head>
|
|
14
|
-
* <GtmScript containers="GTM-XXXXXX" />
|
|
15
|
-
* </head>
|
|
16
|
-
* ```
|
|
17
|
-
*/
|
|
18
|
-
import type { ContainerConfigInput, ScriptAttributes } from '@jwiedeman/gtm-kit';
|
|
19
|
-
import {
|
|
20
|
-
generateScriptTags,
|
|
21
|
-
generateDataLayerScript,
|
|
22
|
-
DEFAULT_GTM_HOST
|
|
23
|
-
} from './helpers';
|
|
24
|
-
|
|
25
|
-
export interface Props {
|
|
26
|
-
/** GTM container ID(s) - single string or array */
|
|
27
|
-
containers: ContainerConfigInput | ContainerConfigInput[];
|
|
28
|
-
/** GTM host URL */
|
|
29
|
-
host?: string;
|
|
30
|
-
/** Query parameters to add to all containers */
|
|
31
|
-
defaultQueryParams?: Record<string, string | number | boolean>;
|
|
32
|
-
/** Script tag attributes (async, defer, nonce, etc.) */
|
|
33
|
-
scriptAttributes?: ScriptAttributes;
|
|
34
|
-
/** Custom dataLayer name */
|
|
35
|
-
dataLayerName?: string;
|
|
36
|
-
/** Whether to include dataLayer initialization script */
|
|
37
|
-
initDataLayer?: boolean;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const {
|
|
41
|
-
containers,
|
|
42
|
-
host = DEFAULT_GTM_HOST,
|
|
43
|
-
defaultQueryParams,
|
|
44
|
-
scriptAttributes,
|
|
45
|
-
dataLayerName = 'dataLayer',
|
|
46
|
-
initDataLayer = true
|
|
47
|
-
} = Astro.props;
|
|
48
|
-
|
|
49
|
-
const scripts = generateScriptTags({
|
|
50
|
-
containers,
|
|
51
|
-
host,
|
|
52
|
-
defaultQueryParams,
|
|
53
|
-
scriptAttributes,
|
|
54
|
-
dataLayerName
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const dataLayerInit = initDataLayer ? generateDataLayerScript(dataLayerName) : null;
|
|
58
|
-
---
|
|
59
|
-
|
|
60
|
-
{dataLayerInit && <script is:inline set:html={dataLayerInit} />}
|
|
61
|
-
{scripts.map((script) => (
|
|
62
|
-
<script
|
|
63
|
-
src={script.src}
|
|
64
|
-
async={script.async}
|
|
65
|
-
defer={script.defer}
|
|
66
|
-
nonce={script.nonce}
|
|
67
|
-
data-gtm-container-id={script.id}
|
|
68
|
-
{...script.attributes}
|
|
69
|
-
/>
|
|
70
|
-
))}
|