@eox/pages-theme-eox 2.0.0-alpha.7 → 2.0.0-alpha.9

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/README.md CHANGED
@@ -21,45 +21,6 @@ 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
-
63
24
  ### Navigation Bar
64
25
 
65
26
  The Navigation Bar follows the [Default Theme Nav](https://vitepress.dev/reference/default-theme-nav) structure with the following differences:
@@ -184,26 +145,40 @@ The theme provides several built-in components and layout features. Some are dri
184
145
 
185
146
  ### Hero Section
186
147
 
187
- To add a Hero section to the top of a page, use the `hero` frontmatter object.
148
+ To add a Hero section to the top of a page, use the `hero` frontmatter object. Heros feature a dark gradient transitioning from **DARK (top) to LIGHT (bottom)** (`.hero-gradient-bg`), allowing the top navigation bar to seamlessly sit transparently over the hero. On the landing page, the hero is larger and incorporates an isometric line pattern.
188
149
 
189
150
  ```yaml
190
151
  ---
191
152
  hero:
192
- title: "Welcome to My Site"
193
- tagline: "Building the future of things"
153
+ text: "Welcome to My Site" # or title: "Welcome to My Site"
154
+ tagline: "Building the future of geospatial engineering"
194
155
  image:
195
156
  src: "/hero-image.png"
196
157
  alt: "Hero Image"
197
158
  actions:
198
159
  - text: "Get Started"
199
160
  link: "/start"
200
- type: "primary"
161
+ theme: "brand" # "brand" (primary surface), "secondary", or "alt" (bordered)
201
162
  - text: "Learn More"
202
163
  link: "/about"
203
- type: "secondary"
164
+ theme: "secondary"
204
165
  ---
205
166
  ```
206
167
 
168
+ #### Hero on Aggregator Pages (`blog`, `team`, `tags`)
169
+
170
+ Pages using aggregator layouts (`layout: team`, `layout: blog`, or `layout: tags`) support optional heroes:
171
+
172
+ - **Without Hero (when `hero` is omitted or `hero: false`)**: The page renders a standard in-page header title beneath the solid top navigation bar.
173
+ - **With Hero (`hero: true` or custom `hero: { text, tagline, ... }`)**: The page renders the full dark hero with the `#000C14` → `#004170` gradient spanning seamlessly behind the transparent navigation bar. Setting `hero: true` automatically supplies appropriate layout titles (including dynamic `Tag: <tag>` and `Author: <author>` for filtered blog listings). Custom text, taglines, images, and actions can also be provided via the `hero` frontmatter object.
174
+
175
+ #### Gradient Design Standards
176
+
177
+ The theme standardizes two distinct gradient directions for dark visual elements using `#000C14` and `#004170`:
178
+
179
+ - **Hero Gradient (`.hero-gradient-bg`)**: Always transitions from **DARK (top) to LIGHT (bottom)**, running from `#000C14` (top) to `#004170` (bottom). Used for the landing page hero and all sub-page heroes. This anchors the fixed header area with dark contrast under navigation links while flowing naturally into lighter content below.
180
+ - **Dark Sections Gradient (`.primary-gradient-bg` / `.dark-section-gradient-bg`)**: Used in intermediate dark page sections further down the page (such as `<CTASection dark>`, feature highlights, or dark showcase galleries). Always transitions from **LIGHT (top) to DARK (bottom)**, running from `#004170` (top) to `#000C14` (bottom).
181
+
207
182
  ### Feature Section
208
183
 
209
184
  Use the `<FeatureSection>` component to highlight specific features.
@@ -262,7 +237,7 @@ A flexible pricing table component.
262
237
 
263
238
  #### CTA Section
264
239
 
265
- A Call-to-Action section typically placed at the bottom of a page to encourage user engagement.
240
+ A Call-to-Action section typically placed at the bottom of a page to encourage user engagement. By default, it renders with a clean white (`surface-container-lowest`) background, or a dark gradient when the `dark` prop is passed.
266
241
 
267
242
  ```html
268
243
  <CTASection
@@ -373,7 +348,7 @@ A standard content card with an icon or image, title, optional chips, metadata,
373
348
  section-title="All Features"
374
349
  variant="default"
375
350
  columns="3/2/1"
376
- background="surface-container-low"
351
+ background="surface-container-lowest"
377
352
  :cards="[
378
353
  {
379
354
  id: 1,
@@ -48,9 +48,31 @@ window.__helpersMock = {
48
48
  }
49
49
  },
50
50
  isActive: (item, path) => {
51
- if (item.link === path) return true;
52
- if (item.items) {
53
- return item.items.some((sub) => window.__helpersMock.isActive(sub, path));
51
+ if (!item || !path) return false;
52
+ if (item.activeMatch) {
53
+ try {
54
+ return new RegExp(item.activeMatch).test(path);
55
+ } catch (e) {
56
+ return false;
57
+ }
58
+ }
59
+ if (
60
+ item.items &&
61
+ item.items.some((sub) => window.__helpersMock.isActive(sub, path))
62
+ ) {
63
+ return true;
64
+ }
65
+ if (item.link) {
66
+ const normalize = (p) =>
67
+ decodeURI(p)
68
+ .replace(/[?#].*$/, "")
69
+ .replace(/\.html$/, "")
70
+ .replace(/\/$/, "");
71
+ const linkPath = normalize(item.link);
72
+ const currentPath = normalize(path);
73
+ if (linkPath === currentPath) return true;
74
+ if (linkPath === "") return currentPath === "";
75
+ return currentPath === linkPath || currentPath.startsWith(linkPath + "/");
54
76
  }
55
77
  return false;
56
78
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eox/pages-theme-eox",
3
- "version": "2.0.0-alpha.7",
3
+ "version": "2.0.0-alpha.9",
4
4
  "type": "module",
5
5
  "description": "Vitepress Theme with EOX branding",
6
6
  "main": "src/index.js",
@@ -13,7 +13,7 @@
13
13
  "format": "npx prettier --write .",
14
14
  "format:check": "npx prettier --check .",
15
15
  "test": "npm run test:unit && npm run test:component",
16
- "test:unit": "node --test src/helpers.test.js src/llms/llms.test.js",
16
+ "test:unit": "node --test src/helpers.test.js",
17
17
  "test:component": "cypress run --component",
18
18
  "release": "release-it"
19
19
  },
@@ -21,8 +21,7 @@
21
21
  "@eox/eslint-config": "^2.0.0",
22
22
  "@eox/feedback": "^1.1.1",
23
23
  "@eox/ui": "^1.0.1",
24
- "vitepress": "^1.6.3",
25
- "vitepress-plugin-llms": "^1.13.5"
24
+ "vitepress": "^1.6.3"
26
25
  },
27
26
  "devDependencies": {
28
27
  "@release-it/conventional-changelog": "^10.0.6",
package/src/Layout.vue CHANGED
@@ -1,10 +1,11 @@
1
1
  <template>
2
2
  <div
3
3
  v-if="frontmatter.layout === 'blog'"
4
- class="layout-blog min-h-screen flex flex-col"
4
+ class="Layout layout-blog min-h-screen flex flex-col"
5
5
  >
6
6
  <NavBar />
7
7
  <NewsBanner v-if="theme.newsBanner" :content="theme.newsBanner" />
8
+ <HeroSection />
8
9
  <main class="flex-grow">
9
10
  <BlogList />
10
11
  </main>
@@ -13,10 +14,11 @@
13
14
  </div>
14
15
  <div
15
16
  v-else-if="frontmatter.layout === 'tags'"
16
- class="layout-tags min-h-screen flex flex-col"
17
+ class="Layout layout-tags min-h-screen flex flex-col"
17
18
  >
18
19
  <NavBar />
19
20
  <NewsBanner v-if="theme.newsBanner" :content="theme.newsBanner" />
21
+ <HeroSection />
20
22
  <main class="flex-grow">
21
23
  <TagList />
22
24
  </main>
@@ -25,10 +27,11 @@
25
27
  </div>
26
28
  <div
27
29
  v-else-if="frontmatter.layout === 'team'"
28
- class="layout-team min-h-screen flex flex-col"
30
+ class="Layout layout-team min-h-screen flex flex-col"
29
31
  >
30
32
  <NavBar />
31
33
  <NewsBanner v-if="theme.newsBanner" :content="theme.newsBanner" />
34
+ <HeroSection />
32
35
  <main class="flex-grow">
33
36
  <TeamPage />
34
37
  </main>
@@ -1,7 +1,10 @@
1
1
  <template>
2
- <div class="blog-landing max-w-[1200px] mx-auto px-6 md:px-8">
3
- <!-- Page Header -->
4
- <div class="blog-landing-header" role="banner">
2
+ <div
3
+ class="blog-landing max-w-[1200px] mx-auto px-6 md:px-8"
4
+ :class="{ 'has-hero': Boolean(frontmatter.hero) }"
5
+ >
6
+ <!-- Page Header (shown only when hero is not present) -->
7
+ <div v-if="!frontmatter.hero" class="blog-landing-header" role="banner">
5
8
  <div class="flex flex-col md:flex-row md:items-end justify-between gap-6">
6
9
  <div>
7
10
  <h1 class="blog-landing-title">EOX Blog</h1>
@@ -10,28 +13,28 @@
10
13
  </p>
11
14
  </div>
12
15
  </div>
16
+ </div>
13
17
 
14
- <!-- Active Filter Banner -->
15
- <div v-if="activeTag || activeAuthor" class="active-filter-banner">
16
- <span class="filter-label">Active filter:</span>
17
- <span
18
- v-if="activeTag"
19
- :style="getTagStyle(activeTag)"
20
- class="active-filter-tag shadow-sm inline-flex items-center gap-1.5"
21
- >
22
- {{ activeTag }}
23
- </span>
24
- <span
25
- v-if="activeAuthor"
26
- class="active-filter-author shadow-sm inline-flex items-center gap-1.5"
27
- >
28
- Author: {{ getAuthorName(activeAuthor) }}
29
- </span>
30
- <button @click="clearFilters" class="clear-filter-btn">
31
- <i class="mdi mdi-close-circle"></i>
32
- <span>Clear Filter</span>
33
- </button>
34
- </div>
18
+ <!-- Active Filter Banner -->
19
+ <div v-if="activeTag || activeAuthor" class="active-filter-banner mb-6">
20
+ <span class="filter-label">Active filter:</span>
21
+ <span
22
+ v-if="activeTag"
23
+ :style="getTagStyle(activeTag)"
24
+ class="active-filter-tag shadow-sm inline-flex items-center gap-1.5"
25
+ >
26
+ {{ activeTag }}
27
+ </span>
28
+ <span
29
+ v-if="activeAuthor"
30
+ class="active-filter-author shadow-sm inline-flex items-center gap-1.5"
31
+ >
32
+ Author: {{ getAuthorName(activeAuthor) }}
33
+ </span>
34
+ <button @click="clearFilters" class="clear-filter-btn">
35
+ <i class="mdi mdi-close-circle"></i>
36
+ <span>Clear Filter</span>
37
+ </button>
35
38
  </div>
36
39
 
37
40
  <!-- Grid of Blog Posts -->
@@ -195,7 +198,7 @@ const calculatedHotTags = computed(() => {
195
198
  });
196
199
 
197
200
  const router = useRouter();
198
- const { params } = useData();
201
+ const { params, frontmatter } = useData();
199
202
 
200
203
  const activeTag = ref("");
201
204
  const activeAuthor = ref("");
@@ -345,6 +348,10 @@ function clearFilters() {
345
348
  padding-bottom: 48px !important;
346
349
  }
347
350
 
351
+ .blog-landing.has-hero {
352
+ padding-top: 2rem !important;
353
+ }
354
+
348
355
  @media (min-width: 640px) {
349
356
  .blog-landing {
350
357
  padding-left: 1.5rem !important;
@@ -352,6 +359,9 @@ function clearFilters() {
352
359
  padding-top: calc(var(--vp-nav-height) + 40px) !important;
353
360
  padding-bottom: 64px !important;
354
361
  }
362
+ .blog-landing.has-hero {
363
+ padding-top: 2.5rem !important;
364
+ }
355
365
  }
356
366
 
357
367
  @media (min-width: 960px) {
@@ -361,6 +371,9 @@ function clearFilters() {
361
371
  padding-top: calc(var(--vp-nav-height) + 58px) !important;
362
372
  padding-bottom: 96px !important;
363
373
  }
374
+ .blog-landing.has-hero {
375
+ padding-top: 3rem !important;
376
+ }
364
377
  }
365
378
 
366
379
  .blog-landing-header {
@@ -4,7 +4,7 @@
4
4
  :class="
5
5
  dark !== undefined
6
6
  ? 'primary primary-gradient-bg'
7
- : 'surface-container-low'
7
+ : 'surface-container-lowest'
8
8
  "
9
9
  >
10
10
  <div class="large-space"></div>
@@ -150,7 +150,12 @@ function formatDate(dateString) {
150
150
  ]"
151
151
  >
152
152
  <!-- Cover Image for cover/blog/rich cards -->
153
- <a v-if="isCoverLayout" :href="resolvedLinkHref" class="card-cover-link">
153
+ <a
154
+ v-if="isCoverLayout && resolvedLinkHref"
155
+ :href="resolvedLinkHref"
156
+ :target="link?.target ?? '_blank'"
157
+ class="card-cover-link"
158
+ >
154
159
  <img
155
160
  :src="resolvedImage"
156
161
  alt=""
@@ -158,6 +163,14 @@ function formatDate(dateString) {
158
163
  loading="lazy"
159
164
  />
160
165
  </a>
166
+ <div v-else-if="isCoverLayout" class="card-cover-link">
167
+ <img
168
+ :src="resolvedImage"
169
+ alt=""
170
+ class="card-cover-image"
171
+ loading="lazy"
172
+ />
173
+ </div>
161
174
 
162
175
  <div class="card-content-wrapper">
163
176
  <i
@@ -532,6 +545,8 @@ function formatDate(dateString) {
532
545
  }
533
546
 
534
547
  /* card title overrides to make sure it respects the 20px request */
548
+ h5.small.card-title-text,
549
+ .card-title-text,
535
550
  .VPHome .vp-doc h5.small.card-title-text {
536
551
  font-size: 20px !important;
537
552
  line-height: 1.4 !important;
@@ -120,16 +120,11 @@ const props = defineProps([
120
120
  }
121
121
  .feature-section.landing .holder {
122
122
  padding: 32px 0;
123
- background: var(--surface-container-low);
123
+ background: var(--surface-container-lowest, #ffffff);
124
124
  }
125
125
  .feature-section.dark .holder {
126
- background: var(--primary);
127
- background: radial-gradient(
128
- 74.48% 130.95% at 50% -12.27%,
129
- hsl(from var(--primary) h s calc(l + 7)) 0%,
130
- var(--primary) 45%,
131
- hsl(from var(--primary) h s calc(l - 20)) 100%
132
- );
126
+ background: #000c14;
127
+ background: linear-gradient(180deg, var(--primary, #004170) 0%, #000c14 100%);
133
128
  color: #fff;
134
129
  }
135
130
  .feature-section nav {
@@ -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
 
@@ -74,6 +75,8 @@ function resolveUrl(url) {
74
75
  function normalizeFeature(feature) {
75
76
  return {
76
77
  ...feature,
78
+ content: feature.content || feature.description,
79
+ chips: feature.chips || feature.tags,
77
80
  image: resolveUrl(feature.image),
78
81
  link: feature.link
79
82
  ? {
@@ -172,6 +175,7 @@ const gridCols = computed(() => {
172
175
  :metadata="feature.metadata"
173
176
  :link="feature.link"
174
177
  :secondaryLink="feature.secondaryLink"
178
+ :variant="variant"
175
179
  :class="`${cardBackground}`"
176
180
  />
177
181
  </div>
@@ -186,6 +190,7 @@ const gridCols = computed(() => {
186
190
  :metadata="feature.metadata"
187
191
  :link="feature.link"
188
192
  :secondaryLink="feature.secondaryLink"
193
+ :variant="variant"
189
194
  :class="`${cardBackground}`"
190
195
  />
191
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
 
@@ -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">
@@ -36,7 +36,9 @@
36
36
  class="footer-column-item"
37
37
  v-if="theme.nav.filter((i) => !i.action && i.link).length"
38
38
  >
39
- <p class="bold">{{ t("About", theme.i18n) }}</p>
39
+ <p class="bold">
40
+ {{ theme.theme?.brandConfig?.title || site.title }}
41
+ </p>
40
42
  <p v-for="item in theme.nav.filter((i) => !i.action && i.link)">
41
43
  <a
42
44
  :href="withBase(item.link)"
@@ -72,7 +74,7 @@
72
74
  "
73
75
  target="_blank"
74
76
  class="link"
75
- >{{ t("About", theme.i18n) }}</a
77
+ >{{ t("Imprint", theme.i18n) }}</a
76
78
  >
77
79
  </p>
78
80
  <p v-for="(term, index) in termsLinks" :key="term.id || index">
@@ -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' : ''}`"
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
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"
@@ -62,17 +67,100 @@
62
67
  </div>
63
68
  </div>
64
69
  <img
65
- v-if="frontmatter.hero.background"
70
+ v-if="heroData.background"
66
71
  class="background-image"
67
- :src="withBase(frontmatter.hero.background.src)"
72
+ :src="withBase(heroData.background.src)"
73
+ :alt="heroData.background.alt || ''"
68
74
  />
69
75
  </header>
70
76
  </template>
71
77
 
72
78
  <script setup>
79
+ import { computed } from "vue";
73
80
  import { useData, withBase } from "vitepress";
74
81
  import { trackEvent } from "../helpers";
75
- const { frontmatter } = useData();
82
+
83
+ const props = defineProps({
84
+ text: String,
85
+ title: String,
86
+ tagline: String,
87
+ actions: Array,
88
+ image: Object,
89
+ video: Object,
90
+ background: Object,
91
+ });
92
+
93
+ const { frontmatter, params } = useData();
94
+
95
+ const heroData = computed(() => {
96
+ if (props.text || props.title || props.tagline) {
97
+ return {
98
+ text: props.text || props.title,
99
+ tagline: props.tagline,
100
+ actions: props.actions,
101
+ image: props.image,
102
+ video: props.video,
103
+ background: props.background,
104
+ };
105
+ }
106
+
107
+ if (!frontmatter.value.hero || frontmatter.value.hero === false) {
108
+ return null;
109
+ }
110
+
111
+ // If hero is an object with custom properties
112
+ if (typeof frontmatter.value.hero === "object") {
113
+ return {
114
+ ...frontmatter.value.hero,
115
+ text:
116
+ frontmatter.value.hero.text ||
117
+ frontmatter.value.hero.title ||
118
+ frontmatter.value.title,
119
+ };
120
+ }
121
+
122
+ // If hero is true (boolean), provide intelligent layout defaults
123
+ if (frontmatter.value.hero === true) {
124
+ if (frontmatter.value.layout === "team") {
125
+ return {
126
+ text: frontmatter.value.title || "Our Team",
127
+ tagline:
128
+ frontmatter.value.tagline ||
129
+ "Meet the diverse team of geospatial experts, engineers, and visionaries driving EOX forward.",
130
+ };
131
+ }
132
+ if (frontmatter.value.layout === "blog") {
133
+ const tag = params?.value?.tag;
134
+ const author = params?.value?.author;
135
+ let blogTitle = frontmatter.value.title || "EOX Blog";
136
+ if (tag) {
137
+ blogTitle = `Tag: ${tag}`;
138
+ } else if (author) {
139
+ blogTitle = `Author: ${author}`;
140
+ }
141
+ return {
142
+ text: blogTitle,
143
+ tagline:
144
+ frontmatter.value.tagline ||
145
+ "Insights, updates, and deep dives from our geospatial experts.",
146
+ };
147
+ }
148
+ if (frontmatter.value.layout === "tags") {
149
+ return {
150
+ text: frontmatter.value.title || "Blog Tags",
151
+ tagline:
152
+ frontmatter.value.tagline ||
153
+ "Explore our blog posts by topics and technology pillars.",
154
+ };
155
+ }
156
+ return {
157
+ text: frontmatter.value.title || "",
158
+ tagline: frontmatter.value.tagline || frontmatter.value.description || "",
159
+ };
160
+ }
161
+
162
+ return null;
163
+ });
76
164
  </script>
77
165
 
78
166
  <style scoped>