@eox/pages-theme-eox 1.9.0 → 2.0.0-alpha.10

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.
Files changed (53) hide show
  1. package/.gitlab-ci.yml +4 -1
  2. package/README.md +88 -11
  3. package/cypress/support/mocks/authors.data.js +14 -0
  4. package/cypress/support/mocks/helpers.js +65 -4
  5. package/cypress/support/mocks/vitepress.js +8 -1
  6. package/cypress.config.js +14 -0
  7. package/package.json +3 -1
  8. package/src/Layout.vue +69 -1
  9. package/src/authors.data.js +14 -0
  10. package/src/components/BlogList.vue +655 -0
  11. package/src/components/BlogPost.vue +472 -0
  12. package/src/components/CTASection.vue +18 -4
  13. package/src/components/FeatureCard/Index.vue +462 -31
  14. package/src/components/FeatureCard/Poster.vue +25 -3
  15. package/src/components/FeatureSection.cy.js +16 -0
  16. package/src/components/FeatureSection.vue +3 -8
  17. package/src/components/FeaturesGallery.vue +34 -2
  18. package/src/components/Footer.cy.js +52 -2
  19. package/src/components/Footer.vue +17 -5
  20. package/src/components/HeroSection.cy.js +20 -0
  21. package/src/components/HeroSection.vue +140 -25
  22. package/src/components/ImageCarousel.vue +11 -2
  23. package/src/components/ImageElement.vue +16 -4
  24. package/src/components/ImageGallery.vue +29 -4
  25. package/src/components/LogoSection.vue +26 -3
  26. package/src/components/MobileNavDropdown.vue +13 -3
  27. package/src/components/NavBar.cy.js +15 -0
  28. package/src/components/NavBar.vue +149 -21
  29. package/src/components/NavDropdown.vue +12 -2
  30. package/src/components/OpenPositions.vue +292 -0
  31. package/src/components/SectionHeader.cy.js +54 -0
  32. package/src/components/SectionHeader.vue +115 -0
  33. package/src/components/TagList.vue +311 -0
  34. package/src/components/TeamPage.vue +347 -0
  35. package/src/components/TestimonialsSection.cy.js +76 -0
  36. package/src/components/TestimonialsSection.vue +368 -0
  37. package/src/components/TextSection.cy.js +115 -0
  38. package/src/components/TextSection.vue +396 -0
  39. package/src/helpers.js +182 -37
  40. package/src/helpers.test.js +181 -0
  41. package/src/index.js +16 -0
  42. package/src/pages/author/[author].md +5 -0
  43. package/src/pages/author/[author].paths.js +60 -0
  44. package/src/pages/blog.md +5 -0
  45. package/src/pages/tag/[tag].md +5 -0
  46. package/src/pages/tag/[tag].paths.js +49 -0
  47. package/src/pages/tag.md +5 -0
  48. package/src/pages/team.md +5 -0
  49. package/src/posts.data.js +134 -0
  50. package/src/style.css +389 -8
  51. package/src/tagUtils.js +148 -0
  52. package/src/team.data.js +1 -1
  53. package/src/vitepressConfig.mjs +101 -1
