@eox/pages-theme-eox 2.0.0-alpha.4 → 2.0.0-alpha.6
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 +54 -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 +3 -1
- package/src/components/BlogList.vue +4 -0
- package/src/components/BlogPost.vue +17 -1
- package/src/components/CTASection.vue +2 -0
- package/src/components/FeatureCard/Index.vue +2 -0
- package/src/components/FeatureCard/Poster.vue +2 -0
- package/src/components/FeatureSection.cy.js +16 -0
- package/src/components/FeaturesGallery.vue +11 -13
- package/src/components/Footer.cy.js +12 -0
- package/src/components/Footer.vue +7 -0
- package/src/components/ImageCarousel.vue +2 -0
- package/src/components/ImageElement.vue +2 -0
- package/src/components/ImageGallery.vue +2 -0
- package/src/components/LogoSection.vue +2 -0
- package/src/components/MobileNavDropdown.vue +7 -0
- package/src/components/NavBar.vue +1 -1
- 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/pages/author/[author].paths.js +2 -2
- package/src/posts.data.js +39 -1
- package/src/style.css +140 -0
- package/src/vitepressConfig.mjs +1 -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
|
@@ -572,7 +572,14 @@ feedback: false
|
|
|
572
572
|
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
573
|
|
|
574
574
|
```javascript
|
|
575
|
-
import {
|
|
575
|
+
import {
|
|
576
|
+
formatPrice,
|
|
577
|
+
trackEvent,
|
|
578
|
+
trackPageView,
|
|
579
|
+
trackSiteSearch,
|
|
580
|
+
normalizeTrackingUrl,
|
|
581
|
+
t,
|
|
582
|
+
} from "@eox/pages-theme-eox/src/helpers";
|
|
576
583
|
```
|
|
577
584
|
|
|
578
585
|
### Available Helpers
|
|
@@ -598,12 +605,12 @@ formatPrice(12000, "en"); // "€ 12,000.-"
|
|
|
598
605
|
formatPrice(49.99, "de"); // "€ 49,99"
|
|
599
606
|
```
|
|
600
607
|
|
|
601
|
-
#### `trackEvent(eventDetails)`
|
|
608
|
+
#### `trackEvent(eventDetails, ...rest)`
|
|
602
609
|
|
|
603
|
-
Sends a custom tracking event to Matomo Analytics.
|
|
610
|
+
Sends a custom tracking event to Matomo Analytics. Automatically normalizes any path strings passed in category, action, or name.
|
|
604
611
|
|
|
605
612
|
- **Parameters**:
|
|
606
|
-
- `eventDetails` (`Array<string
|
|
613
|
+
- `eventDetails` (`Array<string|number>|string`): An array `[Category, Action, Name?, Value?]` or positional arguments `(category, action, name, value)`.
|
|
607
614
|
|
|
608
615
|
##### Example Usage:
|
|
609
616
|
|
|
@@ -611,8 +618,51 @@ Sends a custom tracking event to Matomo Analytics.
|
|
|
611
618
|
import { trackEvent } from "@eox/pages-theme-eox/src/helpers";
|
|
612
619
|
|
|
613
620
|
trackEvent(["CTA", "Click", "Technical Specifications", "Viewing Ready"]);
|
|
621
|
+
trackEvent("indicators", "select_indicator", "N1_NO2");
|
|
614
622
|
```
|
|
615
623
|
|
|
624
|
+
#### `trackPageView(options)`
|
|
625
|
+
|
|
626
|
+
Tracks a pageview with normalized path (trailing slash stripped, transient parameters removed) and custom document title.
|
|
627
|
+
|
|
628
|
+
- **Parameters**:
|
|
629
|
+
- `options` (`Object|string`): Vitepress router object, `{ path, title }` options object, or custom path string.
|
|
630
|
+
|
|
631
|
+
##### Example Usage:
|
|
632
|
+
|
|
633
|
+
```javascript
|
|
634
|
+
import { trackPageView } from "@eox/pages-theme-eox/src/helpers";
|
|
635
|
+
|
|
636
|
+
trackPageView({ path: "/explore/", title: "Explore" });
|
|
637
|
+
// Sends normalized URL "/explore" to Matomo
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
#### `trackSiteSearch(keyword, category, resultsCount)`
|
|
641
|
+
|
|
642
|
+
Sends search keyword telemetry to Matomo Site Search reports.
|
|
643
|
+
|
|
644
|
+
- **Parameters**:
|
|
645
|
+
- `keyword` (`string`): Searched term.
|
|
646
|
+
- `category` (`string|boolean`): Category (optional, e.g. `'indicators'`, `'stories'`).
|
|
647
|
+
- `resultsCount` (`number`): Number of search results returned.
|
|
648
|
+
|
|
649
|
+
##### Example Usage:
|
|
650
|
+
|
|
651
|
+
```javascript
|
|
652
|
+
import { trackSiteSearch } from "@eox/pages-theme-eox/src/helpers";
|
|
653
|
+
|
|
654
|
+
trackSiteSearch("methane", "indicators", 4);
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
#### `normalizeTrackingUrl(inputUrl, options)`
|
|
658
|
+
|
|
659
|
+
Pure helper that strips trailing slashes (e.g., `/explore/` -> `/explore`), strips volatile coordinate query params (`x`, `y`, `z`), and sorts remaining query parameters.
|
|
660
|
+
|
|
661
|
+
- **Parameters**:
|
|
662
|
+
- `inputUrl` (`string`): URL or path to normalize.
|
|
663
|
+
- `options` (`Object`): Optional configuration (`ignoredParams`, `stripTrailingSlash`).
|
|
664
|
+
- **Returns**: `string` - Normalized URL string.
|
|
665
|
+
|
|
616
666
|
#### `t(key, i18n)`
|
|
617
667
|
|
|
618
668
|
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.6",
|
|
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",
|
|
15
17
|
"test:component": "cypress run --component",
|
|
16
18
|
"release": "release-it"
|
|
17
19
|
},
|
|
@@ -208,6 +208,10 @@ function getCoverImageUrl(coverImage) {
|
|
|
208
208
|
if (coverImage.startsWith("http") || coverImage.startsWith("//")) {
|
|
209
209
|
return coverImage;
|
|
210
210
|
}
|
|
211
|
+
const base = withBase("/");
|
|
212
|
+
if (base && base !== "/" && coverImage.startsWith(base)) {
|
|
213
|
+
return coverImage;
|
|
214
|
+
}
|
|
211
215
|
const path = coverImage.startsWith("/")
|
|
212
216
|
? coverImage
|
|
213
217
|
: `/images/${coverImage}`;
|
|
@@ -151,6 +151,10 @@ function getCoverImageUrl(coverImage) {
|
|
|
151
151
|
if (coverImage.startsWith("http") || coverImage.startsWith("//")) {
|
|
152
152
|
return coverImage;
|
|
153
153
|
}
|
|
154
|
+
const base = withBase("/");
|
|
155
|
+
if (base && base !== "/" && coverImage.startsWith(base)) {
|
|
156
|
+
return coverImage;
|
|
157
|
+
}
|
|
154
158
|
const path = coverImage.startsWith("/")
|
|
155
159
|
? coverImage
|
|
156
160
|
: `/images/${coverImage}`;
|
|
@@ -187,7 +191,19 @@ const coverImage = computed(() => {
|
|
|
187
191
|
|
|
188
192
|
// Dynamic Recent Posts (excluding current page)
|
|
189
193
|
const calculatedRecentPosts = computed(() => {
|
|
190
|
-
|
|
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);
|
|
191
207
|
});
|
|
192
208
|
|
|
193
209
|
// Dynamic Hot Tags
|
|
@@ -67,6 +67,8 @@ const props = defineProps([
|
|
|
67
67
|
function resolveLink(link) {
|
|
68
68
|
if (!link) return "";
|
|
69
69
|
if (link.startsWith("http") || link.startsWith("//")) return link;
|
|
70
|
+
const base = withBase("/");
|
|
71
|
+
if (base && base !== "/" && link.startsWith(base)) return link;
|
|
70
72
|
return link.startsWith("/") ? withBase(link) : withBase("/" + link);
|
|
71
73
|
}
|
|
72
74
|
|
|
@@ -57,6 +57,8 @@ const props = defineProps({
|
|
|
57
57
|
function resolveUrl(url) {
|
|
58
58
|
if (!url) return "";
|
|
59
59
|
if (url.startsWith("http") || url.startsWith("//")) return url;
|
|
60
|
+
const base = withBase("/");
|
|
61
|
+
if (base && base !== "/" && url.startsWith(base)) return url;
|
|
60
62
|
return url.startsWith("/") ? withBase(url) : withBase("/" + url);
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -45,6 +45,8 @@ const props = defineProps({
|
|
|
45
45
|
function resolveUrl(url) {
|
|
46
46
|
if (!url) return "";
|
|
47
47
|
if (url.startsWith("http") || url.startsWith("//")) return url;
|
|
48
|
+
const base = withBase("/");
|
|
49
|
+
if (base && base !== "/" && url.startsWith(base)) return url;
|
|
48
50
|
return url.startsWith("/") ? withBase(url) : withBase("/" + url);
|
|
49
51
|
}
|
|
50
52
|
|
|
@@ -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
|
});
|
|
@@ -63,30 +63,28 @@ const resolveCardComponent = (variant) => {
|
|
|
63
63
|
|
|
64
64
|
const { page, site } = useData();
|
|
65
65
|
|
|
66
|
+
function resolveUrl(url) {
|
|
67
|
+
if (!url) return "";
|
|
68
|
+
if (url.startsWith("http") || url.startsWith("//")) return url;
|
|
69
|
+
const base = withBase("/");
|
|
70
|
+
if (base && base !== "/" && url.startsWith(base)) return url;
|
|
71
|
+
return url.startsWith("/") ? withBase(url) : withBase("/" + url);
|
|
72
|
+
}
|
|
73
|
+
|
|
66
74
|
function normalizeFeature(feature) {
|
|
67
75
|
return {
|
|
68
76
|
...feature,
|
|
69
|
-
image:
|
|
70
|
-
feature.image && feature.image.startsWith("/")
|
|
71
|
-
? withBase(feature.image)
|
|
72
|
-
: feature.image,
|
|
77
|
+
image: resolveUrl(feature.image),
|
|
73
78
|
link: feature.link
|
|
74
79
|
? {
|
|
75
80
|
...feature.link,
|
|
76
|
-
href:
|
|
77
|
-
feature.link.href && feature.link.href.startsWith("/")
|
|
78
|
-
? withBase(feature.link.href)
|
|
79
|
-
: feature.link.href,
|
|
81
|
+
href: resolveUrl(feature.link.href),
|
|
80
82
|
}
|
|
81
83
|
: undefined,
|
|
82
84
|
secondaryLink: feature.secondaryLink
|
|
83
85
|
? {
|
|
84
86
|
...feature.secondaryLink,
|
|
85
|
-
href:
|
|
86
|
-
feature.secondaryLink.href &&
|
|
87
|
-
feature.secondaryLink.href.startsWith("/")
|
|
88
|
-
? withBase(feature.secondaryLink.href)
|
|
89
|
-
: feature.secondaryLink.href,
|
|
87
|
+
href: resolveUrl(feature.secondaryLink.href),
|
|
90
88
|
}
|
|
91
89
|
: undefined,
|
|
92
90
|
};
|
|
@@ -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;
|
|
@@ -94,6 +94,8 @@ const activeIndex = ref(0);
|
|
|
94
94
|
function resolveUrl(url) {
|
|
95
95
|
if (!url) return "";
|
|
96
96
|
if (url.startsWith("http") || url.startsWith("//")) return url;
|
|
97
|
+
const base = withBase("/");
|
|
98
|
+
if (base && base !== "/" && url.startsWith(base)) return url;
|
|
97
99
|
return url.startsWith("/") ? withBase(url) : withBase("/" + url);
|
|
98
100
|
}
|
|
99
101
|
|
|
@@ -107,6 +107,8 @@ const props = defineProps({
|
|
|
107
107
|
function resolveUrl(url) {
|
|
108
108
|
if (!url) return "";
|
|
109
109
|
if (url.startsWith("http") || url.startsWith("//")) return url;
|
|
110
|
+
const base = withBase("/");
|
|
111
|
+
if (base && base !== "/" && url.startsWith(base)) return url;
|
|
110
112
|
return url.startsWith("/") ? withBase(url) : withBase("/" + url);
|
|
111
113
|
}
|
|
112
114
|
|
|
@@ -196,6 +196,8 @@ const lightboxIndex = ref(null);
|
|
|
196
196
|
function resolveUrl(url) {
|
|
197
197
|
if (!url) return "";
|
|
198
198
|
if (url.startsWith("http") || url.startsWith("//")) return url;
|
|
199
|
+
const base = withBase("/");
|
|
200
|
+
if (base && base !== "/" && url.startsWith(base)) return url;
|
|
199
201
|
return url.startsWith("/") ? withBase(url) : withBase("/" + url);
|
|
200
202
|
}
|
|
201
203
|
|
|
@@ -25,6 +25,8 @@ const logoRow = useTemplateRef("logo-row");
|
|
|
25
25
|
function resolveUrl(url) {
|
|
26
26
|
if (!url) return "";
|
|
27
27
|
if (url.startsWith("http") || url.startsWith("//")) return url;
|
|
28
|
+
const base = withBase("/");
|
|
29
|
+
if (base && base !== "/" && url.startsWith(base)) return url;
|
|
28
30
|
return url.startsWith("/") ? withBase(url) : withBase("/" + url);
|
|
29
31
|
}
|
|
30
32
|
|
|
@@ -239,7 +239,7 @@ const langs = computed(() => {
|
|
|
239
239
|
}
|
|
240
240
|
return Object.entries(locales).map(([id, locale]) => {
|
|
241
241
|
const currentPath = route.path;
|
|
242
|
-
const base = withBase("");
|
|
242
|
+
const base = withBase("/");
|
|
243
243
|
let pathRelativeToBase = currentPath;
|
|
244
244
|
if (base !== "/" && currentPath.startsWith(base)) {
|
|
245
245
|
pathRelativeToBase = currentPath.substring(base.length);
|
|
@@ -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,
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, it, beforeEach } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
normalizeTrackingUrl,
|
|
5
|
+
trackPageView,
|
|
6
|
+
trackEvent,
|
|
7
|
+
trackSiteSearch,
|
|
8
|
+
formatPrice,
|
|
9
|
+
t,
|
|
10
|
+
isActive,
|
|
11
|
+
} from "./helpers.js";
|
|
12
|
+
|
|
13
|
+
describe("normalizeTrackingUrl", () => {
|
|
14
|
+
it("strips trailing slashes from path", () => {
|
|
15
|
+
assert.equal(normalizeTrackingUrl("/explore/"), "/explore");
|
|
16
|
+
assert.equal(normalizeTrackingUrl("/explore"), "/explore");
|
|
17
|
+
assert.equal(normalizeTrackingUrl("/story/"), "/story");
|
|
18
|
+
assert.equal(normalizeTrackingUrl("/news_updates/"), "/news_updates");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("keeps single root slash as /", () => {
|
|
22
|
+
assert.equal(normalizeTrackingUrl("/"), "/");
|
|
23
|
+
assert.equal(normalizeTrackingUrl(""), "");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("removes volatile map coordinate parameters (x, y, z) and keeps template/zoom/datetime", () => {
|
|
27
|
+
assert.equal(
|
|
28
|
+
normalizeTrackingUrl("/explore/?x=15.0000&y=48.0000&z=4.0000"),
|
|
29
|
+
"/explore",
|
|
30
|
+
);
|
|
31
|
+
assert.equal(
|
|
32
|
+
normalizeTrackingUrl(
|
|
33
|
+
"/explore/?x=15.0000&y=48.0000&z=4.0000&template=expert&indicator=N11&datetime=2024-08-16&zoom=5",
|
|
34
|
+
),
|
|
35
|
+
"/explore?datetime=2024-08-16&indicator=N11&template=expert&zoom=5",
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("preserves semantic content query params and sorts them", () => {
|
|
40
|
+
assert.equal(
|
|
41
|
+
normalizeTrackingUrl("/story/?page=2&id=sea-ice-extent"),
|
|
42
|
+
"/story?id=sea-ice-extent&page=2",
|
|
43
|
+
);
|
|
44
|
+
assert.equal(
|
|
45
|
+
normalizeTrackingUrl("/story/?id=sea-ice-extent&page=2"),
|
|
46
|
+
"/story?id=sea-ice-extent&page=2",
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("normalizes full URLs", () => {
|
|
51
|
+
assert.equal(
|
|
52
|
+
normalizeTrackingUrl("https://eodash.eox.at/explore/"),
|
|
53
|
+
"https://eodash.eox.at/explore",
|
|
54
|
+
);
|
|
55
|
+
assert.equal(
|
|
56
|
+
normalizeTrackingUrl(
|
|
57
|
+
"https://eodash.eox.at/explore/?x=12&y=34&indicator=TEST",
|
|
58
|
+
),
|
|
59
|
+
"https://eodash.eox.at/explore?indicator=TEST",
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("trackEvent", () => {
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
globalThis._paq = [];
|
|
67
|
+
globalThis.window = globalThis;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("pushes array payload to _paq and normalizes path arguments", () => {
|
|
71
|
+
trackEvent(["Interaction", "Scroll Depth", "/explore/", 50]);
|
|
72
|
+
assert.deepEqual(globalThis._paq, [
|
|
73
|
+
["trackEvent", "Interaction", "Scroll Depth", "/explore", 50],
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("supports positional arguments", () => {
|
|
78
|
+
trackEvent("CTA", "/click-link/", "Button Text");
|
|
79
|
+
assert.deepEqual(globalThis._paq, [
|
|
80
|
+
["trackEvent", "CTA", "/click-link", "Button Text"],
|
|
81
|
+
]);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("trackPageView", () => {
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
globalThis._paq = [];
|
|
88
|
+
globalThis.window = globalThis;
|
|
89
|
+
globalThis.document = { title: "Default Title" };
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("normalizes URL and sets document title before tracking page view", () => {
|
|
93
|
+
trackPageView({
|
|
94
|
+
path: "/explore/?x=10&indicator=AIR",
|
|
95
|
+
title: "Explore Indicators",
|
|
96
|
+
});
|
|
97
|
+
assert.deepEqual(globalThis._paq, [
|
|
98
|
+
["setCustomUrl", "/explore?indicator=AIR"],
|
|
99
|
+
["setDocumentTitle", "Explore Indicators"],
|
|
100
|
+
["trackPageView"],
|
|
101
|
+
["enableLinkTracking"],
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("accepts router route object", () => {
|
|
106
|
+
trackPageView({
|
|
107
|
+
route: {
|
|
108
|
+
path: "/story/",
|
|
109
|
+
data: { title: "Climate Story" },
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
assert.deepEqual(globalThis._paq, [
|
|
113
|
+
["setCustomUrl", "/story"],
|
|
114
|
+
["setDocumentTitle", "Climate Story"],
|
|
115
|
+
["trackPageView"],
|
|
116
|
+
["enableLinkTracking"],
|
|
117
|
+
]);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("trackSiteSearch", () => {
|
|
122
|
+
beforeEach(() => {
|
|
123
|
+
globalThis._paq = [];
|
|
124
|
+
globalThis.window = globalThis;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("pushes trackSiteSearch event with keyword, category, and result count", () => {
|
|
128
|
+
trackSiteSearch("methane", "indicators", 5);
|
|
129
|
+
assert.deepEqual(globalThis._paq, [
|
|
130
|
+
["trackSiteSearch", "methane", "indicators", 5],
|
|
131
|
+
]);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("existing helpers", () => {
|
|
136
|
+
it("formats prices correctly", () => {
|
|
137
|
+
assert.equal(formatPrice(12000, "en"), "€ 12,000.-");
|
|
138
|
+
assert.equal(formatPrice(49.99, "de"), "€ 49,99");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("translates i18n keys with fallback", () => {
|
|
142
|
+
assert.equal(t("nav.home", { nav: { home: "Home Page" } }), "Home Page");
|
|
143
|
+
assert.equal(
|
|
144
|
+
t("nav.unknown", { nav: { home: "Home Page" } }),
|
|
145
|
+
"nav.unknown",
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("checks isActive links correctly", () => {
|
|
150
|
+
assert.equal(isActive({ link: "/explore/" }, "/explore"), true);
|
|
151
|
+
assert.equal(isActive({ link: "/explore" }, "/explore/"), true);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -19,7 +19,7 @@ export default {
|
|
|
19
19
|
const yaml = frontmatterMatch[1];
|
|
20
20
|
|
|
21
21
|
// 1. Match inline array or string style author
|
|
22
|
-
const authorMatch = yaml.match(/author
|
|
22
|
+
const authorMatch = yaml.match(/author:[ \t]+(.*?)\r?\n/);
|
|
23
23
|
if (authorMatch) {
|
|
24
24
|
const val = authorMatch[1].trim();
|
|
25
25
|
if (val.startsWith("[") && val.endsWith("]")) {
|
|
@@ -30,7 +30,7 @@ export default {
|
|
|
30
30
|
const clean = a.trim().replace(/['"]/g, "").toLowerCase();
|
|
31
31
|
if (clean) allAuthors.add(clean);
|
|
32
32
|
});
|
|
33
|
-
} else if (val) {
|
|
33
|
+
} else if (val && !val.startsWith("-")) {
|
|
34
34
|
const clean = val.replace(/['"]/g, "").toLowerCase();
|
|
35
35
|
if (clean) allAuthors.add(clean);
|
|
36
36
|
}
|
package/src/posts.data.js
CHANGED
|
@@ -62,6 +62,44 @@ function getExcerpt(src, maxLength = 200) {
|
|
|
62
62
|
return text;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
function resolvePostUrl(page) {
|
|
66
|
+
if (!page.url) return "";
|
|
67
|
+
const cleanUrls = global.VITEPRESS_CONFIG?.cleanUrls ?? true;
|
|
68
|
+
|
|
69
|
+
const relFile = page.url.replace(/^\//, "").replace(/\.html$/, "") + ".md";
|
|
70
|
+
|
|
71
|
+
// 1. Check VitePress rewrites map first if available
|
|
72
|
+
const rewritten = global.VITEPRESS_CONFIG?.rewrites?.map?.[relFile];
|
|
73
|
+
if (rewritten) {
|
|
74
|
+
return (
|
|
75
|
+
"/" +
|
|
76
|
+
rewritten
|
|
77
|
+
.replace(/(^|\/)index\.md$/, "$1")
|
|
78
|
+
.replace(/\.md$/, cleanUrls ? "" : ".html")
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 2. If it is in _posts/, compute fallback rewrite based on frontmatter date and slug
|
|
83
|
+
if (page.url.startsWith("/_posts/")) {
|
|
84
|
+
const baseName = page.url.replace(/^\/_posts\//, "").replace(/\.html$/, "");
|
|
85
|
+
const slug = baseName
|
|
86
|
+
.toLowerCase()
|
|
87
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
88
|
+
.replace(/^-+|-+$/g, "");
|
|
89
|
+
|
|
90
|
+
const dateStr = String(page.frontmatter?.date || "");
|
|
91
|
+
const dateMatch = dateStr.match(/^(\d{4})-(\d{2})/);
|
|
92
|
+
if (dateMatch) {
|
|
93
|
+
const [, year, month] = dateMatch;
|
|
94
|
+
return `/${year}/${month}/${slug}${cleanUrls ? "" : ".html"}`;
|
|
95
|
+
} else {
|
|
96
|
+
return `/blog/${slug}${cleanUrls ? "" : ".html"}`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return page.url;
|
|
101
|
+
}
|
|
102
|
+
|
|
65
103
|
export default createContentLoader("_posts/*.md", {
|
|
66
104
|
includeSrc: true,
|
|
67
105
|
transform(rawData) {
|
|
@@ -78,7 +116,7 @@ export default createContentLoader("_posts/*.md", {
|
|
|
78
116
|
|
|
79
117
|
return {
|
|
80
118
|
title: page.frontmatter.title || "",
|
|
81
|
-
url: page
|
|
119
|
+
url: resolvePostUrl(page),
|
|
82
120
|
date: page.frontmatter.date || "",
|
|
83
121
|
authors,
|
|
84
122
|
tags: page.frontmatter.tags || [],
|
package/src/style.css
CHANGED
|
@@ -92,6 +92,81 @@ body {
|
|
|
92
92
|
margin-block-end: 0 !important;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/* When a section follows a title (title plus section combination), eliminate start padding */
|
|
96
|
+
:is(
|
|
97
|
+
h1,
|
|
98
|
+
h2,
|
|
99
|
+
h3,
|
|
100
|
+
h4,
|
|
101
|
+
h5,
|
|
102
|
+
h6,
|
|
103
|
+
header,
|
|
104
|
+
.section-header-group,
|
|
105
|
+
[class*="section-title"]
|
|
106
|
+
)
|
|
107
|
+
+ section,
|
|
108
|
+
:is(
|
|
109
|
+
h1,
|
|
110
|
+
h2,
|
|
111
|
+
h3,
|
|
112
|
+
h4,
|
|
113
|
+
h5,
|
|
114
|
+
h6,
|
|
115
|
+
header,
|
|
116
|
+
.section-header-group,
|
|
117
|
+
[class*="section-title"]
|
|
118
|
+
)
|
|
119
|
+
+ :not(section, hr)
|
|
120
|
+
+ section,
|
|
121
|
+
:is(
|
|
122
|
+
h1,
|
|
123
|
+
h2,
|
|
124
|
+
h3,
|
|
125
|
+
h4,
|
|
126
|
+
h5,
|
|
127
|
+
h6,
|
|
128
|
+
header,
|
|
129
|
+
.section-header-group,
|
|
130
|
+
[class*="section-title"]
|
|
131
|
+
)
|
|
132
|
+
+ :not(section, hr)
|
|
133
|
+
+ :not(section, hr)
|
|
134
|
+
+ section,
|
|
135
|
+
:is(
|
|
136
|
+
h1,
|
|
137
|
+
h2,
|
|
138
|
+
h3,
|
|
139
|
+
h4,
|
|
140
|
+
h5,
|
|
141
|
+
h6,
|
|
142
|
+
header,
|
|
143
|
+
.section-header-group,
|
|
144
|
+
[class*="section-title"]
|
|
145
|
+
)
|
|
146
|
+
+ :not(section, hr)
|
|
147
|
+
+ :not(section, hr)
|
|
148
|
+
+ :not(section, hr)
|
|
149
|
+
+ section,
|
|
150
|
+
:is(
|
|
151
|
+
h1,
|
|
152
|
+
h2,
|
|
153
|
+
h3,
|
|
154
|
+
h4,
|
|
155
|
+
h5,
|
|
156
|
+
h6,
|
|
157
|
+
header,
|
|
158
|
+
.section-header-group,
|
|
159
|
+
[class*="section-title"]
|
|
160
|
+
)
|
|
161
|
+
+ :not(section, hr)
|
|
162
|
+
+ :not(section, hr)
|
|
163
|
+
+ :not(section, hr)
|
|
164
|
+
+ :not(section, hr)
|
|
165
|
+
+ section {
|
|
166
|
+
padding-block-start: 0 !important;
|
|
167
|
+
padding-top: 0 !important;
|
|
168
|
+
}
|
|
169
|
+
|
|
95
170
|
.VPHome .vp-doc a {
|
|
96
171
|
font-weight: 400;
|
|
97
172
|
color: inherit;
|
|
@@ -565,3 +640,68 @@ eox-feedback-button {
|
|
|
565
640
|
white-space: nowrap !important;
|
|
566
641
|
border-width: 0 !important;
|
|
567
642
|
}
|
|
643
|
+
|
|
644
|
+
/* ==========================================================================
|
|
645
|
+
Links in text hover underline
|
|
646
|
+
========================================================================== */
|
|
647
|
+
|
|
648
|
+
a.link {
|
|
649
|
+
text-decoration: none;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
:is(p, li, blockquote, figcaption, td, th, dd, dt, .card-body-text)
|
|
653
|
+
a:not(
|
|
654
|
+
.button,
|
|
655
|
+
[role="button"],
|
|
656
|
+
.chip,
|
|
657
|
+
.badge,
|
|
658
|
+
.circle,
|
|
659
|
+
.header-anchor,
|
|
660
|
+
.card-cover-link,
|
|
661
|
+
.grid-link,
|
|
662
|
+
.sidebar-tag,
|
|
663
|
+
.post-tag,
|
|
664
|
+
.clickable-chip,
|
|
665
|
+
.tag-link,
|
|
666
|
+
.social-link-btn,
|
|
667
|
+
.custom-cta-button,
|
|
668
|
+
:has(> img:only-child),
|
|
669
|
+
:has(> svg:only-child)
|
|
670
|
+
):hover,
|
|
671
|
+
a.link:hover,
|
|
672
|
+
.vp-doc
|
|
673
|
+
a:not(
|
|
674
|
+
.button,
|
|
675
|
+
[role="button"],
|
|
676
|
+
.header-anchor,
|
|
677
|
+
.card-cover-link,
|
|
678
|
+
.grid-link,
|
|
679
|
+
.sidebar-tag,
|
|
680
|
+
.post-tag,
|
|
681
|
+
.clickable-chip,
|
|
682
|
+
.tag-link,
|
|
683
|
+
.social-link-btn,
|
|
684
|
+
.custom-cta-button,
|
|
685
|
+
:has(> img:only-child),
|
|
686
|
+
:has(> svg:only-child)
|
|
687
|
+
):hover {
|
|
688
|
+
text-decoration: underline;
|
|
689
|
+
text-underline-offset: 2px;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/* Ensure navigation bars, dropdown menus, sidebars, and outline links do not get hover underline unless explicitly marked .link */
|
|
693
|
+
:is(
|
|
694
|
+
nav,
|
|
695
|
+
menu,
|
|
696
|
+
dialog,
|
|
697
|
+
.top-nav,
|
|
698
|
+
.social-links,
|
|
699
|
+
.VPSidebar,
|
|
700
|
+
.VPDocAsideOutline,
|
|
701
|
+
.VPLocalSearchBox
|
|
702
|
+
)
|
|
703
|
+
a:not(.link):hover,
|
|
704
|
+
menu a:hover,
|
|
705
|
+
menu li a:hover {
|
|
706
|
+
text-decoration: none !important;
|
|
707
|
+
}
|
package/src/vitepressConfig.mjs
CHANGED
|
@@ -212,7 +212,7 @@ export const generate = (brandConfig) => {
|
|
|
212
212
|
></span>`,
|
|
213
213
|
copyright:
|
|
214
214
|
brandConfig.theme?.footer?.copyright ||
|
|
215
|
-
'Copyright ©
|
|
215
|
+
'Copyright © 2008-present <a href="https://eox.at" target="_blank">EOX IT Services GmbH</a>',
|
|
216
216
|
},
|
|
217
217
|
},
|
|
218
218
|
title: brandConfig.meta?.title,
|