@eox/pages-theme-eox 0.5.5 → 0.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eox/pages-theme-eox",
3
- "version": "0.5.5",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "Vitepress Theme with EOX branding",
6
6
  "main": "src/index.js",
package/src/Layout.vue CHANGED
@@ -107,6 +107,7 @@
107
107
  :href="withBase(item.link)"
108
108
  :target="item.target"
109
109
  :rel="item.rel"
110
+ @click="trackEvent(['CTA', 'Click', 'Nav', item.text])"
110
111
  >
111
112
  <span>{{ item.text }}</span>
112
113
  <i
@@ -172,6 +173,14 @@
172
173
  : 'border no-elevate white-text'
173
174
  "
174
175
  :style="i === 0 ? 'margin-left: 0 !important' : ''"
176
+ @click="
177
+ trackEvent([
178
+ 'CTA',
179
+ 'Click',
180
+ `Hero ${action.theme === 'brand' ? 'primary' : action.theme === 'secondary' ? 'secondary' : 'alt'}`,
181
+ action.text,
182
+ ])
183
+ "
175
184
  >
176
185
  <span>{{ action.text }}</span>
177
186
  <i v-if="action.theme !== 'alt'" class="mdi mdi-arrow-right"></i>
@@ -185,6 +194,12 @@
185
194
  />
186
195
  </header>
187
196
  </template>
197
+ <template #not-found>
198
+ <ClientOnly v-if="router.route.path === '/cookie-settings'">
199
+ <CookieSettings></CookieSettings>
200
+ </ClientOnly>
201
+ <NotFound v-else></NotFound>
202
+ </template>
188
203
  <template #page-bottom>
189
204
  <FeaturesGallery
190
205
  v-if="page.relativePath.startsWith('features/')"
@@ -207,9 +222,21 @@
207
222
  :href="theme.nav.find((i) => i.link.includes('contact')).link"
208
223
  class="button small border no-margin"
209
224
  style="color: var(--on-surface)"
225
+ @click="trackEvent(['CTA', 'Click', 'Footer', action.text])"
210
226
  >{{ theme.nav.find((i) => i.link.includes("contact")).text }}</a
211
227
  >
212
228
  <p v-html="theme.footer.copyright"></p>
229
+ <p class="middle-align">
230
+ Powered by
231
+ <a
232
+ href="https://hub.eox.at"
233
+ target="_blank"
234
+ class="left-margin small-margin"
235
+ ><img
236
+ src="https://hub.eox.at/hub/custom/logos/eoxhub.svg"
237
+ style="height: 25px"
238
+ /></a>
239
+ </p>
213
240
  </div>
214
241
  <div class="s12 l6">
215
242
  <div class="grid large-line">
@@ -229,15 +256,32 @@
229
256
  <p class="bold">Legal</p>
230
257
  <p>
231
258
  <a
232
- href="https://eox.at/impressum"
259
+ :href="
260
+ theme.theme.brandConfig?.legal?.about ||
261
+ 'https://eox.at/impressum'
262
+ "
263
+ target="_blank"
264
+ class="link"
265
+ >About</a
266
+ >
267
+ </p>
268
+ <p>
269
+ <a
270
+ :href="
271
+ theme.theme.brandConfig?.legal?.termsAndConditions ||
272
+ 'https://eox.at/impressum'
273
+ "
233
274
  target="_blank"
234
275
  class="link"
235
- >About & Terms</a
276
+ >Terms & Conditions</a
236
277
  >
237
278
  </p>
238
279
  <p>
239
280
  <a
240
- href="https://eox.at/privacy-notice"
281
+ :href="
282
+ theme.theme.brandConfig?.legal?.privacyPolicy ||
283
+ 'https://eox.at/privacy-notice'
284
+ "
241
285
  target="_blank"
242
286
  class="link"
243
287
  >Privacy</a
@@ -250,6 +294,7 @@
250
294
  <div class="large-space"></div>
251
295
  </div>
252
296
  </footer>
297
+ <CookieBanner v-if="theme.theme.brandConfig?.analytics"></CookieBanner>
253
298
  <slot name="layout-bottom"></slot>
