Skip to content

Commit cbbe654

Browse files
Merge pull request #10516 from mendix/olu-usercentrics-fy25update
Adding the new Usercentrics package for marketing & legal compliance
2 parents 63cca8e + 91f3d41 commit cbbe654

6 files changed

Lines changed: 278 additions & 12 deletions

File tree

config/_default/hugo.toml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,6 @@ disableKinds = ["taxonomy", "term"]
3030
quality = 75
3131
anchor = "smart"
3232

33-
[services]
34-
[services.googleAnalytics]
35-
# Marketing Google Tag ID
36-
id = 'UA-163813-1'
37-
3833
# Language configuration
3934

4035
[languages]
@@ -161,7 +156,7 @@ replacements = "github.com/FortAwesome/Font-Awesome -> ., github.com/twbs/bootst
161156
sidebar_menu_truncate = 100
162157

163158
# Adds a H2 section titled "Feedback" to the bottom of each doc. The responses are sent to Google Analytics as events.
164-
# This feature depends on [services.googleAnalytics] and will be disabled if "services.googleAnalytics.id" is not set.
159+
# This feature used to depend on [services.googleAnalytics],but that is now become obsolete.
165160
# If you want this feature, but occasionally need to remove the "Feedback" section from a single page,
166161
# add "hide_feedback: true" to the page's front matter.
167162
[params.ui.feedback]

layouts/_default/content.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ <h1>{{ .Title }}</h1>
99
{{ end }}
1010
</header>
1111
{{ .Content }}
12-
{{ if (and (not .Params.hide_feedback) (.Site.Params.ui.feedback.enable) .Site.Config.Services.GoogleAnalytics.ID) }}
12+
<!-- Olu: removed checking for .Site.Config.Services.GoogleAnalytics.ID as part of the conditions -->
13+
{{ if (and (not .Params.hide_feedback) (.Site.Params.ui.feedback.enable)) }}
1314
{{ partial "feedback.html" .Site.Params.ui.feedback }}
1415
<br />
1516
{{ end }}

layouts/partials/hooks/body-end.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<!-- Include mx-dock on all pages-->
22
<div>{{ partial "mx-dock.html" . }}</div>
3+
34
<!-- Add analytics as defined by Marketing -->
45
{{- if eq hugo.Environment "production" -}}<noscript><iframe src="//www.googletagmanager.com/ns.html?id=GTM-QJG4"
56
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
@@ -8,6 +9,7 @@
89
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
910
'//www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
1011
})(window,document,'script','dataLayer','GTM-QJG4');</script>{{- end -}}
12+
1113
<!-- To make images clickable and zoomable as pop-ups -->
1214

1315
<script src='{{ "js/medium-zoom.js" | relURL }}'></script>
@@ -27,3 +29,8 @@
2729
}
2830
});
2931
</script>
32+
33+
<!-- Olu: Added analytics as defined by Marketing/Legal. Read more here: https://github.com/mendix-web/mendix-analytics-package. You need access from the Mendix Web team -->
34+
<script src='{{ "js/analytics-packages/datalayer-tracking.js" | relURL }}'></script>
35+
<script src='{{ "js/analytics-packages/usercentrics-consent.js" | relURL }}'></script>
36+

