├── renovate.json ├── example ├── plugins │ └── gtm.js ├── pages │ └── index.vue └── nuxt.config.js ├── .eslintignore ├── .gitignore ├── .eslintrc.js ├── babel.config.js ├── .editorconfig ├── lib ├── plugin.utils.js ├── defaults.js ├── compatibility.js ├── plugin.mock.js ├── plugin.js └── module.js ├── jest.config.js ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── package.json ├── test └── module.test.js ├── README.md └── CHANGELOG.md /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@nuxtjs" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /example/plugins/gtm.js: -------------------------------------------------------------------------------- 1 | export default function ({ $gtm }) { 2 | $gtm.init('GTM-56H68LH') 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Common 2 | node_modules 3 | dist 4 | .nuxt 5 | coverage 6 | 7 | # Plugin 8 | lib/plugin*.js 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.iml 3 | .idea 4 | *.log* 5 | .nuxt 6 | .vscode 7 | .DS_Store 8 | coverage 9 | dist 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | parser: 'babel-eslint', 5 | sourceType: 'module' 6 | }, 7 | extends: [ 8 | '@nuxtjs' 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', { 5 | targets: { 6 | esmodules: true 7 | } 8 | } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /example/pages/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = space 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /lib/plugin.utils.js: -------------------------------------------------------------------------------- 1 | const logSyle = 'background: #2E495E;border-radius: 0.5em;color: white;font-weight: bold;padding: 2px 0.5em;' 2 | 3 | export function log(...args) { 4 | // eslint-disable-next-line no-console 5 | <% if (options.debug) { %>console.log('%cGTM', logSyle, ...args)<% } %> 6 | } 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | collectCoverage: true, 4 | collectCoverageFrom: [ 5 | 'lib/**/*.js', 6 | '!lib/plugin.js' 7 | ], 8 | moduleNameMapper: { 9 | '^~/(.*)$': '/lib/$1', 10 | '^~~$': '', 11 | '^@@$': '', 12 | '^@/(.*)$': '/lib/$1' 13 | }, 14 | transform: { 15 | '^.+\\.js$': 'babel-jest' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/defaults.js: -------------------------------------------------------------------------------- 1 | const defaults = { 2 | enabled: undefined, 3 | debug: false, 4 | 5 | id: undefined, 6 | layer: 'dataLayer', 7 | variables: {}, 8 | 9 | pageTracking: false, 10 | pageViewEventName: 'nuxtRoute', 11 | 12 | autoInit: true, 13 | respectDoNotTrack: true, 14 | 15 | scriptId: 'gtm-script', 16 | scriptDefer: false, 17 | scriptURL: 'https://www.googletagmanager.com/gtm.js', 18 | crossOrigin: false, 19 | 20 | noscript: true, 21 | noscriptId: 'gtm-noscript', 22 | noscriptURL: 'https://www.googletagmanager.com/ns.html' 23 | } 24 | 25 | module.exports = defaults 26 | -------------------------------------------------------------------------------- /lib/compatibility.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk') 2 | const semver = require('semver') 3 | 4 | function requireNuxtVersion (nuxt, version) { 5 | const pkgName = require('../package.json').name 6 | const currentVersion = semver.coerce(nuxt.constructor.version) 7 | const requiredVersion = semver.coerce(version) 8 | 9 | if (semver.lt(currentVersion, requiredVersion)) { 10 | throw new Error(`\n 11 | ${chalk.cyan(pkgName)} is not compatible with your current Nuxt version : ${chalk.yellow('v' + currentVersion)}\n 12 | Required: ${chalk.green('v' + requiredVersion)} or ${chalk.cyan('higher')} 13 | `) 14 | } 15 | } 16 | 17 | module.exports = { 18 | requireNuxtVersion 19 | } 20 | -------------------------------------------------------------------------------- /example/nuxt.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path') 2 | 3 | module.exports = { 4 | rootDir: resolve(__dirname, '..'), 5 | buildDir: resolve(__dirname, '.nuxt'), 6 | head: { 7 | title: '@nuxtjs/gtm-module' 8 | }, 9 | srcDir: __dirname, 10 | render: { 11 | resourceHints: false 12 | }, 13 | modules: [ 14 | { handler: require('../') } 15 | ], 16 | plugins: [ 17 | '~/plugins/gtm' 18 | ], 19 | gtm: { 20 | id: process.env.GTM_ID || 'GTM-KLQB72K', 21 | scriptDefer: true, 22 | pageTracking: true, 23 | // layer: 'test', 24 | variables: { 25 | test: '1' 26 | } 27 | }, 28 | publicRuntimeConfig: { 29 | gtm: { 30 | id: 'GTM-KLQB72K&runtime' 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | 12 | jobs: 13 | ci: 14 | runs-on: ${{ matrix.os }} 15 | 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest] 19 | node: [12] 20 | 21 | steps: 22 | - uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node }} 25 | 26 | - name: checkout 27 | uses: actions/checkout@master 28 | 29 | - name: cache node_modules 30 | uses: actions/cache@v2 31 | with: 32 | path: node_modules 33 | key: ${{ matrix.os }}-node-v${{ matrix.node }}-deps-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }} 34 | 35 | - name: Install dependencies 36 | if: steps.cache.outputs.cache-hit != 'true' 37 | run: yarn 38 | 39 | - name: Lint 40 | run: yarn lint 41 | 42 | - name: Test 43 | run: yarn jest 44 | 45 | # - name: Coverage 46 | # uses: codecov/codecov-action@v1 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Nuxt.js Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nuxtjs/gtm", 3 | "version": "2.4.0", 4 | "description": "Google Tag Manager Module for Nuxt.js", 5 | "repository": "nuxt-community/gtm-module", 6 | "license": "MIT", 7 | "main": "lib/module.js", 8 | "files": [ 9 | "lib" 10 | ], 11 | "scripts": { 12 | "dev": "nuxt example", 13 | "generate": "nuxt generate example", 14 | "lint": "eslint --ext .js,.vue example lib test", 15 | "release": "yarn test && standard-version && git push --follow-tags && npm publish", 16 | "test": "yarn lint && jest" 17 | }, 18 | "dependencies": { 19 | "chalk": "^4.1.0", 20 | "semver": "^7.3.2" 21 | }, 22 | "devDependencies": { 23 | "@babel/core": "latest", 24 | "@babel/preset-env": "latest", 25 | "@nuxtjs/eslint-config": "latest", 26 | "@nuxtjs/module-test-utils": "latest", 27 | "@types/jest": "latest", 28 | "babel-eslint": "latest", 29 | "babel-jest": "latest", 30 | "codecov": "latest", 31 | "eslint": "latest", 32 | "jest": "latest", 33 | "nuxt-edge": "latest", 34 | "standard-version": "latest" 35 | }, 36 | "publishConfig": { 37 | "access": "public" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/plugin.mock.js: -------------------------------------------------------------------------------- 1 | // This is a mock version because gtm module is disabled 2 | // You can explicitly enable module using `gtm.enabled: true` in nuxt.config 3 | import { log } from './gtm.utils' 4 | 5 | const _layer = '<%= options.layer %>' 6 | const _id = '<%= options.id %>' 7 | 8 | function startPageTracking (ctx) { 9 | ctx.app.router.afterEach((to) => { 10 | setTimeout(() => { 11 | ctx.$gtm.push(to.gtm || { 12 | routeName: to.name, 13 | pageType: 'PageView', 14 | pageUrl: '<%= options.routerBase %>' + to.fullPath, 15 | pageTitle: (typeof document !== 'undefined' && document.title) || '', 16 | event: '<%= options.pageViewEventName %>' 17 | }) 18 | }, 250) 19 | }) 20 | } 21 | 22 | export default function (ctx, inject) { 23 | log('Using mocked API. Real GTM events will not be reported.') 24 | const gtm = { 25 | init: (id) => { 26 | log('init', id) 27 | }, 28 | push: (event) => { 29 | log('push', process.client ? event : JSON.stringify(event)) 30 | if (typeof event.eventCallback === 'function') { 31 | event.eventCallback() 32 | } 33 | } 34 | } 35 | 36 | ctx.$gtm = gtm 37 | inject('gtm', gtm) 38 | <% if (options.pageTracking) { %>if (process.client) { startPageTracking(ctx); }<% } %> 39 | } 40 | -------------------------------------------------------------------------------- /lib/plugin.js: -------------------------------------------------------------------------------- 1 | import { log } from './gtm.utils' 2 | 3 | const _layer = '<%= options.layer %>' 4 | const _id = '<%= options.id %>' 5 | 6 | function gtmClient(ctx, initialized) { 7 | return { 8 | init(id = _id) { 9 | if (initialized[id] || !window._gtm_inject) { 10 | return 11 | } 12 | window._gtm_inject(id) 13 | initialized[id] = true 14 | log('init', id) 15 | }, 16 | push(obj) { 17 | if (!window[_layer]) { 18 | window[_layer] = [] 19 | } 20 | window[_layer].push(obj) 21 | log('push', obj) 22 | } 23 | } 24 | } 25 | 26 | function gtmServer(ctx, initialized) { 27 | const events = [] 28 | const inits = [] 29 | 30 | ctx.beforeNuxtRender(() => { 31 | if (!inits.length && !events.length) { 32 | return 33 | } 34 | 35 | const gtmScript = ctx.app.head.script.find(s => s.hid == '<%= options.scriptId %>') 36 | gtmScript.innerHTML = `window['${_layer}']=${JSON.stringify(events)};${gtmScript.innerHTML}` 37 | 38 | if (inits.length) { 39 | gtmScript.innerHTML += `;${JSON.stringify(inits)}.forEach(function(i){window._gtm_inject(i)})` 40 | } 41 | <% if (options.noscript) { %> 42 | const gtmIframe = ctx.app.head.noscript.find(s => s.hid == '<%= options.noscriptId %>') 43 | const renderIframe = id => `<%= options.renderIframe('${id}') %>` 44 | if (inits.length) { 45 | gtmIframe.innerHTML += inits.map(renderIframe) 46 | } 47 | <% } %> 48 | }) 49 | 50 | return { 51 | init(id = _id) { 52 | if (initialized[id]) { 53 | return 54 | } 55 | inits.push(id) 56 | initialized[id] = true 57 | log('init', id) 58 | }, 59 | push(obj) { 60 | events.push(obj) 61 | log('push', JSON.stringify(obj)) 62 | } 63 | } 64 | } 65 | 66 | function startPageTracking(ctx) { 67 | ctx.app.router.afterEach((to) => { 68 | setTimeout(() => { 69 | ctx.$gtm.push(to.gtm || { 70 | routeName: to.name, 71 | pageType: 'PageView', 72 | pageUrl: '<%= options.routerBase %>' + to.fullPath, 73 | pageTitle: (typeof document !== 'undefined' && document.title) || '', 74 | event: '<%= options.pageViewEventName %>' 75 | }) 76 | }, 250) 77 | }) 78 | } 79 | 80 | export default function (ctx, inject) { 81 | const runtimeConfig = (ctx.$config && ctx.$config.gtm) || {} 82 | const autoInit = <%= options.autoInit %> 83 | const id = '<%= options.id %>' 84 | const runtimeId = runtimeConfig.id 85 | const initialized = autoInit && id ? {[id]: true} : {} 86 | const $gtm = process.client ? gtmClient(ctx, initialized) : gtmServer(ctx, initialized) 87 | if (autoInit && runtimeId && runtimeId !== id) { 88 | $gtm.init(runtimeId) 89 | } 90 | ctx.$gtm = $gtm 91 | inject('gtm', ctx.$gtm) 92 | <% if (options.pageTracking) { %>if (process.client) { startPageTracking(ctx); }<% } %> 93 | } 94 | -------------------------------------------------------------------------------- /test/module.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { setup, loadConfig, get, url } = require('@nuxtjs/module-test-utils') 3 | const defaultSettings = require(path.join(__dirname, '../', 'lib', 'defaults.js')) 4 | 5 | function expectPageTrackingEvent (eventsArray, expectedEvent) { 6 | return new Promise((resolve) => { 7 | // Need to wait at least 250ms as that's how long plugin delays before triggering event. 8 | setTimeout(() => { 9 | expect(eventsArray).toStrictEqual( 10 | expect.arrayContaining([ 11 | expect.objectContaining(expectedEvent) 12 | ]) 13 | ) 14 | expect(eventsArray.filter(event => event.event === 'nuxtRoute').length).toBe(1) 15 | resolve() 16 | }, 300) 17 | }) 18 | } 19 | 20 | const modes = ['universal', 'spa'] 21 | 22 | for (const mode of modes) { 23 | describe(`Module (${mode} mode)`, () => { 24 | let nuxt 25 | 26 | const nuxtConfig = loadConfig(__dirname, '../../example') 27 | nuxtConfig.mode = mode 28 | 29 | const gtmId = nuxtConfig.gtm.id 30 | const runtimeId = nuxtConfig.publicRuntimeConfig.gtm.id 31 | const scriptId = nuxtConfig.gtm.scriptId || defaultSettings.scriptId 32 | const noscriptId = nuxtConfig.gtm.noscriptId || defaultSettings.noscriptId 33 | 34 | beforeAll(async () => { 35 | ({ nuxt } = (await setup(nuxtConfig))) 36 | }, 60000) 37 | 38 | afterAll(async () => { 39 | await nuxt.close() 40 | }) 41 | 42 | test('Render', async () => { 43 | const html = await get('/') 44 | const expected = { universal: 'Works!', spa: 'Loading...' }[mode] 45 | expect(html).toContain(expected) 46 | }) 47 | 48 | test('Has GTM script', async () => { 49 | const html = await get('/') 50 | expect(html).toContain(`data-hid="${scriptId}"`) 51 | }) 52 | 53 | test('Has GTM noscript', async () => { 54 | const html = await get('/') 55 | expect(html).toContain(`data-hid="${noscriptId}"`) 56 | }) 57 | 58 | // test with real GTM id 59 | test('GTM should be defined ($nuxt.$gtm)', async () => { 60 | const window = await nuxt.renderAndGetWindow(url('/')) 61 | expect(window.$nuxt.$gtm).toBeDefined() 62 | }) 63 | 64 | test('Should include runtimeConfig', async () => { 65 | const window = await nuxt.renderAndGetWindow(url('/')) 66 | 67 | const headGtmScriptsExternal = window.document.querySelectorAll(`head script[src^="https://www.googletagmanager.com/gtm.js?id=${runtimeId}"]`) 68 | 69 | expect(headGtmScriptsExternal.length).toBe(1) 70 | }) 71 | 72 | test('Verifying duplicate GTM script', async () => { 73 | const window = await nuxt.renderAndGetWindow(url('/')) 74 | 75 | const headGtmScriptsExternal = window.document.querySelectorAll(`head script[src^="https://www.googletagmanager.com/gtm.js?id=${gtmId}"]`) 76 | const headGtmScriptsHid = window.document.querySelectorAll(`head script[data-hid="${scriptId}"]`) 77 | const totalAmoutOfGtmScriptsAtHead = headGtmScriptsExternal.length + headGtmScriptsHid.length 78 | 79 | expect(totalAmoutOfGtmScriptsAtHead).toBeLessThan(4) 80 | }) 81 | 82 | test('Should include pushed event', async () => { 83 | const window = await nuxt.renderAndGetWindow(url('/')) 84 | expect(window.dataLayer).toStrictEqual( 85 | expect.arrayContaining([ 86 | expect.objectContaining({ event: 'ssr' }) 87 | ]) 88 | ) 89 | expect(window.dataLayer.filter(event => event.event === 'ssr').length).toBe(1) 90 | }) 91 | 92 | test('Should include page tracking event', async () => { 93 | const window = await nuxt.renderAndGetWindow(url('/')) 94 | 95 | await expectPageTrackingEvent(window.dataLayer, { 96 | event: 'nuxtRoute', 97 | pageTitle: '@nuxtjs/gtm-module', 98 | pageType: 'PageView', 99 | pageUrl: '/', 100 | routeName: 'index' 101 | }) 102 | }) 103 | }) 104 | } 105 | 106 | for (const mode of modes) { 107 | describe(`Page tracking with router base (${mode} mode)`, () => { 108 | let nuxt 109 | 110 | const override = { 111 | gtm: { 112 | layer: 'testDataLayer', 113 | pageTracking: true 114 | } 115 | } 116 | 117 | const nuxtConfig = loadConfig(__dirname, '../../example', override, { merge: true }) 118 | if (!nuxtConfig.router) { 119 | nuxtConfig.router = {} 120 | } 121 | nuxtConfig.router.base = '/base/' 122 | 123 | beforeAll(async () => { 124 | ({ nuxt } = (await setup(nuxtConfig))) 125 | }, 60000) 126 | 127 | afterAll(async () => { 128 | await nuxt.close() 129 | }) 130 | 131 | test('Event should contain page URL with base', async () => { 132 | const window = await nuxt.renderAndGetWindow(url('/base/')) 133 | 134 | await expectPageTrackingEvent(window.testDataLayer, { 135 | event: 'nuxtRoute', 136 | pageTitle: '@nuxtjs/gtm-module', 137 | pageType: 'PageView', 138 | pageUrl: '/base/', 139 | routeName: 'index' 140 | }) 141 | }) 142 | }) 143 | } 144 | -------------------------------------------------------------------------------- /lib/module.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const defaults = require('./defaults') 3 | const { requireNuxtVersion } = require('./compatibility') 4 | 5 | // doNotTrack polyfill 6 | // https://gist.github.com/pi0/a76fd97c4ea259c89f728a4a8ebca741 7 | const dnt = "(function(w,n,d,m,e,p){w[d]=(w[d]==1||n[d]=='yes'||n[d]==1||n[m]==1||(w[e]&&w[e][p]&&w[e][p]()))?1:0})(window,navigator,'doNotTrack','msDoNotTrack','external','msTrackingProtectionEnabled')" 8 | 9 | module.exports = async function gtmModule (_options) { 10 | requireNuxtVersion(this.nuxt, '2.12.0') 11 | 12 | const options = { 13 | ...defaults, 14 | ..._options, 15 | ...this.options.gtm 16 | } 17 | 18 | // By default enable only for non development 19 | if (options.enabled === undefined) { 20 | options.enabled = !this.options.dev 21 | } 22 | 23 | if (options.dev !== undefined) { 24 | // eslint-disable-next-line no-console 25 | console.warn('[gtm] `dev` option is deprecated! Please use `enabled`') 26 | if (options.dev === true && this.options.dev) { 27 | options.enabled = true 28 | } 29 | delete options.dev 30 | } 31 | 32 | this.addTemplate({ 33 | src: path.resolve(__dirname, 'plugin.utils.js'), 34 | fileName: 'gtm.utils.js', 35 | options 36 | }) 37 | 38 | if (!options.enabled) { 39 | // Register mock plugin 40 | this.addPlugin({ 41 | src: path.resolve(__dirname, 'plugin.mock.js'), 42 | fileName: 'gtm.js', 43 | options 44 | }) 45 | return 46 | } 47 | 48 | // Async id evaluation 49 | if (typeof (options.id) === 'function') { 50 | options.id = await options.id() 51 | } 52 | 53 | // Build query 54 | const query = { 55 | // Default is dataLayer for google 56 | l: options.layer !== 'dataLayer' ? options.layer : null, 57 | ...options.variables 58 | } 59 | const queryString = Object.keys(query) 60 | .filter(key => query[key] !== null && query[key] !== undefined) 61 | .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(query[key])}`) 62 | .join('&') 63 | 64 | // Compile scripts 65 | const injectScript = `var f=d.getElementsByTagName(s)[0],j=d.createElement(s);${options.crossOrigin ? 'j.crossOrigin=\'' + options.crossOrigin + '\';' : ''}j.${options.scriptDefer ? 'defer' : 'async'}=true;j.src='${options.scriptURL + '?id=\'+i' + (queryString ? (`+'&${queryString}` + '\'') : '')};f.parentNode.insertBefore(j,f)` // deps: d,s,i 66 | 67 | const doNotTrackScript = options.respectDoNotTrack ? 'if(w.doNotTrack||w[x][i])return;' : '' 68 | 69 | const initLayer = "w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'})" // deps: w,l 70 | let script = `w[x]={};w._gtm_inject=function(i){${doNotTrackScript}w[x][i]=1;${initLayer};${injectScript};}` 71 | 72 | if (options.autoInit && options.id) { 73 | script += `;w[y]('${options.id}')` 74 | } 75 | 76 | // Add doNotTrack polyfill and wrap to IIFE 77 | script = `${dnt};(function(w,d,s,l,x,y){${script}})(window,document,'script','${options.layer}','_gtm_ids','_gtm_inject')` 78 | 79 | // Guard against double IIFE executation in SPA mode (#3) 80 | script = `if(!window._gtm_init){window._gtm_init=1;${script}}` 81 | 82 | // Add google tag manager