@@ -50,6 +50,7 @@ const { cards, sectionTitle, background, variant, columns } = defineProps({
50
50
 
51
51
  const cardComponents = {
52
52
  default: FeatureCardDefault,
53
+ cover: FeatureCardDefault,
53
54
  poster: defineAsyncComponent(() => import("./FeatureCard/Poster.vue")),
54
55
  };
55
56
 
@@ -63,9 +64,38 @@ const resolveCardComponent = (variant) => {
63
64
 
64
65
  const { page, site } = useData();
65
66
 
67
+ function resolveUrl(url) {
68
+ if (!url) return "";
69
+ if (url.startsWith("http") || url.startsWith("//")) return url;
70
+ const base = withBase("/");
71
+ if (base && base !== "/" && url.startsWith(base)) return url;
72
+ return url.startsWith("/") ? withBase(url) : withBase("/" + url);
73
+ }
74
+
75
+ function normalizeFeature(feature) {
76
+ return {
77
+ ...feature,
78
+ content: feature.content || feature.description,
79
+ chips: feature.chips || feature.tags,
80
+ image: resolveUrl(feature.image),
81
+ link: feature.link
82
+ ? {
83
+ ...feature.link,
84
+ href: resolveUrl(feature.link.href),
85
+ }
86
+ : undefined,
87
+ secondaryLink: feature.secondaryLink
88
+ ? {
89
+ ...feature.secondaryLink,
90
+ href: resolveUrl(feature.secondaryLink.href),
91
+ }
92
+ : undefined,
93
+ };
94
+ }
95
+
66
96
  const featuresExcerpts =
67
97
  cards && cards.length
68
- ? cards
98
+ ? cards.map(normalizeFeature)
69
99
  : features
70
100
  .map((f) => {
71
101
  if (import.meta.env.SSR) {
@@ -89,7 +119,7 @@ const featuresExcerpts =
89
119
  }
90
120
  : undefined,
91
121
  link: {
92
- href: f.url,
122
+ href: withBase(f.url),
93
123
  text: "Read more",
94
124
  target: "_self",
95
125
  },
@@ -145,6 +175,7 @@ const gridCols = computed(() => {
145
175
  :metadata="feature.metadata"
146
176
  :link="feature.link"
147
177
  :secondaryLink="feature.secondaryLink"
178
+ :variant="variant"
148
179
  :class="`${cardBackground}`"
149
180
  />
150
181
  </div>
@@ -159,6 +190,7 @@ const gridCols = computed(() => {
159
190
  :metadata="feature.metadata"
160
191
  :link="feature.link"
161
192
  :secondaryLink="feature.secondaryLink"
193
+ :variant="variant"
162
194
  :class="`${cardBackground}`"
163
195
  />
164
196
  </div>
@@ -42,7 +42,7 @@ describe("<Footer />", () => {
42
42
 
43
43
  it("renders legal links", () => {
44
44
  cy.mount(Footer);
45
- cy.contains("a", "About").should("have.attr", "href", "/about");
45
+ cy.contains("a", "Imprint").should("have.attr", "href", "/about");
46
46
  cy.contains("a", "Terms & Conditions").should(
47
47
  "have.attr",
48
48
  "href",
@@ -51,6 +51,43 @@ describe("<Footer />", () => {
51
51
  cy.contains("a", "Privacy").should("have.attr", "href", "/privacy");
52
52
  });
53
53
 
54
+ it("renders uncategorized nav links under site title", () => {
55
+ cy.mount(Footer);
56
+ cy.contains("p.bold", "EOX Site").should("exist");
57
+ cy.contains("a", "Link 1").should("have.attr", "href", "/link1");
58
+ });
59
+
60
+ it("renders uncategorized nav links under brandConfig title when present", () => {
61
+ __setMockData({
62
+ theme: {
63
+ theme: {
64
+ brandConfig: {
65
+ title: "Custom Brand",
66
+ },
67
+ },
68
+ },
69
+ });
70
+ cy.mount(Footer);
71
+ cy.contains("p.bold", "Custom Brand").should("exist");
72
+ cy.contains("a", "Link 1").should("have.attr", "href", "/link1");
73
+ });
74
+
75
+ it("renders dropdown items under category title", () => {
76
+ __setMockData({
77
+ theme: {
78
+ nav: [
79
+ {
80
+ text: "Our Company",
81
+ items: [{ text: "About", link: "/about" }],
82
+ },
83
+ ],
84
+ },
85
+ });
86
+ cy.mount(Footer);
87
+ cy.contains("p.bold", "Our Company").should("exist");
88
+ cy.contains("a", "About").should("have.attr", "href", "/about");
89
+ });
90
+
54
91
  it("renders footer message with HTML content", () => {
55
92
  __setMockData({
56
93
  site: { title: "EOX Site" },
@@ -87,6 +124,7 @@ describe("<Footer />", () => {
87
124
  },
88
125
  i18n: {
89
126
  About: "Über uns",
127
+ Imprint: "Impressum",
90
128
  Legal: "Rechtliches",
91
129
  "Powered by": "Unterstützt von",
92
130
  },
@@ -96,7 +134,7 @@ describe("<Footer />", () => {
96
134
  },
97
135
  });
98
136
  cy.mount(Footer);
99
- cy.contains("Über uns").should("exist");
137
+ cy.contains("Impressum").should("exist");
100
138
  cy.contains("Rechtliches").should("exist");
101
139
  });
102
140
 
@@ -107,4 +145,16 @@ describe("<Footer />", () => {
107
145
  .should("have.attr", "href", "https://eox.at")
108
146
  .and("contain", "EOX");
109
147
  });
148
+
149
+ it("defines hover underline styles for footer links", () => {
150
+ cy.mount(Footer);
151
+ cy.get("footer .link").should("have.css", "text-decoration-line", "none");
152
+ cy.document().then((doc) => {
153
+ const styles = Array.from(doc.querySelectorAll("style"))
154
+ .map((s) => s.textContent)
155
+ .join("\n");
156
+ expect(styles).to.include("footer .link:hover");
157
+ expect(styles).to.include("text-decoration: underline");
158
+ });
159
+ });
110
160
  });
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <footer class="surface-container-low row center-align">
2
+ <footer class="surface-container-lowest row center-align">
3
3
  <div class="holder large-padding">
4
4
  <div class="large-space"></div>
5
5
  <div class="grid">
@@ -14,7 +14,10 @@
14
14
  <a
15
15
  v-if="theme.nav.find((i) => i.link && i.link.includes('contact'))"
16
16
  :href="
17
- theme.nav.find((i) => i.link && i.link.includes('contact')).link
17
+ withBase(
18
+ theme.nav.find((i) => i.link && i.link.includes('contact'))
19
+ .link,
20
+ )
18
21
  "
19
22
  class="button small border no-margin"
20
23
  style="color: var(--on-surface)"
@@ -33,7 +36,9 @@
33
36
  class="footer-column-item"
34
37
  v-if="theme.nav.filter((i) => !i.action && i.link).length"
35
38
  >
36
- <p class="bold">{{ t("About", theme.i18n) }}</p>
39
+ <p class="bold">
40
+ {{ theme.theme?.brandConfig?.title || site.title }}
41
+ </p>
37
42
  <p v-for="item in theme.nav.filter((i) => !i.action && i.link)">
38
43
  <a
39
44
  :href="withBase(item.link)"
@@ -69,7 +74,7 @@
69
74
  "
70
75
  target="_blank"
71
76
  class="link"
72
- >{{ t("About", theme.i18n) }}</a
77
+ >{{ t("Imprint", theme.i18n) }}</a
73
78
  >
74
79
  </p>
75
80
  <p v-for="(term, index) in termsLinks" :key="term.id || index">
@@ -89,7 +94,7 @@
89
94
  >
90
95
  </p>
91
96
  <p v-if="theme.theme.brandConfig?.analytics">
92
- <a href="/cookie-settings" class="link">{{
97
+ <a :href="withBase('/cookie-settings')" class="link">{{
93
98
  t("Cookie settings", theme.i18n)
94
99
  }}</a>
95
100
  </p>
@@ -133,10 +138,17 @@ footer .grid > div:not(:first-child) p,
133
138
  margin: 0 !important;
134
139
  }
135
140
 
141
+ footer a:not(.button),
136
142
  footer .link {
137
143
  text-decoration: none;
138
144
  }
139
145
 
146
+ footer a:not(.button):hover,
147
+ footer .link:hover {
148
+ text-decoration: underline;
149
+ text-underline-offset: 2px;
150
+ }
151
+
140
152
  .footer-columns {
141
153
  columns: 1;
142
154
  column-gap: 2rem;
@@ -36,6 +36,26 @@ describe("<HeroSection />", () => {
36
36
  cy.get("img.hero-image").should("have.attr", "src", "/image.png");
37
37
  });
38
38
 
39
+ it("renders with left alignment by default", () => {
40
+ cy.mount(HeroSection);
41
+ cy.get(".hero-container").should("have.css", "text-align", "left");
42
+ });
43
+
44
+ it("supports center alignment when configured", () => {
45
+ __setMockData({
46
+ frontmatter: {
47
+ hero: {
48
+ text: "Centered Hero",
49
+ align: "center",
50
+ },
51
+ },
52
+ });
53
+
54
+ cy.mount(HeroSection);
55
+ cy.get(".hero-container").should("have.class", "center-align");
56
+ cy.get(".hero-container").should("have.css", "text-align", "center");
57
+ });
58
+
39
59
  it("renders video when provided", () => {
40
60
  __setMockData({
41
61
  frontmatter: {
@@ -1,40 +1,45 @@
1
1
  <template>
2
- <header v-if="frontmatter.hero" class="primary primary-gradient-bg">
2
+ <header v-if="heroData" class="primary hero-gradient-bg">
3
3
  <div
4
- :class="`large-padding hero-container ${frontmatter.hero.image ? 'image' : ''}`"
4
+ :class="`large-padding hero-container ${heroData.image ? 'image' : ''} ${heroData.align === 'center' || heroData.align === 'center-align' ? 'center-align' : ''}`"
5
5
  >
6
6
  <iframe
7
- v-if="frontmatter.hero.image?.src?.includes('youtube')"
7
+ v-if="heroData.image?.src?.includes('youtube')"
8
8
  class="hero-image large-elevate small-round"
9
9
  style="grid-area: image; aspect-ratio: 16/9"
10
- :src="frontmatter.hero.image.src"
10
+ :src="heroData.image.src"
11
11
  frameborder="0"
12
12
  allowfullscreen
13
13
  />
14
14
  <video
15
- v-else-if="frontmatter.hero.video"
15
+ v-else-if="heroData.video"
16
16
  class="hero-image large-elevate small-round"
17
- :src="withBase(frontmatter.hero.video.src)"
17
+ :src="withBase(heroData.video.src)"
18
18
  style="grid-area: image; aspect-ratio: 16/9"
19
19
  autoplay
20
20
  muted
21
21
  loop
22
22
  />
23
23
  <img
24
- v-else-if="frontmatter.hero.image"
24
+ v-else-if="heroData.image"
25
25
  class="hero-image large-elevate small-round"
26
26
  style="grid-area: image"
27
- :src="withBase(frontmatter.hero.image.src)"
27
+ :src="withBase(heroData.image.src)"
28
+ :alt="heroData.image.alt || heroData.text"
28
29
  />
29
- <div class="title" style="grid-area: title; align-content: end">
30
+ <div class="title" style="grid-area: title">
30
31
  <h1 class="bold" style="font-size: clamp(2.25rem, 5vw, 3.3rem)">
31
- {{ frontmatter.hero.text }}
32
+ {{ heroData.text }}
32
33
  </h1>
33
- <p v-html="frontmatter.hero.tagline"></p>
34
+ <p v-if="heroData.tagline" v-html="heroData.tagline"></p>
34
35
  </div>
35
- <div style="grid-area: actions">
36
+ <div
37
+ v-if="heroData.actions && heroData.actions.length > 0"
38
+ style="grid-area: actions"
39
+ >
36
40
  <a
37
- v-for="(action, i) in frontmatter.hero.actions"
41
+ v-for="(action, i) in heroData.actions"
42
+ :key="i"
38
43
  :href="withBase(action.link)"
39
44
  :target="action.target"
40
45
  :rel="action.rel"
@@ -57,35 +62,135 @@
57
62
  "
58
63
  >
59
64
  <span>{{ action.text }}</span>
60
- <i v-if="action.theme !== 'alt'" class="mdi mdi-arrow-right"></i>
65
+ <i
66
+ v-if="action.icon"
67
+ :class="
68
+ action.icon.startsWith('mdi') ? `mdi ${action.icon}` : action.icon
69
+ "
70
+ ></i>
71
+ <i
72
+ v-else-if="action.theme !== 'alt' && action.icon !== false"
73
+ class="mdi mdi-arrow-right"
74
+ ></i>
61
75
  </a>
62
76
  </div>
63
77
  </div>
64
78
  <img
65
- v-if="frontmatter.hero.background"
79
+ v-if="heroData.background"
66
80
  class="background-image"
67
- :src="withBase(frontmatter.hero.background.src)"
81
+ :src="withBase(heroData.background.src)"
82
+ :alt="heroData.background.alt || ''"
68
83
  />
69
84
  </header>
70
85
  </template>
71
86
 
72
87
  <script setup>
88
+ import { computed } from "vue";
73
89
  import { useData, withBase } from "vitepress";
74
90
  import { trackEvent } from "../helpers";
75
- const { frontmatter } = useData();
91
+
92
+ const props = defineProps({
93
+ text: String,
94
+ title: String,
95
+ tagline: String,
96
+ actions: Array,
97
+ image: Object,
98
+ video: Object,
99
+ background: Object,
100
+ align: String,
101
+ });
102
+
103
+ const { frontmatter, params } = useData();
104
+
105
+ const heroData = computed(() => {
106
+ if (props.text || props.title || props.tagline) {
107
+ return {
108
+ text: props.text || props.title,
109
+ tagline: props.tagline,
110
+ actions: props.actions,
111
+ image: props.image,
112
+ video: props.video,
113
+ background: props.background,
114
+ align: props.align,
115
+ };
116
+ }
117
+
118
+ if (!frontmatter.value.hero || frontmatter.value.hero === false) {
119
+ return null;
120
+ }
121
+
122
+ // If hero is an object with custom properties
123
+ if (typeof frontmatter.value.hero === "object") {
124
+ return {
125
+ ...frontmatter.value.hero,
126
+ text:
127
+ frontmatter.value.hero.text ||
128
+ frontmatter.value.hero.title ||
129
+ frontmatter.value.title,
130
+ align: frontmatter.value.hero.align,
131
+ };
132
+ }
133
+
134
+ // If hero is true (boolean), provide intelligent layout defaults
135
+ if (frontmatter.value.hero === true) {
136
+ if (frontmatter.value.layout === "team") {
137
+ return {
138
+ text: frontmatter.value.title || "Our Team",
139
+ tagline:
140
+ frontmatter.value.tagline ||
141
+ "Meet the diverse team of geospatial experts, engineers, and visionaries driving EOX forward.",
142
+ };
143
+ }
144
+ if (frontmatter.value.layout === "blog") {
145
+ const tag = params?.value?.tag;
146
+ const author = params?.value?.author;
147
+ let blogTitle = frontmatter.value.title || "EOX Blog";
148
+ if (tag) {
149
+ blogTitle = `Tag: ${tag}`;
150
+ } else if (author) {
151
+ blogTitle = `Author: ${author}`;
152
+ }
153
+ return {
154
+ text: blogTitle,
155
+ tagline:
156
+ frontmatter.value.tagline ||
157
+ "Insights, updates, and deep dives from our geospatial experts.",
158
+ };
159
+ }
160
+ if (frontmatter.value.layout === "tags") {
161
+ return {
162
+ text: frontmatter.value.title || "Blog Tags",
163
+ tagline:
164
+ frontmatter.value.tagline ||
165
+ "Explore our blog posts by topics and technology pillars.",
166
+ };
167
+ }
168
+ return {
169
+ text: frontmatter.value.title || "",
170
+ tagline: frontmatter.value.tagline || frontmatter.value.description || "",
171
+ };
172
+ }
173
+
174
+ return null;
175
+ });
76
176
  </script>
77
177
 
78
- <style>
178
+ <style scoped>
79
179
  header {
180
+ position: relative;
80
181
  margin-top: calc(var(--vp-nav-height) * -1);
81
- padding-top: 10rem !important;
82
- padding-bottom: 10rem !important;
83
- height: 76dvh;
84
- max-height: 1000px;
182
+ padding-top: calc(var(--vp-nav-height) + 3.5rem) !important;
183
+ padding-bottom: 4.5rem !important;
184
+ padding-left: 0 !important;
185
+ padding-right: 0 !important;
186
+ min-height: 380px;
85
187
  }
86
188
  @media (max-width: 1024px) {
87
189
  header {
88
- padding: 0 1.5rem !important;
190
+ padding-top: calc(var(--vp-nav-height) + 2rem) !important;
191
+ padding-bottom: 3rem !important;
192
+ padding-left: 0 !important;
193
+ padding-right: 0 !important;
89
194
  }
90
195
  header h1.large {
91
196
  font-size: 3rem;
@@ -103,10 +208,12 @@ header > img.background-image {
103
208
  border-radius: 0 !important;
104
209
  }
105
210
  header > .hero-container {
211
+ width: 100%;
106
212
  max-width: 1200px;
107
213
  max-height: 80%;
108
214
  margin-left: auto;
109
215
  margin-right: auto;
216
+ box-sizing: border-box;
110
217
  display: grid;
111
218
  grid-gap: 2rem;
112
219
  grid-template-areas:
@@ -114,7 +221,7 @@ header > .hero-container {
114
221
  "title"
115
222
  "actions";
116
223
  grid-auto-rows: min-content;
117
- text-align: center;
224
+ text-align: left;
118
225
  z-index: 1;
119
226
  }
120
227
  @media (min-width: 768px) {
@@ -122,7 +229,7 @@ header > .hero-container {
122
229
  grid-template-areas:
123
230
  "title"
124
231
  "actions";
125
- text-align: center;
232
+ text-align: left;
126
233
  }
127
234
  header > .hero-container.image {
128
235
  grid-template-areas:
@@ -132,6 +239,14 @@ header > .hero-container {
132
239
  grid-template-columns: repeat(2, minmax(0, 1fr));
133
240
  }
134
241
  }
242
+ header > .hero-container.center-align {
243
+ text-align: center;
244
+ }
245
+ @media (min-width: 768px) {
246
+ header > .hero-container.center-align {
247
+ text-align: center;
248
+ }
249
+ }
135
250
  header > .hero-container > .hero-image {
136
251
  width: 100%;
137
252
  object-fit: cover;
@@ -79,6 +79,7 @@
79
79
 
80
80
  <script setup>
81
81
  import { computed, ref } from "vue";
82
+ import { withBase } from "vitepress";
82
83
 
83
84
  const props = defineProps({
84
85
  images: {
@@ -90,14 +91,22 @@ const props = defineProps({
90
91
  const container = ref(null);
91
92
  const activeIndex = ref(0);
92
93
 
94
+ function resolveUrl(url) {
95
+ if (!url) return "";
96
+ if (url.startsWith("http") || url.startsWith("//")) return url;
97
+ const base = withBase("/");
98
+ if (base && base !== "/" && url.startsWith(base)) return url;
99
+ return url.startsWith("/") ? withBase(url) : withBase("/" + url);
100
+ }
101
+
93
102
  const normalizedImages = computed(() => {
94
103
  if (!props.images) return [];
95
104
  return props.images.map((img) => {
96
105
  if (typeof img === "string") {
97
- return { src: img, alt: "", caption: "" };
106
+ return { src: resolveUrl(img), alt: "", caption: "" };
98
107
  }
99
108
  return {
100
- src: img.src || "",
109
+ src: resolveUrl(img.src),
101
110
  alt: img.alt || "",
102
111
  caption: img.caption || "",
103
112
  };
@@ -9,7 +9,7 @@
9
9
  >
10
10
  <!-- Background Image (Original/Right) -->
11
11
  <img
12
- :src="compare"
12
+ :src="resolvedCompare"
13
13
  :alt="alt || 'Image comparison background'"
14
14
  class="compare-img compare-bg"
15
15
  />
@@ -22,7 +22,7 @@
22
22
  }"
23
23
  >
24
24
  <img
25
- :src="src"
25
+ :src="resolvedSrc"
26
26
  :alt="alt || 'Image comparison foreground'"
27
27
  class="compare-img compare-fg"
28
28
  />
@@ -61,7 +61,7 @@
61
61
  <!-- Standard Image Mode -->
62
62
  <img
63
63
  v-else
64
- :src="src"
64
+ :src="resolvedSrc"
65
65
  :alt="alt || 'Post image'"
66
66
  :style="{ maxWidth: width || '100%' }"
67
67
  class="standard-img"
@@ -78,7 +78,8 @@
78
78
  </template>
79
79
 
80
80
  <script setup>
81
- import { ref } from "vue";
81
+ import { ref, computed } from "vue";
82
+ import { withBase } from "vitepress";
82
83
 
83
84
  const props = defineProps({
84
85
  src: {
@@ -103,6 +104,17 @@ const props = defineProps({
103
104
  },
104
105
  });
105
106
 
107
+ function resolveUrl(url) {
108
+ if (!url) return "";
109
+ if (url.startsWith("http") || url.startsWith("//")) return url;
110
+ const base = withBase("/");
111
+ if (base && base !== "/" && url.startsWith(base)) return url;
112
+ return url.startsWith("/") ? withBase(url) : withBase("/" + url);
113
+ }
114
+
115
+ const resolvedSrc = computed(() => resolveUrl(props.src));
116
+ const resolvedCompare = computed(() => resolveUrl(props.compare));
117
+
106
118
  const sliderValue = ref(50);
107
119
  </script>
108
120
 
@@ -170,6 +170,7 @@
170
170
 
171
171
  <script setup>
172
172
  import { computed, ref, onMounted, onUnmounted } from "vue";
173
+ import { withBase } from "vitepress";
173
174
 
174
175
  const props = defineProps({
175
176
  images: {
@@ -192,6 +193,14 @@ const props = defineProps({
192
193
 
193
194
  const lightboxIndex = ref(null);
194
195
 
196
+ function resolveUrl(url) {
197
+ if (!url) return "";
198
+ if (url.startsWith("http") || url.startsWith("//")) return url;
199
+ const base = withBase("/");
200
+ if (base && base !== "/" && url.startsWith(base)) return url;
201
+ return url.startsWith("/") ? withBase(url) : withBase("/" + url);
202
+ }
203
+
195
204
  const layoutMode = computed(() => {
196
205
  // If explicitly 'grid', or if we detect link in image objects, default to grid
197
206
  if (props.layout === "grid") return "grid";
@@ -211,13 +220,23 @@ const normalizedImages = computed(() => {
211
220
  if (Array.isArray(props.images)) {
212
221
  return props.images.map((img) => {
213
222
  if (typeof img === "string") {
214
- return { src: img, alt: "", caption: "", link: "", width: "" };
223
+ return {
224
+ src: resolveUrl(img),
225
+ alt: "",
226
+ caption: "",
227
+ link: "",
228
+ width: "",
229
+ };
215
230
  }
216
231
  return {
217
- src: img.src || "",
232
+ src: resolveUrl(img.src),
218
233
  alt: img.alt || "",
219
234
  caption: img.caption || "",
220
- link: img.link || "",
235
+ link: img.link
236
+ ? img.link.startsWith("/")
237
+ ? withBase(img.link)
238
+ : img.link
239
+ : "",
221
240
  width: img.width || "",
222
241
  };
223
242
  });
@@ -227,7 +246,13 @@ const normalizedImages = computed(() => {
227
246
  .split(",")
228
247
  .map((img) => img.trim())
229
248
  .filter(Boolean)
230
- .map((src) => ({ src, alt: "", caption: "", link: "", width: "" }));
249
+ .map((src) => ({
250
+ src: resolveUrl(src),
251
+ alt: "",
252
+ caption: "",
253
+ link: "",
254
+ width: "",
255
+ }));
231
256
  }
232
257
  return [];
233
258
  });