layouts/partials/hooks/head-end.html

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
<!-- Olu: Added analytics as defined by Marketing/Legal. Read more here: https://github.com/mendix-web/mendix-analytics-package. You need access from the Mendix Web team -->
2+
{{- if eq hugo.Environment "production" -}}
3+
<script src="https://assets.adobedtm.com/5dfc7d97c6fb/f76097206fad/launch-a8be6e5af390.min.js" async></script> {{- end -}}
4+
5+
{{- if eq hugo.Environment "development" -}}
6+
<script src="https://assets.adobedtm.com/5dfc7d97c6fb/f76097206fad/launch-ef5f6245245b-staging.min.js" async></script> {{- end -}}
7+
18
<!-- Not using Docsy Algolia - nothing needs to go here-->
29
{{- with .Site.Params.search.algolia -}}
310
<!-- stylesheet for algolia docsearch -->
@@ -8,11 +15,7 @@
815
{{- else -}}
916
<link rel="canonical" href="{{ .Permalink }}">
1017
{{- end -}}
11-
<!-- MvM: Add Javascript for Cookie management - requested by Marketing/Legal -->
12-
<script id="usercentrics-cmp" src="https://app.usercentrics.eu/browser-ui/latest/loader.js" data-settings-id="Fhgux2LDfg_YUL" async></script>
13-
<script type="application/javascript" src="https://privacy-proxy.usercentrics.eu/latest/uc-block.bundle.js"
14-
>
15-
</script>
18+
1619
<!-- MvM: Add Javascript for Japanese translation -->
1720
<!-- MvM: Only if page has a title -->
1821
{{- with .Params.title -}}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* DataLayer Management and Analytics Tracking
3+
*
4+
* Centralizes dataLayer initialization and provides consistent API
5+
* for all analytics tracking across the site.
6+
*
7+
* @version 2.0.0 - Production optimized
8+
*/
9+
10+
// Initialize dataLayer as early as possible
11+
window.dataLayer = window.dataLayer || [];
12+
13+
/**
14+
* Unified dataLayer push function
15+
* @param {Object} data - Data to push to dataLayer
16+
* @param {string} source - Source identifier (optional, for internal tracking)
17+
*/
18+
window.pushToDataLayer = function (data, source) {
19+
if (window.dataLayer) {
20+
window.dataLayer.push(data);
21+
}
22+
};
23+
24+
/**
25+
* Legacy pusher function for backwards compatibility
26+
* Provides consistent interface for pushing consent and tracking data
27+
* @param {Object} payload - Data payload to push to dataLayer
28+
*/
29+
window.pusher = function (payload) {
30+
if (typeof window.pushToDataLayer === 'function') {
31+
window.pushToDataLayer(payload, 'legacy-pusher');
32+
} else {
33+
window.dataLayer.push(payload);
34+
}
35+
};
36+
37+
/**
38+
* Track various clicks and page events in Google Analytics
39+
*/
40+
document.addEventListener('DOMContentLoaded', function () {
41+
// Track PDF downloads - ensure PDFs aren't crawled by search engines
42+
const pdfLinks = document.querySelectorAll('a[href*="pdf"]');
43+
pdfLinks.forEach(function (link) {
44+
link.setAttribute('rel', 'nofollow');
45+
link.addEventListener('click', function () {
46+
pushToDataLayer(
47+
{
48+
event: 'PDFClick',
49+
clickedHREF: this.getAttribute('href'),
50+
},
51+
'pdf-tracking'
52+
);
53+
});
54+
});
55+
56+
// Track signin clicks
57+
const signinLinks = document.querySelectorAll("a[href*='home.mendix.com']");
58+
signinLinks.forEach(function (link) {
59+
link.addEventListener('click', function () {
60+
pushToDataLayer(
61+
{
62+
event: 'SigninClick',
63+
},
64+
'signin-tracking'
65+
);
66+
});
67+
});
68+
69+
// Send virtual page view when document is ready
70+
pushToDataLayer(
71+
{
72+
event: 'virtual_page_view',
73+
},
74+
'page-view'
75+
);
76+
});
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/**
2+
* Usercentrics Cookie Consent Management
3+
*
4+
* Handles Cookie Monster consent events and Google Consent Mode v2 integration.
5+
*
6+
* Note: Usercentrics and Cookie Monster are loaded by Adobe Launch.
7+
* This script only sets up event listeners to respond to consent changes.
8+
*
9+
* Adobe Launch loads:
10+
* - Usercentrics banner (settings ID: xUbnOnjLm)
11+
* - Cookie Monster script
12+
* - Environment-specific configurations
13+
*
14+
* @version 2.0.0 - Production optimized
15+
*/
16+
17+
window.UserCentrics = {
18+
/**
19+
* Initialize Cookie Monster listeners
20+
*/
21+
init() {
22+
// Ensure dataLayer exists
23+
window.dataLayer = window.dataLayer || [];
24+
25+
// Create gtag function for Google Consent Mode v2
26+
this.createGtagFunction();
27+
28+
// Wait for Cookie Monster to be available (loaded by Adobe Launch)
29+
this.waitForCookieMonster();
30+
},
31+
32+
/**
33+
* Wait for Cookie Monster to load (from Adobe Launch)
34+
*/
35+
waitForCookieMonster() {
36+
const self = this;
37+
const maxAttempts = 50; // 5 seconds max
38+
let attempts = 0;
39+
40+
const checkCookieMonster = setInterval(() => {
41+
attempts++;
42+
43+
if (window.cookieMonster) {
44+
clearInterval(checkCookieMonster);
45+
self.setupCookieListeners();
46+
self.restoreSavedConsent();
47+
self.dispatchReadyEvent();
48+
} else if (attempts >= maxAttempts) {
49+
clearInterval(checkCookieMonster);
50+
}
51+
}, 100);
52+
},
53+
54+
/**
55+
* Create gtag function for Google Consent Mode v2 communication
56+
*/
57+
createGtagFunction() {
58+
window.dataLayer = window.dataLayer || [];
59+
if (typeof window.gtag !== 'function') {
60+
window.gtag = function () {
61+
window.dataLayer.push(arguments);
62+
};
63+
}
64+
},
65+
66+
/**
67+
* Set up Usercentrics consent change listeners
68+
* Listens to UC_UI_CMP_EVENT for all consent changes (accept/reject/save)
69+
*/
70+
setupCookieListeners() {
71+
const self = this;
72+
73+
// Listen for all Usercentrics consent events
74+
window.addEventListener('UC_UI_CMP_EVENT', (event) => {
75+
if (event.detail) {
76+
const { type } = event.detail;
77+
78+
// Handle all consent-changing events
79+
if (
80+
type === 'ACCEPT_ALL' ||
81+
type === 'DENY_ALL' ||
82+
type === 'SAVE'
83+
) {
84+
self.updateAllConsentStates();
85+
}
86+
}
87+
});
88+
},
89+
90+
/**
91+
* Restore saved consent preferences on page load
92+
* Uses polling to wait for Cookie Monster to load consent data
93+
*/
94+
restoreSavedConsent() {
95+
const self = this;
96+
97+
// Check if user has previously saved consent
98+
if (window.cookieMonster.hasInteracted()) {
99+
// Poll until Cookie Monster has loaded the consent data
100+
let attempts = 0;
101+
const maxAttempts = 10; // 1 second max
102+
103+
const checkReady = setInterval(() => {
104+
attempts++;
105+
106+
// Test if data is loaded by checking if required cookies return true
107+
const dataLoaded =
108+
window.cookieMonster.permitted('reqd') === true;
109+
110+
if (dataLoaded || attempts >= maxAttempts) {
111+
clearInterval(checkReady);
112+
self.updateAllConsentStates();
113+
}
114+
}, 100);
115+
}
116+
},
117+
118+
/**
119+
* Update all consent states in GTM based on current Cookie Monster permissions
120+
*/
121+
updateAllConsentStates() {
122+
// Check Targeting cookies
123+
const targPermitted = window.cookieMonster.permitted('targ');
124+
gtag('consent', 'update', {
125+
ad_storage: targPermitted ? 'granted' : 'denied',
126+
ad_user_data: targPermitted ? 'granted' : 'denied',
127+
ad_personalization: targPermitted ? 'granted' : 'denied',
128+
personalization_storage: targPermitted ? 'granted' : 'denied',
129+
});
130+
131+
// Check Performance cookies
132+
const perfPermitted = window.cookieMonster.permitted('perf');
133+
gtag('consent', 'update', {
134+
analytics_storage: perfPermitted ? 'granted' : 'denied',
135+
});
136+
137+
// Check Functional cookies
138+
const fnctPermitted = window.cookieMonster.permitted('fnct');
139+
gtag('consent', 'update', {
140+
functionality_storage: fnctPermitted ? 'granted' : 'denied',
141+
});
142+
143+
// Push custom dataLayer event for GTM triggers
144+
// This allows GTM to fire tags conditionally based on consent state
145+
window.dataLayer.push({
146+
event: 'consent_update',
147+
consent_state: {
148+
ad_storage: targPermitted ? 'granted' : 'denied',
149+
ad_user_data: targPermitted ? 'granted' : 'denied',
150+
ad_personalization: targPermitted ? 'granted' : 'denied',
151+
personalization_storage: targPermitted ? 'granted' : 'denied',
152+
analytics_storage: perfPermitted ? 'granted' : 'denied',
153+
functionality_storage: fnctPermitted ? 'granted' : 'denied',
154+
},
155+
});
156+
},
157+
158+
/**
159+
* Dispatch custom event when Cookie Monster is ready
160+
*/
161+
dispatchReadyEvent() {
162+
window.cookieMonsterReady = true;
163+
164+
if (typeof CustomEvent !== 'undefined') {
165+
const event = new CustomEvent('cookieMonsterReady', {
166+
detail: {
167+
version: window.cookieMonster.VERSION || 'unknown',
168+
engine: window.cookieMonster.ENGINE || 'unknown',
169+
},
170+
});
171+
document.dispatchEvent(event);
172+
}
173+
},
174+
};
175+
176+
/**
177+
* Auto-initialize when DOM is ready
178+
*/
179+
if (document.readyState === 'loading') {
180+
document.addEventListener('DOMContentLoaded', () => UserCentrics.init());
181+
} else {
182+
// DOM already loaded
183+
UserCentrics.init();
184+
}

0 commit comments

Comments
 (0)