@eox/pages-theme-eox 2.0.0-alpha.5 → 2.0.0-alpha.7
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/.gitlab-ci.yml +4 -1
- package/README.md +93 -4
- package/cypress/support/mocks/authors.data.js +14 -0
- package/cypress/support/mocks/helpers.js +40 -1
- package/cypress.config.js +9 -0
- package/package.json +5 -2
- package/src/components/BlogPost.vue +13 -1
- package/src/components/FeatureSection.cy.js +16 -0
- package/src/components/Footer.cy.js +12 -0
- package/src/components/Footer.vue +7 -0
- package/src/components/MobileNavDropdown.vue +7 -0
- package/src/components/NavDropdown.vue +7 -0
- package/src/components/TestimonialsSection.cy.js +2 -5
- package/src/helpers.js +169 -32
- package/src/helpers.test.js +153 -0
- package/src/llms/index.js +64 -0
- package/src/llms/llms.test.js +980 -0
- package/src/llms/storiesPlugin.js +771 -0
- package/src/pages/author/[author].paths.js +2 -2
- package/src/posts.data.js +39 -1
- package/src/style.css +140 -0
- package/src/vitepressConfig.mjs +7 -1
package/.gitlab-ci.yml
CHANGED
|
@@ -10,9 +10,11 @@ cache:
|
|
|
10
10
|
- package-lock.json
|
|
11
11
|
paths:
|
|
12
12
|
- .npm/
|
|
13
|
+
- .cache/Cypress
|
|
13
14
|
|
|
14
15
|
variables:
|
|
15
16
|
npm_config_cache: "$CI_PROJECT_DIR/.npm"
|
|
17
|
+
CYPRESS_CACHE_FOLDER: "$CI_PROJECT_DIR/.cache/Cypress"
|
|
16
18
|
|
|
17
19
|
security:
|
|
18
20
|
stage: security
|
|
@@ -33,6 +35,7 @@ test:
|
|
|
33
35
|
stage: test
|
|
34
36
|
script:
|
|
35
37
|
- npm ci
|
|
36
|
-
-
|
|
38
|
+
- npx cypress install
|
|
39
|
+
- npm run test
|
|
37
40
|
rules:
|
|
38
41
|
- if: $CI_PIPELINE_SOURCE == 'merge_request_event' && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'main'
|
package/README.md
CHANGED
|
@@ -21,6 +21,45 @@ See also the [Vitepress Docs](https://vitepress.dev/guide/custom-theme#consuming
|
|
|
21
21
|
|
|
22
22
|
This theme supports standard [VitePress Default Theme Configuration](https://vitepress.dev/reference/default-theme-config) with some specific adaptations.
|
|
23
23
|
|
|
24
|
+
### LLMs Configuration (`llms.txt`)
|
|
25
|
+
|
|
26
|
+
This theme integrates [vitepress-plugin-llms](https://github.com/nmoodev/vitepress-plugin-llms) to automatically generate AI-friendly documentation (`llms.txt` and `llms-full.txt`), along with custom support for VitePress story routes and remote story manifests.
|
|
27
|
+
|
|
28
|
+
Generation is enabled by default. You can configure options via `brandConfig.llms` in your brand configuration:
|
|
29
|
+
|
|
30
|
+
```javascript
|
|
31
|
+
// Example brandConfig:
|
|
32
|
+
export const config = {
|
|
33
|
+
// ... other brand settings ...
|
|
34
|
+
llms: {
|
|
35
|
+
// Upstream vitepress-plugin-llms options
|
|
36
|
+
domain: "https://yourdomain.com", // Base URL to resolve absolute links in llms.txt
|
|
37
|
+
copyButtons: true, // (Optional) Adds copy buttons to code blocks in documentation
|
|
38
|
+
|
|
39
|
+
// Theme-specific Story Routes config
|
|
40
|
+
stories: {
|
|
41
|
+
heading: "Stories", // (Optional) Heading used in llms.txt and route pages (default: "Stories")
|
|
42
|
+
path: "narratives/[narrative]", // (Optional) Story route pattern (e.g. "narratives/[narrative]" or "story"). Also accepts `storyPath`
|
|
43
|
+
manifest: "https://cdn.example.com/narratives.json", // (Optional) URL to stories manifest (auto-detected from gallery components if omitted)
|
|
44
|
+
concurrency: 8, // (Optional) Maximum concurrent story downloads (default: 8)
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
#### Disabling LLMs
|
|
51
|
+
|
|
52
|
+
- To disable LLM documentation generation entirely: set `llms: false`.
|
|
53
|
+
- To generate general docs without stories: set `stories: false` inside `llms`:
|
|
54
|
+
|
|
55
|
+
```javascript
|
|
56
|
+
export const config = {
|
|
57
|
+
llms: {
|
|
58
|
+
stories: false,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
```
|
|
62
|
+
|
|
24
63
|
### Navigation Bar
|
|
25
64
|
|
|
26
65
|
The Navigation Bar follows the [Default Theme Nav](https://vitepress.dev/reference/default-theme-nav) structure with the following differences:
|
|
@@ -572,7 +611,14 @@ feedback: false
|
|
|
572
611
|
The theme exports a set of shared utility and helper functions from its helpers module. You can import and use these helpers in your custom VitePress components, checkout pages, or inline script setups:
|
|
573
612
|
|
|
574
613
|
```javascript
|
|
575
|
-
import {
|
|
614
|
+
import {
|
|
615
|
+
formatPrice,
|
|
616
|
+
trackEvent,
|
|
617
|
+
trackPageView,
|
|
618
|
+
trackSiteSearch,
|
|
619
|
+
normalizeTrackingUrl,
|
|
620
|
+
t,
|
|
621
|
+
} from "@eox/pages-theme-eox/src/helpers";
|
|
576
622
|
```
|
|
577
623
|
|
|
578
624
|
### Available Helpers
|
|
@@ -598,12 +644,12 @@ formatPrice(12000, "en"); // "€ 12,000.-"
|
|
|
598
644
|
formatPrice(49.99, "de"); // "€ 49,99"
|
|
599
645
|
```
|
|
600
646
|
|
|
601
|
-
#### `trackEvent(eventDetails)`
|
|
647
|
+
#### `trackEvent(eventDetails, ...rest)`
|
|
602
648
|
|
|
603
|
-
Sends a custom tracking event to Matomo Analytics.
|
|
649
|
+
Sends a custom tracking event to Matomo Analytics. Automatically normalizes any path strings passed in category, action, or name.
|
|
604
650
|
|
|
605
651
|
- **Parameters**:
|
|
606
|
-
- `eventDetails` (`Array<string
|
|
652
|
+
- `eventDetails` (`Array<string|number>|string`): An array `[Category, Action, Name?, Value?]` or positional arguments `(category, action, name, value)`.
|
|
607
653
|
|
|
608
654
|
##### Example Usage:
|
|
609
655
|
|
|
@@ -611,8 +657,51 @@ Sends a custom tracking event to Matomo Analytics.
|
|
|
611
657
|
import { trackEvent } from "@eox/pages-theme-eox/src/helpers";
|
|
612
658
|
|
|
613
659
|
trackEvent(["CTA", "Click", "Technical Specifications", "Viewing Ready"]);
|
|
660
|
+
trackEvent("indicators", "select_indicator", "N1_NO2");
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
#### `trackPageView(options)`
|
|
664
|
+
|
|
665
|
+
Tracks a pageview with normalized path (trailing slash stripped, transient parameters removed) and custom document title.
|
|
666
|
+
|
|
667
|
+
- **Parameters**:
|
|
668
|
+
- `options` (`Object|string`): Vitepress router object, `{ path, title }` options object, or custom path string.
|
|
669
|
+
|
|
670
|
+
##### Example Usage:
|
|
671
|
+
|
|
672
|
+
```javascript
|
|
673
|
+
import { trackPageView } from "@eox/pages-theme-eox/src/helpers";
|
|
674
|
+
|
|
675
|
+
trackPageView({ path: "/explore/", title: "Explore" });
|
|
676
|
+
// Sends normalized URL "/explore" to Matomo
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
#### `trackSiteSearch(keyword, category, resultsCount)`
|
|
680
|
+
|
|
681
|
+
Sends search keyword telemetry to Matomo Site Search reports.
|
|
682
|
+
|
|
683
|
+
- **Parameters**:
|
|
684
|
+
- `keyword` (`string`): Searched term.
|
|
685
|
+
- `category` (`string|boolean`): Category (optional, e.g. `'indicators'`, `'stories'`).
|
|
686
|
+
- `resultsCount` (`number`): Number of search results returned.
|
|
687
|
+
|
|
688
|
+
##### Example Usage:
|
|
689
|
+
|
|
690
|
+
```javascript
|
|
691
|
+
import { trackSiteSearch } from "@eox/pages-theme-eox/src/helpers";
|
|
692
|
+
|
|
693
|
+
trackSiteSearch("methane", "indicators", 4);
|
|
614
694
|
```
|
|
615
695
|
|
|
696
|
+
#### `normalizeTrackingUrl(inputUrl, options)`
|
|
697
|
+
|
|
698
|
+
Pure helper that strips trailing slashes (e.g., `/explore/` -> `/explore`), strips volatile coordinate query params (`x`, `y`, `z`), and sorts remaining query parameters.
|
|
699
|
+
|
|
700
|
+
- **Parameters**:
|
|
701
|
+
- `inputUrl` (`string`): URL or path to normalize.
|
|
702
|
+
- `options` (`Object`): Optional configuration (`ignoredParams`, `stripTrailingSlash`).
|
|
703
|
+
- **Returns**: `string` - Normalized URL string.
|
|
704
|
+
|
|
616
705
|
#### `t(key, i18n)`
|
|
617
706
|
|
|
618
707
|
Retrieves localized translation strings from an i18n mapping object using dot-notation. Falls back to the key itself if no translation is defined.
|
|
@@ -1,7 +1,40 @@
|
|
|
1
1
|
// Mock generic helpers
|
|
2
2
|
// We delegate to a global object to allow spying in tests
|
|
3
3
|
window.__helpersMock = {
|
|
4
|
-
trackEvent: (details) => {},
|
|
4
|
+
trackEvent: (details, ...rest) => {},
|
|
5
|
+
trackPageView: (options) => {},
|
|
6
|
+
trackSiteSearch: (keyword, category, count) => {},
|
|
7
|
+
normalizeTrackingUrl: (url, options) => {
|
|
8
|
+
if (!url || typeof url !== "string") return url;
|
|
9
|
+
const { ignoredParams = ["x", "y", "z"], stripTrailingSlash = true } =
|
|
10
|
+
options || {};
|
|
11
|
+
try {
|
|
12
|
+
const base =
|
|
13
|
+
typeof window !== "undefined" && window.location?.href
|
|
14
|
+
? window.location.href
|
|
15
|
+
: "https://eox.at";
|
|
16
|
+
const u = new URL(url, base);
|
|
17
|
+
let pathname = u.pathname;
|
|
18
|
+
if (stripTrailingSlash && pathname.length > 1 && pathname.endsWith("/")) {
|
|
19
|
+
pathname = pathname.replace(/\/+$/, "");
|
|
20
|
+
}
|
|
21
|
+
const searchParams = new URLSearchParams(u.search);
|
|
22
|
+
for (const key of ignoredParams) {
|
|
23
|
+
searchParams.delete(key);
|
|
24
|
+
}
|
|
25
|
+
searchParams.sort();
|
|
26
|
+
const search = searchParams.toString();
|
|
27
|
+
const isRelative = !/^https?:\/\//i.test(url);
|
|
28
|
+
if (isRelative) {
|
|
29
|
+
return pathname + (search ? `?${search}` : "");
|
|
30
|
+
}
|
|
31
|
+
u.pathname = pathname;
|
|
32
|
+
u.search = search ? `?${search}` : "";
|
|
33
|
+
return u.toString();
|
|
34
|
+
} catch {
|
|
35
|
+
return url;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
5
38
|
acceptCookies: (router) => {},
|
|
6
39
|
declineCookies: (router) => {},
|
|
7
40
|
enableTracking: (enable, router) => {},
|
|
@@ -94,6 +127,12 @@ window.__helpersMock = {
|
|
|
94
127
|
};
|
|
95
128
|
|
|
96
129
|
export const trackEvent = (...args) => window.__helpersMock.trackEvent(...args);
|
|
130
|
+
export const trackPageView = (...args) =>
|
|
131
|
+
window.__helpersMock.trackPageView(...args);
|
|
132
|
+
export const trackSiteSearch = (...args) =>
|
|
133
|
+
window.__helpersMock.trackSiteSearch(...args);
|
|
134
|
+
export const normalizeTrackingUrl = (...args) =>
|
|
135
|
+
window.__helpersMock.normalizeTrackingUrl(...args);
|
|
97
136
|
export const acceptCookies = (...args) =>
|
|
98
137
|
window.__helpersMock.acceptCookies(...args);
|
|
99
138
|
export const declineCookies = (...args) =>
|
package/cypress.config.js
CHANGED
|
@@ -34,6 +34,15 @@ export default defineConfig({
|
|
|
34
34
|
__dirname,
|
|
35
35
|
"cypress/support/mocks/features.data.js",
|
|
36
36
|
),
|
|
37
|
+
// Alias authors.data.js to our mock
|
|
38
|
+
"../authors.data.js": path.resolve(
|
|
39
|
+
__dirname,
|
|
40
|
+
"cypress/support/mocks/authors.data.js",
|
|
41
|
+
),
|
|
42
|
+
"../../authors.data.js": path.resolve(
|
|
43
|
+
__dirname,
|
|
44
|
+
"cypress/support/mocks/authors.data.js",
|
|
45
|
+
),
|
|
37
46
|
},
|
|
38
47
|
},
|
|
39
48
|
plugins: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eox/pages-theme-eox",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Vitepress Theme with EOX branding",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
"scripts": {
|
|
13
13
|
"format": "npx prettier --write .",
|
|
14
14
|
"format:check": "npx prettier --check .",
|
|
15
|
+
"test": "npm run test:unit && npm run test:component",
|
|
16
|
+
"test:unit": "node --test src/helpers.test.js src/llms/llms.test.js",
|
|
15
17
|
"test:component": "cypress run --component",
|
|
16
18
|
"release": "release-it"
|
|
17
19
|
},
|
|
@@ -19,7 +21,8 @@
|
|
|
19
21
|
"@eox/eslint-config": "^2.0.0",
|
|
20
22
|
"@eox/feedback": "^1.1.1",
|
|
21
23
|
"@eox/ui": "^1.0.1",
|
|
22
|
-
"vitepress": "^1.6.3"
|
|
24
|
+
"vitepress": "^1.6.3",
|
|
25
|
+
"vitepress-plugin-llms": "^1.13.5"
|
|
23
26
|
},
|
|
24
27
|
"devDependencies": {
|
|
25
28
|
"@release-it/conventional-changelog": "^10.0.6",
|
|
@@ -191,7 +191,19 @@ const coverImage = computed(() => {
|
|
|
191
191
|
|
|
192
192
|
// Dynamic Recent Posts (excluding current page)
|
|
193
193
|
const calculatedRecentPosts = computed(() => {
|
|
194
|
-
|
|
194
|
+
const rel = page.value.relativePath || "";
|
|
195
|
+
const current = (
|
|
196
|
+
"/" + rel.replace(/(^|\/)index\.md$/, "$1").replace(/\.md$/, "")
|
|
197
|
+
)
|
|
198
|
+
.replace(/\/$/, "")
|
|
199
|
+
.replace(/\.html$/, "");
|
|
200
|
+
|
|
201
|
+
return posts
|
|
202
|
+
.filter((p) => {
|
|
203
|
+
const pUrl = (p.url || "").replace(/\/$/, "").replace(/\.html$/, "");
|
|
204
|
+
return pUrl !== current && p.title !== frontmatter.value.title;
|
|
205
|
+
})
|
|
206
|
+
.slice(0, 5);
|
|
195
207
|
});
|
|
196
208
|
|
|
197
209
|
// Dynamic Hot Tags
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { h } from "vue";
|
|
1
2
|
import FeatureSection from "./FeatureSection.vue";
|
|
2
3
|
|
|
3
4
|
describe("<FeatureSection />", () => {
|
|
@@ -60,4 +61,19 @@ describe("<FeatureSection />", () => {
|
|
|
60
61
|
cy.get(".feature-section").should("have.class", "dark");
|
|
61
62
|
cy.get(".feature-section").should("have.class", "reverse");
|
|
62
63
|
});
|
|
64
|
+
|
|
65
|
+
it("has padding-block-start 0 when following a title", () => {
|
|
66
|
+
const TestWrapper = {
|
|
67
|
+
render() {
|
|
68
|
+
return h("div", [
|
|
69
|
+
h("h2", "Products & Services"),
|
|
70
|
+
h("p", "Subtitle paragraph"),
|
|
71
|
+
h(FeatureSection, defaultProps),
|
|
72
|
+
]);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
cy.mount(TestWrapper);
|
|
77
|
+
cy.get(".feature-section").should("have.css", "padding-top", "0px");
|
|
78
|
+
});
|
|
63
79
|
});
|
|
@@ -107,4 +107,16 @@ describe("<Footer />", () => {
|
|
|
107
107
|
.should("have.attr", "href", "https://eox.at")
|
|
108
108
|
.and("contain", "EOX");
|
|
109
109
|
});
|
|
110
|
+
|
|
111
|
+
it("defines hover underline styles for footer links", () => {
|
|
112
|
+
cy.mount(Footer);
|
|
113
|
+
cy.get("footer .link").should("have.css", "text-decoration-line", "none");
|
|
114
|
+
cy.document().then((doc) => {
|
|
115
|
+
const styles = Array.from(doc.querySelectorAll("style"))
|
|
116
|
+
.map((s) => s.textContent)
|
|
117
|
+
.join("\n");
|
|
118
|
+
expect(styles).to.include("footer .link:hover");
|
|
119
|
+
expect(styles).to.include("text-decoration: underline");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
110
122
|
});
|
|
@@ -136,10 +136,17 @@ footer .grid > div:not(:first-child) p,
|
|
|
136
136
|
margin: 0 !important;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
footer a:not(.button),
|
|
139
140
|
footer .link {
|
|
140
141
|
text-decoration: none;
|
|
141
142
|
}
|
|
142
143
|
|
|
144
|
+
footer a:not(.button):hover,
|
|
145
|
+
footer .link:hover {
|
|
146
|
+
text-decoration: underline;
|
|
147
|
+
text-underline-offset: 2px;
|
|
148
|
+
}
|
|
149
|
+
|
|
143
150
|
.footer-columns {
|
|
144
151
|
columns: 1;
|
|
145
152
|
column-gap: 2rem;
|
|
@@ -5,10 +5,7 @@ describe("<TestimonialsSection />", () => {
|
|
|
5
5
|
cy.mount(TestimonialsSection);
|
|
6
6
|
|
|
7
7
|
// Verify title
|
|
8
|
-
cy.get(".
|
|
9
|
-
"contain",
|
|
10
|
-
"What our customers are saying",
|
|
11
|
-
);
|
|
8
|
+
cy.get(".headline h2").should("contain", "What our customers are saying");
|
|
12
9
|
|
|
13
10
|
// Verify first testimonial quote
|
|
14
11
|
cy.get(".quote-text").should(
|
|
@@ -61,7 +58,7 @@ describe("<TestimonialsSection />", () => {
|
|
|
61
58
|
});
|
|
62
59
|
|
|
63
60
|
// Check title
|
|
64
|
-
cy.get(".
|
|
61
|
+
cy.get(".headline h2").should("contain", customTitle);
|
|
65
62
|
|
|
66
63
|
// Check first custom testimonial
|
|
67
64
|
cy.get(".quote-text").should("contain", "This is a custom quote");
|
package/src/helpers.js
CHANGED
|
@@ -1,28 +1,157 @@
|
|
|
1
1
|
import { watch } from "vue";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Normalizes a URL / path for tracking by:
|
|
5
|
+
* - Removing trailing slash (e.g. `/explore/` -> `/explore`, except for root `/`)
|
|
6
|
+
* - Stripping transient / high-cardinality UI params (e.g., coordinates `x`, `y`, `z`)
|
|
7
|
+
* - Sorting remaining query parameters alphabetically
|
|
5
8
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* @param {string} inputUrl - The URL or relative path to normalize
|
|
10
|
+
* @param {Object} [options] - Normalization options
|
|
11
|
+
* @param {Array<string>} [options.ignoredParams] - Query parameter keys to strip
|
|
12
|
+
* @param {boolean} [options.stripTrailingSlash=true] - Whether to strip trailing slashes
|
|
13
|
+
* @returns {string} Normalized URL / path
|
|
14
|
+
*/
|
|
15
|
+
export const normalizeTrackingUrl = (inputUrl, options = {}) => {
|
|
16
|
+
if (!inputUrl || typeof inputUrl !== "string") return inputUrl;
|
|
17
|
+
const { ignoredParams = ["x", "y", "z"], stripTrailingSlash = true } =
|
|
18
|
+
options;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const base =
|
|
22
|
+
typeof window !== "undefined" && window.location?.href
|
|
23
|
+
? window.location.href
|
|
24
|
+
: "https://eox.at";
|
|
25
|
+
const url = new URL(inputUrl, base);
|
|
26
|
+
let pathname = url.pathname;
|
|
27
|
+
|
|
28
|
+
if (stripTrailingSlash && pathname.length > 1 && pathname.endsWith("/")) {
|
|
29
|
+
pathname = pathname.replace(/\/+$/, "");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const searchParams = new URLSearchParams(url.search);
|
|
33
|
+
for (const key of ignoredParams) {
|
|
34
|
+
searchParams.delete(key);
|
|
35
|
+
}
|
|
36
|
+
searchParams.sort();
|
|
37
|
+
|
|
38
|
+
const search = searchParams.toString();
|
|
39
|
+
const isRelativeInput = !/^https?:\/\//i.test(inputUrl);
|
|
40
|
+
|
|
41
|
+
if (isRelativeInput) {
|
|
42
|
+
return pathname + (search ? `?${search}` : "");
|
|
43
|
+
}
|
|
44
|
+
url.pathname = pathname;
|
|
45
|
+
url.search = search ? `?${search}` : "";
|
|
46
|
+
return url.toString();
|
|
47
|
+
} catch {
|
|
48
|
+
if (stripTrailingSlash && inputUrl.length > 1 && inputUrl.endsWith("/")) {
|
|
49
|
+
return inputUrl.replace(/\/+$/, "");
|
|
50
|
+
}
|
|
51
|
+
return inputUrl;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Track Matomo event with array or positional arguments of four primary components:
|
|
57
|
+
* Category, Action, Name (optional), Value (optional)
|
|
58
|
+
*
|
|
59
|
+
* Automatically normalizes any path parameters (e.g., Action or Name).
|
|
10
60
|
*
|
|
11
61
|
* See https://matomo.org/faq/reports/the-anatomy-of-an-event/
|
|
12
62
|
*
|
|
13
|
-
* @param {Array<string
|
|
63
|
+
* @param {Array<string|number>|string} eventDetails - Array of event details or Category string
|
|
64
|
+
* @param {...*} rest - Remaining event details if passed positionally
|
|
65
|
+
*/
|
|
66
|
+
export const trackEvent = (eventDetails, ...rest) => {
|
|
67
|
+
if (typeof window === "undefined") return;
|
|
68
|
+
const _paq = (window._paq = window._paq || []);
|
|
69
|
+
|
|
70
|
+
const details = Array.isArray(eventDetails)
|
|
71
|
+
? [...eventDetails]
|
|
72
|
+
: [eventDetails, ...rest];
|
|
73
|
+
|
|
74
|
+
if (typeof details[1] === "string" && details[1].startsWith("/")) {
|
|
75
|
+
details[1] = normalizeTrackingUrl(details[1]);
|
|
76
|
+
}
|
|
77
|
+
if (typeof details[2] === "string" && details[2].startsWith("/")) {
|
|
78
|
+
details[2] = normalizeTrackingUrl(details[2]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
_paq.push(["trackEvent", ...details]);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Track page view with normalized URL and standardized document title.
|
|
86
|
+
*
|
|
87
|
+
* @param {Object|string} [options] - Router instance, options object, or path string
|
|
88
|
+
* @param {string} [options.path] - Custom path to track
|
|
89
|
+
* @param {string} [options.title] - Custom document title to track
|
|
90
|
+
* @param {Object} [options.route] - VitePress router route object
|
|
14
91
|
*/
|
|
15
|
-
export const
|
|
16
|
-
if (
|
|
17
|
-
|
|
92
|
+
export const trackPageView = (options = {}) => {
|
|
93
|
+
if (typeof window === "undefined") return;
|
|
94
|
+
const _paq = (window._paq = window._paq || []);
|
|
95
|
+
|
|
96
|
+
let rawPath = "";
|
|
97
|
+
let rawTitle = "";
|
|
98
|
+
|
|
99
|
+
if (typeof options === "string") {
|
|
100
|
+
rawPath = options;
|
|
101
|
+
} else if (options && typeof options === "object") {
|
|
102
|
+
if (options.route) {
|
|
103
|
+
rawPath = options.route.path;
|
|
104
|
+
rawTitle = options.route.data?.title;
|
|
105
|
+
} else {
|
|
106
|
+
rawPath =
|
|
107
|
+
options.path ||
|
|
108
|
+
options.customUrl ||
|
|
109
|
+
(typeof window !== "undefined"
|
|
110
|
+
? window.location.pathname + window.location.search
|
|
111
|
+
: "");
|
|
112
|
+
rawTitle =
|
|
113
|
+
options.title ||
|
|
114
|
+
options.customTitle ||
|
|
115
|
+
(typeof document !== "undefined" ? document.title : "");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const normalizedUrl = normalizeTrackingUrl(rawPath);
|
|
120
|
+
if (normalizedUrl) {
|
|
121
|
+
_paq.push(["setCustomUrl", normalizedUrl]);
|
|
18
122
|
}
|
|
123
|
+
if (rawTitle) {
|
|
124
|
+
_paq.push(["setDocumentTitle", rawTitle.trim()]);
|
|
125
|
+
}
|
|
126
|
+
_paq.push(["trackPageView"]);
|
|
127
|
+
_paq.push(["enableLinkTracking"]);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Track site search keyword query in Matomo.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} keyword - Searched term
|
|
134
|
+
* @param {string|boolean} [category=false] - Search category (e.g. 'stories', 'indicators')
|
|
135
|
+
* @param {number} [resultsCount=0] - Number of search results found
|
|
136
|
+
*/
|
|
137
|
+
export const trackSiteSearch = (
|
|
138
|
+
keyword,
|
|
139
|
+
category = false,
|
|
140
|
+
resultsCount = 0,
|
|
141
|
+
) => {
|
|
142
|
+
if (typeof window === "undefined") return;
|
|
143
|
+
const _paq = (window._paq = window._paq || []);
|
|
144
|
+
_paq.push(["trackSiteSearch", keyword, category, resultsCount]);
|
|
19
145
|
};
|
|
20
146
|
|
|
21
147
|
/**
|
|
22
148
|
* Accept cookies
|
|
23
149
|
*/
|
|
24
150
|
export const acceptCookies = (router) => {
|
|
25
|
-
|
|
151
|
+
if (typeof window !== "undefined") {
|
|
152
|
+
const _paq = (window._paq = window._paq || []);
|
|
153
|
+
_paq.push(["rememberCookieConsentGiven"]);
|
|
154
|
+
}
|
|
26
155
|
showBanner(false);
|
|
27
156
|
enableTracking(true, router);
|
|
28
157
|
};
|
|
@@ -31,8 +160,11 @@ export const acceptCookies = (router) => {
|
|
|
31
160
|
* Decline cookies
|
|
32
161
|
*/
|
|
33
162
|
export const declineCookies = (router) => {
|
|
34
|
-
|
|
35
|
-
|
|
163
|
+
if (typeof window !== "undefined") {
|
|
164
|
+
const _paq = (window._paq = window._paq || []);
|
|
165
|
+
_paq.push(["forgetCookieConsentGiven"]);
|
|
166
|
+
_paq.push(["optUserOut"]);
|
|
167
|
+
}
|
|
36
168
|
showBanner(false);
|
|
37
169
|
enableTracking(false, router);
|
|
38
170
|
};
|
|
@@ -60,17 +192,24 @@ export const enableTracking = (enabled, router) => {
|
|
|
60
192
|
};
|
|
61
193
|
|
|
62
194
|
/**
|
|
63
|
-
* Track page scrolling depth
|
|
195
|
+
* Track page scrolling depth with normalized path and numeric depth values.
|
|
64
196
|
*
|
|
65
197
|
* @param enabled Boolean
|
|
198
|
+
* @param router VitePress router
|
|
66
199
|
*/
|
|
67
200
|
export const trackPageScrolling = (enabled, router) => {
|
|
68
201
|
function getScrollPercent() {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
202
|
+
try {
|
|
203
|
+
const h = document.documentElement,
|
|
204
|
+
b = document.body,
|
|
205
|
+
st = "scrollTop",
|
|
206
|
+
sh = "scrollHeight";
|
|
207
|
+
const scrollable = (h[sh] || b[sh]) - h.clientHeight;
|
|
208
|
+
if (scrollable <= 0) return 0;
|
|
209
|
+
return ((h[st] || b[st]) / scrollable) * 100;
|
|
210
|
+
} catch {
|
|
211
|
+
return 0;
|
|
212
|
+
}
|
|
74
213
|
}
|
|
75
214
|
const scrollTargets = {
|
|
76
215
|
25: [],
|
|
@@ -79,17 +218,18 @@ export const trackPageScrolling = (enabled, router) => {
|
|
|
79
218
|
100: [],
|
|
80
219
|
};
|
|
81
220
|
const scrollListener = () => {
|
|
221
|
+
const currentPath = normalizeTrackingUrl(router.route.path);
|
|
82
222
|
Object.keys(scrollTargets).forEach((target) => {
|
|
83
223
|
if (
|
|
84
|
-
getScrollPercent() >= parseInt(target) &&
|
|
85
|
-
!scrollTargets[target].includes(
|
|
224
|
+
getScrollPercent() >= parseInt(target, 10) &&
|
|
225
|
+
!scrollTargets[target].includes(currentPath)
|
|
86
226
|
) {
|
|
87
|
-
scrollTargets[target].push(
|
|
227
|
+
scrollTargets[target].push(currentPath);
|
|
88
228
|
trackEvent([
|
|
89
229
|
"Interaction",
|
|
90
230
|
"Scroll Depth",
|
|
91
|
-
|
|
92
|
-
|
|
231
|
+
currentPath,
|
|
232
|
+
parseInt(target, 10),
|
|
93
233
|
]);
|
|
94
234
|
}
|
|
95
235
|
});
|
|
@@ -102,19 +242,16 @@ export const trackPageScrolling = (enabled, router) => {
|
|
|
102
242
|
};
|
|
103
243
|
|
|
104
244
|
/**
|
|
105
|
-
* Track user navigation from one page to another
|
|
245
|
+
* Track user navigation from one page to another with normalized URLs.
|
|
106
246
|
*
|
|
107
|
-
* @param
|
|
247
|
+
* @param router VitePress router
|
|
108
248
|
*/
|
|
109
249
|
export const trackRouterNavigation = (router) => {
|
|
110
250
|
const trackNavigation = () => {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
_paq.push(["setDocumentTitle", router.route.data.title]);
|
|
116
|
-
_paq.push(["trackPageView"]);
|
|
117
|
-
_paq.push(["enableLinkTracking"]);
|
|
251
|
+
trackPageView({
|
|
252
|
+
path: router.route.path,
|
|
253
|
+
title: router.route.data?.title,
|
|
254
|
+
});
|
|
118
255
|
};
|
|
119
256
|
watch(() => router.route.data.relativePath, trackNavigation, {
|
|
120
257
|
immediate: true,
|