254
299
  </template>
255
300
  </Layout>
@@ -257,9 +302,11 @@
257
302
 
258
303
  <script setup>
259
304
  import DefaultTheme from "vitepress/theme";
305
+ import { useData, useRouter, withBase } from "vitepress";
306
+ import { trackEvent } from "./helpers";
260
307
  const { Layout } = DefaultTheme;
261
- import { useData, withBase } from "vitepress";
262
308
  const { frontmatter, page, site, theme } = useData();
309
+ const router = useRouter();
263
310
 
264
311
  if (!import.meta.env.SSR) {
265
312
  const scrollListener = () => {
@@ -348,7 +395,7 @@ header {
348
395
  margin-top: calc(var(--vp-nav-height) * -1);
349
396
  padding-top: 10rem !important;
350
397
  padding-bottom: 10rem !important;
351
- height: 100svh;
398
+ height: calc(100svh + 2px);
352
399
  max-height: 1000px;
353
400
  }
354
401
  @media (max-width: 1024px) {
@@ -368,6 +415,8 @@ header > img.background-image {
368
415
  object-fit: cover;
369
416
  opacity: 0.4;
370
417
  z-index: 0;
418
+ border-top-left-radius: 0 !important;
419
+ border-top-right-radius: 0 !important;
371
420
  }
372
421
  header > .hero-container {
373
422
  max-width: 1200px;
@@ -19,12 +19,18 @@
19
19
  class="button extra small-margin"
20
20
  :class="dark !== undefined ? 'surface' : 'primary'"
21
21
  :href="primaryLink"
22
+ @click="
23
+ trackEvent(['CTA', 'Click', 'CTA Section Primary', primaryButton])
24
+ "
22
25
  >{{ primaryButton }} <i class="mdi mdi-arrow-right"></i
23
26
  ></a>
24
27
  <a
25
28
  v-if="secondaryButton"
26
29
  class="button extra small-margin secondary"
27
30
  :href="secondaryLink"
31
+ @click="
32
+ trackEvent(['CTA', 'Click', 'CTA Section Secondary', secondaryButton])
33
+ "
28
34
  >{{ secondaryButton }}<i class="mdi mdi-arrow-right"></i
29
35
  ></a>
30
36
  <a
@@ -32,6 +38,7 @@
32
38
  class="button border extra small-margin"
33
39
  :class="dark !== undefined ? 'white-text' : ''"
34
40
  :href="altLink"
41
+ @click="trackEvent(['CTA', 'Click', 'CTA Section Alt', altButton])"
35
42
  >{{ altButton }}</a
36
43
  >
37
44
  </div>
@@ -42,6 +49,7 @@
42
49
  </template>
43
50
 
44
51
  <script setup>
52
+ import { trackEvent } from "../helpers";
45
53
  const props = defineProps([
46
54
  "dark",
47
55
  "primaryButton",
@@ -0,0 +1,66 @@
1
+ <template>
2
+ <div
3
+ class="cookie-banner card surface medium-margin medium-padding medium-elevate no-round"
4
+ >
5
+ <p class="small-text">
6
+ We use optional cookies to improve your experience and for marketing. Read
7
+ our
8
+ <a
9
+ :href="theme.theme.brandConfig?.legal?.privacyPolicy"
10
+ target="_blank"
11
+ class="link"
12
+ >
13
+ privacy policy
14
+ </a>
15
+ or <a class="link" href="/cookie-settings">manage cookies</a>.
16
+ </p>
17
+ <nav>
18
+ <div class="max"></div>
19
+ <button class="small" @click="accept">Accept all</button>
20
+ <button class="small" @click="decline">Reject all</button>
21
+ </nav>
22
+ </div>
23
+ </template>
24
+
25
+ <script setup>
26
+ import { onMounted } from "vue";
27
+ import { useData, useRouter } from "vitepress";
28
+ import {
29
+ acceptCookies,
30
+ declineCookies,
31
+ enableTracking,
32
+ showBanner,
33
+ } from "../helpers";
34
+
35
+ const { theme } = useData();
36
+ const router = useRouter();
37
+
38
+ const accept = () => acceptCookies(router);
39
+ const decline = () => declineCookies(router);
40
+
41
+ onMounted(() => {
42
+ setTimeout(() => {
43
+ // Basic tracking setup
44
+ if (
45
+ !document.cookie.includes("mtm_cookie_consent") &&
46
+ !document.cookie.includes("mtm_consent_removed")
47
+ ) {
48
+ showBanner(true);
49
+ }
50
+ if (document.cookie.includes("mtm_cookie_consent")) {
51
+ enableTracking(true, router);
52
+ }
53
+ });
54
+ });
55
+ </script>
56
+
57
+ <style>
58
+ .cookie-banner {
59
+ display: none;
60
+ position: fixed;
61
+ bottom: 0;
62
+ right: 0;
63
+ max-width: 512px;
64
+ z-index: 9999;
65
+ }
66
+ </style>
@@ -0,0 +1,180 @@
1
+ <template>
2
+ <div class="VPPage">
3
+ <h1>Cookie Settings</h1>
4
+ <p>
5
+ We use cookies and similar technologies to improve your experience and for
6
+ marketing purposes. Review and manage your cookie settings below to
7
+ control your privacy. For more information on how we use cookies, please
8
+ see our
9
+ <a
10
+ class="link"
11
+ :href="
12
+ theme.theme.brandConfig?.legal?.privacyPolicy ||
13
+ 'https://eox.at/privacy-notice/'
14
+ "
15
+ target="_blank"
16
+ >Privacy Policy</a
17
+ >.
18
+ </p>
19
+ <div
20
+ v-for="category in Object.keys(cookies)"
21
+ class="vertical-margin large-margin vertical-padding large-padding"
22
+ :id="category"
23
+ >
24
+ <nav>
25
+ <div class="max">
26
+ <h6 class="bold">{{ category }}</h6>
27
+ <p>
28
+ {{ cookies[category].description }}
29
+ </p>
30
+ </div>
31
+ <label class="switch">
32
+ <input
33
+ type="checkbox"
34
+ :checked="cookies[category].required"
35
+ :disabled="cookies[category].required"
36
+ />
37
+ <span></span>
38
+ </label>
39
+ </nav>
40
+ <details>
41
+ <summary class="middle-align">
42
+ <p class="primary-text bold">
43
+ <span class="view">View</span><span class="hide">Hide</span> cookies
44
+ </p>
45
+ <i class="small mdi mdi-chevron-down"></i>
46
+ </summary>
47
+ <table>
48
+ <thead>
49
+ <tr>
50
+ <th class="min">Name</th>
51
+ <th>Domain</th>
52
+ <th>Type</th>
53
+ <th>Duration</th>
54
+ </tr>
55
+ </thead>
56
+ <tbody>
57
+ <tr v-for="cookie in cookies[category].cookies">
58
+ <td class="min">
59
+ <code>{{ cookie.Name }}</code>
60
+ </td>
61
+ <td>{{ cookie.Domain }}</td>
62
+ <td>{{ cookie.Type }}</td>
63
+ <td>{{ cookie.Duration }}</td>
64
+ </tr>
65
+ </tbody>
66
+ </table>
67
+ </details>
68
+ </div>
69
+ </div>
70
+ <div class="large-space"></div>
71
+ </template>
72
+
73
+ <style>
74
+ .page {
75
+ margin: auto;
76
+ width: 100%;
77
+ max-width: 1200px;
78
+ padding: 48px 24px 0;
79
+ }
80
+ details summary i {
81
+ transition: transform 0.3s ease-in-out;
82
+ }
83
+ details[open] summary i {
84
+ transform: rotate(180deg);
85
+ }
86
+ details summary p {
87
+ display: flex;
88
+ gap: 0.25rem;
89
+ }
90
+ details summary p span {
91
+ display: none;
92
+ }
93
+ details:not([open]) summary .view {
94
+ display: block;
95
+ }
96
+ details[open] summary .hide {
97
+ display: block;
98
+ }
99
+ </style>
100
+
101
+ <script setup>
102
+ import { onMounted } from "vue";
103
+ import { useData, useRouter } from "vitepress";
104
+ import { enableTracking, showBanner } from "../helpers.js";
105
+ const { theme } = useData();
106
+ const router = useRouter();
107
+
108
+ router.route.data.title = "Cookie Settings";
109
+ router.onBeforeRouteChange = (to) => {
110
+ if (to === "/cookie-settings") {
111
+ showBanner(false);
112
+ } else {
113
+ showBanner(
114
+ !document.cookie.includes("mtm_cookie_consent") &&
115
+ !document.cookie.includes("mtm_consent_removed"),
116
+ );
117
+ }
118
+ };
119
+
120
+ const cookies = {
121
+ Essential: {
122
+ description:
123
+ "Cookies that are strictly necessary for basic website or app functionality.",
124
+ required: true,
125
+ cookies: [
126
+ {
127
+ Name: "mtm_consent_removed",
128
+ Domain: `.${window.location.host}`,
129
+ Type: "Opt-out management",
130
+ Duration: "13 months",
131
+ },
132
+ {
133
+ Name: "mtm_consent",
134
+ Domain: `.${window.location.host}`,
135
+ Type: "Consent management",
136
+ Duration: "13 months",
137
+ },
138
+ ],
139
+ },
140
+ Analytics: {
141
+ description:
142
+ "Cookies that are required for analyzing website or app usage.",
143
+ cookies: [
144
+ {
145
+ Name: "_pk_id",
146
+ Domain: `.${window.location.host}`,
147
+ Type: "First-party website analytics",
148
+ Duration: "13 months",
149
+ },
150
+ {
151
+ Name: "_pk_ses",
152
+ Domain: `.${window.location.host}`,
153
+ Type: "First-party website analytics",
154
+ Duration: "30 minutes",
155
+ },
156
+ ],
157
+ },
158
+ };
159
+
160
+ onMounted(() => {
161
+ setTimeout(() => {
162
+ showBanner(false);
163
+
164
+ const analyticsOpt = document.querySelector("#Analytics input");
165
+ analyticsOpt.checked = document.cookie.includes("mtm_cookie_consent");
166
+
167
+ analyticsOpt.addEventListener("input", () => {
168
+ if (analyticsOpt.checked) {
169
+ _paq.push(["forgetUserOptOut"]);
170
+ _paq.push(["setCookieConsentGiven"]);
171
+ _paq.push(["rememberCookieConsentGiven"]);
172
+ enableTracking(true, router);
173
+ } else {
174
+ _paq.push(["optUserOut"]);
175
+ enableTracking(false, router);
176
+ }
177
+ });
178
+ });
179
+ });
180
+ </script>
@@ -0,0 +1,14 @@
1
+ <template>
2
+ <div class="VPPage">
3
+ <h1>404</h1>
4
+ <p>The page you requested was not found.</p>
5
+ <div class="small-space"></div>
6
+ <nav>
7
+ <a class="button" href="/">
8
+ <i class="mdi mdi-arrow-left"></i>
9
+ <span>Back to home</span>
10
+ </a>
11
+ </nav>
12
+ <div class="large-space"></div>
13
+ </div>
14
+ </template>
package/src/helpers.js ADDED
@@ -0,0 +1,122 @@
1
+ import { watch } from "vue";
2
+
3
+ /**
4
+ * Track Matomo event with array of four primary components
5
+ *
6
+ * - Category (Required) – This describes the type of events you want to track. For example, Link Clicks, Videos, Outbound Links, and Form Events.
7
+ * - Action (Required) – This is the specific action that is taken. For example, with the Video category, you might have a Play, Pause and Complete action.
8
+ * - Name (Optional – Recommended) – This is usually the title of the element that is being interacted with, to aid with analysis. For example, it could be the name of a Video that was played or the specific form that is being submitted.
9
+ * - Value (Optional) – This is a numeric value and is often added dynamically. It could be the cost of a product that is added to a cart, or the completion percentage of a video.
10
+ *
11
+ * See https://matomo.org/faq/reports/the-anatomy-of-an-event/
12
+ *
13
+ * @param {Array<string>} eventDetails
14
+ */
15
+ export const trackEvent = (eventDetails) => {
16
+ if (!import.meta.env.SSR) {
17
+ _paq.push(["trackEvent", ...eventDetails]);
18
+ }
19
+ };
20
+
21
+ /**
22
+ * Accept cookies
23
+ */
24
+ export const acceptCookies = (router) => {
25
+ _paq.push(["rememberCookieConsentGiven"]);
26
+ showBanner(false);
27
+ enableTracking(true, router);
28
+ };
29
+
30
+ /**
31
+ * Decline cookies
32
+ */
33
+ export const declineCookies = (router) => {
34
+ _paq.push(["forgetCookieConsentGiven"]);
35
+ _paq.push(["optUserOut"]);
36
+ showBanner(false);
37
+ enableTracking(false, router);
38
+ };
39
+
40
+ /**
41
+ * Show/hide banner
42
+ *
43
+ * @param show Boolean
44
+ */
45
+ export const showBanner = (show) => {
46
+ document.querySelector(".cookie-banner").style.display = show
47
+ ? "block"
48
+ : "none";
49
+ };
50
+
51
+ /**
52
+ * Enables tracking listeners
53
+ *
54
+ * @param {Boolean} enabled
55
+ * @param {router} router
56
+ */
57
+ export const enableTracking = (enabled, router) => {
58
+ trackPageScrolling(enabled, router);
59
+ trackRouterNavigation(router);
60
+ };
61
+
62
+ /**
63
+ * Track page scrolling depth
64
+ *
65
+ * @param enabled Boolean
66
+ */
67
+ export const trackPageScrolling = (enabled, router) => {
68
+ function getScrollPercent() {
69
+ const h = document.documentElement,
70
+ b = document.body,
71
+ st = "scrollTop",
72
+ sh = "scrollHeight";
73
+ return ((h[st] || b[st]) / ((h[sh] || b[sh]) - h.clientHeight)) * 100;
74
+ }
75
+ const scrollTargets = {
76
+ 25: [],
77
+ 50: [],
78
+ 75: [],
79
+ 100: [],
80
+ };
81
+ const scrollListener = () => {
82
+ Object.keys(scrollTargets).forEach((target) => {
83
+ if (
84
+ getScrollPercent() >= parseInt(target) &&
85
+ !scrollTargets[target].includes(router.route.path)
86
+ ) {
87
+ scrollTargets[target].push(router.route.path);
88
+ trackEvent([
89
+ "Interaction",
90
+ "Scroll Depth",
91
+ router.route.path,
92
+ `${target}%`,
93
+ ]);
94
+ }
95
+ });
96
+ };
97
+ if (enabled) {
98
+ document.addEventListener("scroll", scrollListener);
99
+ } else {
100
+ document.removeEventListener("scroll", scrollListener);
101
+ }
102
+ };
103
+
104
+ /**
105
+ * Track user navigation from one page to another
106
+ *
107
+ * @param enabled Boolean
108
+ */
109
+ export const trackRouterNavigation = (router) => {
110
+ const trackNavigation = () => {
111
+ if (!document.cookie.includes("mtm_cookie_consent")) {
112
+ return;
113
+ }
114
+ _paq.push(["setCustomUrl", router.route.path]);
115
+ _paq.push(["setDocumentTitle", router.route.data.title]);
116
+ _paq.push(["trackPageView"]);
117
+ _paq.push(["enableLinkTracking"]);
118
+ };
119
+ watch(() => router.route.data.relativePath, trackNavigation, {
120
+ immediate: true,
121
+ });
122
+ };
package/src/index.js CHANGED
@@ -3,6 +3,9 @@ import FeatureSection from "./components/FeatureSection.vue";
3
3
  import FeaturesGallery from "./components/FeaturesGallery.vue";
4
4
  import PricingTable from "./components/PricingTable.vue";
5
5
  import CTASection from "./components/CTASection.vue";
6
+ import CookieBanner from "./components/CookieBanner.vue";
7
+ import CookieSettings from "./components/CookieSettings.vue";
8
+ import NotFound from "./components/NotFound.vue";
6
9
  import Layout from "./Layout.vue";
7
10
  import "./style.css";
8
11
 
@@ -14,6 +17,9 @@ export default {
14
17
  app.component("FeaturesGallery", FeaturesGallery);
15
18
  app.component("PricingTable", PricingTable);
16
19
  app.component("CTASection", CTASection);
20
+ app.component("CookieBanner", CookieBanner);
21
+ app.component("CookieSettings", CookieSettings);
22
+ app.component("NotFound", NotFound);
17
23
 
18
24
  router.onAfterRouteChanged = () => {
19
25
  if (!import.meta.env.SSR) {
@@ -8,7 +8,88 @@ export const generate = (brandConfig) => ({
8
8
  appearance: false,
9
9
  cleanUrls: true,
10
10
  description: brandConfig.meta?.description,
11
- head: [["link", { rel: "icon", href: brandConfig.meta?.favicon }]],
11
+ head: [
12
+ ["link", { rel: "icon", href: brandConfig.meta?.favicon }],
13
+ // Open Graph / Facebook
14
+ ["meta", { property: "og:type", content: "website" }],
15
+ [
16
+ "meta",
17
+ {
18
+ property: "og:url",
19
+ content: brandConfig.home,
20
+ },
21
+ ],
22
+ ["meta", { property: "og:title", content: brandConfig.meta?.title }],
23
+ [
24
+ "meta",
25
+ {
26
+ property: "og:description",
27
+ content: brandConfig.meta?.description,
28
+ },
29
+ ],
30
+ // TODO
31
+ // [
32
+ // "meta",
33
+ // {
34
+ // property: "og:image",
35
+ // content:
36
+ // "https://earthcode.esa.int/img/EarthCODE_Herobanner_1920x1080.jpg",
37
+ // },
38
+ // ],
39
+ // Twitter
40
+ ["meta", { property: "twitter:card", content: "summary_large_image" }],
41
+ [
42
+ "meta",
43
+ {
44
+ property: "twitter:url",
45
+ content: brandConfig.home,
46
+ },
47
+ ],
48
+ ["meta", { property: "twitter:title", content: brandConfig.meta?.title }],
49
+ [
50
+ "meta",
51
+ {
52
+ property: "twitter:description",
53
+ content: brandConfig.meta?.description,
54
+ },
55
+ ],
56
+ // TODO
57
+ // [
58
+ // "meta",
59
+ // {
60
+ // property: "twitter:image",
61
+ // content:
62
+ // "https://earthcode.esa.int/img/EarthCODE_Herobanner_1920x1080.jpg",
63
+ // },
64
+ // ],
65
+ ...(brandConfig.analytics
66
+ ? [
67
+ [
68
+ "script",
69
+ {},
70
+ `
71
+ var _paq = (window._paq = window._paq || []);
72
+ /* tracker methods like "setCustomDimension" should be called before "trackPageView" */
73
+ _paq.push(["requireCookieConsent"]);
74
+ _paq.push(["setDocumentTitle", document.domain + "/" + document.title]);
75
+ _paq.push(["trackPageView"]);
76
+ _paq.push(["enableLinkTracking"]);
77
+ (function () {
78
+ var u = "https://nix.eox.at/piwik/";
79
+ _paq.push(["setTrackerUrl", u + "matomo.php"]);
80
+ _paq.push(["setSiteId", "${brandConfig.analytics.siteId}"]);
81
+ var d = document,
82
+ g = d.createElement("script"),
83
+ s = d.getElementsByTagName("script")[0];
84
+ g.async = true;
85
+ g.src = u + "matomo.js";
86
+ s.parentNode.insertBefore(g, s);
87
+ })();
88
+ `,
89
+ ],
90
+ ]
91
+ : []),
92
+ ],
12
93
  srcExclude: ["README.md"],
13
94
  themeConfig: {
14
95
  siteTitle: false,
@@ -16,6 +97,7 @@ export const generate = (brandConfig) => ({
16
97
  primaryColor: brandConfig.theme?.primary_color,
17
98
  secondaryColor:
18
99
  brandConfig.theme?.secondary_color || brandConfig.theme?.primary_color,
100
+ brandConfig,
19
101
  },
20
102
  logo: brandConfig.logo,
21
103
  footer: {