diff --git a/assets/js/ga.js b/assets/js/ga.js deleted file mode 100644 index 6375501..0000000 --- a/assets/js/ga.js +++ /dev/null @@ -1,139 +0,0 @@ -import {analyticsCode} from '@params'; - -let dataLayer; - -function isDoNotTrackEnabled() { - if (typeof window === 'undefined') { - return false; - } - const {doNotTrack, navigator} = window; - - // Do Not Track Settings across browsers - const dnt = (doNotTrack || navigator.doNotTrack || navigator.msDoNotTrack); - - if (!dnt) { - return false; - } - - return dnt === true || - dnt === 1 || - dnt === 'yes' || - (typeof dnt === 'string' && dnt.charAt(0) === '1'); -} - -function gtag() { - dataLayer.push(arguments); -} - -/** - * Function that tracks a click on an outbound link in Analytics. - * - * This function takes a valid URL string as an argument, optional - * 'isBlank' argument, and uses that URL string as the event label. - * - * Setting the transport method to 'beacon' lets the hit be sent using - * navigator.sendBeacon() in browser that support it. The navigator.sendBeacon() - * method asynchronously sends an HTTP POST request containing a small amount of - * data to a web server. - */ -function trackOutboundLink(url, isBlank = false) { - gtag('event', 'click', { - 'event_label': url, - 'event_category': 'outbound', - 'transport_type': 'beacon', - 'event_callback': () => { - if (!isBlank) { - document.location = url; - } - } - }); -} - -function trackInternalEvent(label, category) { - gtag('event', 'click', { - 'event_label': label, - 'event_category': category - }); -} - -function onClickCallback(event) { - const element = event.target; - const className = element.getAttribute('class'); - - // Track 'Back to top' click - if (className === 'top-of-site-link') { - trackInternalEvent('Back to top', 'navigation'); - return; - } - - // Track menu show - if (className === 'sidebar-toggle') { - trackInternalEvent('Sidebar Toggle', 'navigation'); - return; - } - - // Track annotation usage - if (className === 'hypothesis-container') { - trackInternalEvent('Annotation open', 'navigation'); - return; - } - - // Track feeds click - if (className === 'menu-feeds-item') { - const feedType = element.dataset.feedType; - trackInternalEvent(`Get ${feedType}`, 'feed'); - return; - } - - // Track only external URLs. - if ((element.tagName !== 'A') || (element.host === window.location.host)) { - return; - } - - // Track outbound link click - trackOutboundLink( - event.target, - event.target.getAttribute('target') === '_blank' - ); -} - -if (isDoNotTrackEnabled()) { - // Skip analytics for users with Do Not Track enabled - console.info('[TRACKING]: Respecting DNT with respect to analytics...'); // eslint-disable-line no-console -} else { - // Known DNT values not set, so we will assume it's off. - if (analyticsCode) { - (function () { - // New Google Site Tag (gtag.js) tagging/analytics framework - // See: https://developers.google.com/gtagjs - const baseUrl = 'https://www.googletagmanager.com'; - const params = new URLSearchParams({ - id: analyticsCode - }); - - let script = document.createElement('script'); - script.src = baseUrl + '/gtag/js?' + params.toString(); - script.type = 'text/javascript'; - script.async = true; - - document.getElementsByTagName('head')[0].appendChild(script); - }()); - - dataLayer = window.dataLayer = window.dataLayer || []; - gtag('js', new Date()); - - const month = 30 * 24 * 60 * 60; // 30 days, in seconds - - // Setup the project analytics code and send a pageview - gtag('config', analyticsCode, { - 'cookie_expires': month - }); - - gtag('set', { - 'cookie_flags': 'SameSite=None;Secure' - }); - - // Outbound link tracking. - document.addEventListener('click', onClickCallback, false); - } -} diff --git a/assets/js/gtm.js b/assets/js/gtm.js new file mode 100644 index 0000000..4381ca9 --- /dev/null +++ b/assets/js/gtm.js @@ -0,0 +1,54 @@ +import {gtmCode, respectDoNotTrack} from '@params'; + +/** + * Check if the Do Not Track setting is enabled. + * + * @returns {boolean} + */ +function isDoNotTrackEnabled() { + if (typeof window === 'undefined') { + return false; + } + const {doNotTrack, navigator} = window; + + // Do Not Track Settings across browsers + const dnt = (doNotTrack || navigator.doNotTrack || navigator.msDoNotTrack); + + if (!dnt) { + return false; + } + + return dnt === true || + dnt === 1 || + dnt === 'yes' || + (typeof dnt === 'string' && dnt.charAt(0) === '1'); +} + +if (respectDoNotTrack && isDoNotTrackEnabled()) { + // Skip analytics for users with Do Not Track enabled + console.info('[TRACKING]: Respecting DNT with respect to analytics...'); // eslint-disable-line no-console +} else { + // Known DNT values not set, so we will assume it's off. + if (gtmCode) { + (function () { + const baseUrl = 'https://www.googletagmanager.com'; + const params = new URLSearchParams({ + id: gtmCode, + l: 'dataLayer' + }); + + const script = document.createElement('script'); + script.src = `${baseUrl}/gtm.js?${params.toString()}`; + script.type = 'text/javascript'; + script.async = true; + + document.getElementsByTagName('head')[0].appendChild(script); + + window.dataLayer = window.dataLayer || []; + window.dataLayer.push({ + 'gtm.start': new Date().getTime(), + event: 'gtm.js' + }); + }()); + } +} diff --git a/exampleSite/config/_default/config.yaml b/exampleSite/config/_default/config.yaml index 719eebb..dca03da 100644 --- a/exampleSite/config/_default/config.yaml +++ b/exampleSite/config/_default/config.yaml @@ -24,15 +24,6 @@ disableKinds: taxonomies: tag: tags -# Google Analytics Tracking ID. -# -# For more info, read the article -# https://support.google.com/analytics/answer/10089681 -# -# Set `HUGO_ENV` environment variable or `site.Params.env` configuration -# parameter to "production" to use Google Analytics. -googleAnalytics: '' - minify: # Do not minify XML files to avoid CDATA escape issues disableXML: true diff --git a/exampleSite/config/_default/params.yaml b/exampleSite/config/_default/params.yaml index 71eca84..0eda434 100644 --- a/exampleSite/config/_default/params.yaml +++ b/exampleSite/config/_default/params.yaml @@ -141,3 +141,12 @@ comments: emitMetadata: 0 inputPosition: bottom lang: en + +privacy: + googleTagManager: + disable: true + respectDoNotTrack: true + +services: + googleTagManager: + id: '' diff --git a/layouts/partials/head.html b/layouts/partials/head.html index 036b26d..5bf6e09 100644 --- a/layouts/partials/head.html +++ b/layouts/partials/head.html @@ -37,5 +37,5 @@ {{- /* Scripts */}} {{- partial "partials/scripts.html" . }} - {{- partial "partials/seo/ga.html" . }} + {{- partial "partials/head/analytics.html" . }} diff --git a/layouts/partials/head/analytics.html b/layouts/partials/head/analytics.html new file mode 100644 index 0000000..74d045d --- /dev/null +++ b/layouts/partials/head/analytics.html @@ -0,0 +1,22 @@ +{{- if eq hugo.Environment "production" }} + {{- if not site.Params.Privacy.GoogleTagManager.Disable }}{{/* First check GTM */}} + {{- with site.Params.Services.GoogleTagManager }} + {{- $gtmParams := dict "gtmCode" (.ID) -}} + {{- $gtmParams = merge $gtmParams (dict "respectDoNotTrack" site.Params.Privacy.GoogleTagManager.RespectDoNotTrack | default true) }} + + {{- $gtmScript := slice -}} + {{- $gtmScript = $gtmScript | append (resources.Get "js/gtm.js") -}} + {{- $gtmScript = $gtmScript | resources.Concat "js/analytics.js" -}} + {{- $gtmScript = $gtmScript | js.Build (dict "format" "iife" "target" "es2015" "minify" true "params" $gtmParams) -}} + + {{- if site.Params.assets.disable_fingerprinting }} + + {{- else -}} + {{- $gtmScript = $gtmScript | fingerprint -}} + + {{- end -}} + {{- end -}} + {{ else }}{{/* If GTM is disabled delegate the rest to Hugo */}} + {{ template "_internal/google_analytics.html" . }} + {{ end }} +{{ end }} diff --git a/layouts/partials/seo/ga.html b/layouts/partials/seo/ga.html deleted file mode 100644 index 40758d8..0000000 --- a/layouts/partials/seo/ga.html +++ /dev/null @@ -1,18 +0,0 @@ -{{- if (or (eq (getenv "HUGO_ENV") "production") (eq site.Params.env "production")) -}} - {{- with site.GoogleAnalytics -}} - {{- $gaParams := dict "analyticsCode" (site.GoogleAnalytics | default "") -}} - - {{- $gaScript := slice -}} - {{- $gaScript = $gaScript | append (resources.Get "js/ga.js") -}} - {{- $gaScript = $gaScript | resources.Concat "js/analytics-bundle.js" -}} - - {{- $gaScript = $gaScript | js.Build (dict "format" "iife" "target" "es2015" "minify" true "params" $gaParams) -}} - - {{- if site.Params.assets.disable_fingerprinting }} - - {{- else -}} - {{- $gaScript = $gaScript | fingerprint }} - - {{- end -}} - {{- end -}} -{{- end -}} diff --git a/netlify-production.js b/netlify-production.js new file mode 100644 index 0000000..18ae1d2 --- /dev/null +++ b/netlify-production.js @@ -0,0 +1,15 @@ +const fs = require('fs'); +const yaml = require('yaml'); + +const filePath = 'exampleSite/config/_default/params.yaml'; +const file = fs.readFileSync(filePath, 'utf8'); +const config = yaml.parse(file); + +config.privacy.googleTagManager.disable = false; +config.services.googleTagManager.id = 'GTM-W8D5W642'; + +const newYaml = yaml.stringify(config); + +fs.writeFileSync(filePath, newYaml, 'utf8'); + +console.log('Updated config.yaml successfully!'); diff --git a/netlify.toml b/netlify.toml index b7de027..bd57110 100644 --- a/netlify.toml +++ b/netlify.toml @@ -7,6 +7,7 @@ # a base has not been set. This sample publishes the directory # located at the absolute path "root/project/build-output" publish = 'public' + command = 'npm install' [build.environment] HUGO_VERSION = '0.121.0' @@ -15,22 +16,24 @@ # Production context: all deploys from the Production branch # set in your site’s Branches settings in the UI will inherit # these settings. You can define environment variables -# here but we recommend using the Netlify UI for sensitive +# here, but we recommend using the Netlify UI for sensitive # values to keep them out of your source repository. [context.production] command = 'hugo --source=exampleSite --baseURL ${URL} --destination ../public --minify' [context.production.environment] HUGO_ENV = 'production' - HUGO_GOOGLEANALYTICS = 'G-DP9Q137C3X' + HUGO_ENABLEGITINFO = 'true' # Deploy Preview context: all deploys generated from # a pull/merge request will inherit these settings. [context.deploy-preview] - command = 'npm run netlify-preview; hugo --source=exampleSite --buildDrafts --buildFuture --baseURL ${DEPLOY_PRIME_URL} --destination ../public --minify' + # command = 'npm run netlify-preview; hugo --source=exampleSite --buildDrafts --buildFuture --baseURL ${DEPLOY_PRIME_URL} --destination ../public --minify' + command = 'npm run netlify-production; npm run netlify-preview; hugo --source=exampleSite --buildDrafts --buildFuture --baseURL ${DEPLOY_PRIME_URL} --destination ../public; cat ./public/index.html' [context.deploy-preview.environment] - HUGO_ENV = 'development' + # HUGO_ENV = 'development' + HUGO_ENV = 'production' # Branch Deploy context: all deploys that are not from # a pull/merge request or from the Production branch diff --git a/package-lock.json b/package-lock.json index 9a46a94..df6cf5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,9 @@ "@playwright/test": "^1.47.2", "@types/node": "^22.7.3", "eslint": "^9.11.1", - "jsdom": "^25.0.1" + "fs": "^0.0.1-security", + "jsdom": "^25.0.1", + "yaml": "^2.5.1" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -774,6 +776,13 @@ "node": ">= 6" } }, + "node_modules/fs": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", + "integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -1617,6 +1626,19 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, + "node_modules/yaml": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", + "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index d7785a4..34dd342 100644 --- a/package.json +++ b/package.json @@ -23,12 +23,15 @@ "server": "HUGO_RESOURCEDIR='../resources' HUGO_ENV=development hugo server --logLevel info --source=exampleSite --buildDrafts --buildFuture --ignoreCache --disableFastRender", "test": "playwright test", "lint": "eslint static/js/* assets/js/* tests/*.spec.js eslint.config.js playwright.config.js netlify-preview.js", + "netlify-production": "node netlify-production.js", "netlify-preview": "node netlify-preview.js" }, "devDependencies": { "@playwright/test": "^1.47.2", "@types/node": "^22.7.3", "eslint": "^9.11.1", - "jsdom": "^25.0.1" + "fs": "^0.0.1-security", + "jsdom": "^25.0.1", + "yaml": "^2.5.1" } }