@f5-sales-demo/docs-theme 4.11.5 → 4.11.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.
@@ -1,47 +1,23 @@
1
1
  ---
2
2
  import Default from '@astrojs/starlight/components/Footer.astro';
3
- import Icon from './Icon.astro';
4
3
  ---
5
4
 
6
5
  <Default {...Astro.props}><slot /></Default>
7
6
 
8
- {process.env.DOCS_PROFILE !== 'canonical-provider' && <div class="social-links">
9
- <a href="https://www.facebook.com/f5incorporated" target="_blank" rel="noopener noreferrer" aria-label="Facebook">
10
- <Icon name="hashicorp-flight:facebook-color" size="1.5rem" />
11
- </a>
12
- <a href="https://x.com/F5" target="_blank" rel="noopener noreferrer" aria-label="X">
13
- <Icon name="hashicorp-flight:twitter-x" size="1.5rem" />
14
- </a>
15
- <a href="https://www.linkedin.com/company/f5/" target="_blank" rel="noopener noreferrer" aria-label="LinkedIn">
16
- <Icon name="hashicorp-flight:linkedin-color" size="1.5rem" />
17
- </a>
18
- <a href="https://www.instagram.com/f5.global/" target="_blank" rel="noopener noreferrer" aria-label="Instagram">
19
- <Icon name="hashicorp-flight:instagram-color" size="1.5rem" />
20
- </a>
21
- <a href="https://www.youtube.com/user/f5networksinc" target="_blank" rel="noopener noreferrer" aria-label="YouTube">
22
- <Icon name="hashicorp-flight:youtube-color" size="1.5rem" />
23
- </a>
24
- </div>}
7
+ {process.env.DOCS_PROFILE !== 'canonical-provider' && <nav class="project-links" aria-label="Project information">
8
+ <a href="https://f5-sales-demo.github.io/en/ecosystem/">Ecosystem directory</a>
9
+ <a href="https://github.com/f5-sales-demo">Project sources</a>
10
+ <a href="https://docs.cloud.f5.com/">Official F5 documentation</a>
11
+ </nav>}
25
12
 
26
13
  <style>
27
- .social-links {
14
+ .project-links {
28
15
  display: flex;
29
- justify-content: flex-end;
30
- align-items: center;
31
- gap: 1.25rem;
32
- padding: 1rem 0 0.5rem;
33
- }
34
-
35
- .social-links a {
36
- display: inline-flex;
37
- transition: opacity 0.2s ease;
38
- }
39
-
40
- .social-links a:hover {
41
- opacity: 0.7;
42
- }
43
-
44
- .social-links :global(svg) {
45
- display: block;
16
+ flex-wrap: wrap;
17
+ gap: 0.5rem 1.5rem;
18
+ border-top: 1px solid var(--sl-color-gray-5);
19
+ margin-top: 2rem;
20
+ padding-block: 1rem;
46
21
  }
22
+ .project-links a { padding-block: 0.5rem; }
47
23
  </style>
@@ -0,0 +1,10 @@
1
+ ---
2
+ import { pageTitle } from '../src/utils/page-title';
3
+
4
+ const route = Astro.locals.starlightRoute;
5
+ const head = route.head.map((entry) => entry.tag === 'title'
6
+ ? { ...entry, content: pageTitle(route.entry.data.title, route.siteTitle, entry.content || '') }
7
+ : entry);
8
+ ---
9
+
10
+ {head.map(({ tag: Tag, attrs, content }) => <Tag {...attrs} set:html={content} />)}
@@ -18,7 +18,7 @@ if (attributes.href && localeSlug) {
18
18
 
19
19
  <div class:list={['sl-link-card', { 'has-icon': !!icon }]}>
20
20
  {icon && (
21
- <span class="card-icon">
21
+ <span class="card-icon" aria-hidden="true">
22
22
  <Icon name={icon} />
23
23
  </span>
24
24
  )}
@@ -28,13 +28,13 @@ if (attributes.href && localeSlug) {
28
28
  </a>
29
29
  {description && <span class="description" set:html={description} />}
30
30
  </span>
31
- <Icon name="right-arrow" size="1.333em" class="icon rtl:flip" />
31
+ <span aria-hidden="true"><Icon name="right-arrow" size="1.333em" class="icon rtl:flip" /></span>
32
32
  </div>
33
33
 
34
34
  <style>
35
35
  .sl-link-card {
36
36
  display: grid;
37
- grid-template-columns: 1fr auto;
37
+ grid-template-columns: minmax(0, 1fr) auto;
38
38
  gap: 0.5rem;
39
39
  border: 1px solid var(--sl-color-gray-5);
40
40
  border-radius: 0.5rem;
@@ -46,7 +46,7 @@ if (attributes.href && localeSlug) {
46
46
  }
47
47
 
48
48
  .sl-link-card.has-icon {
49
- grid-template-columns: auto 1fr auto;
49
+ grid-template-columns: auto minmax(0, 1fr) auto;
50
50
  }
51
51
 
52
52
  a {
@@ -62,7 +62,10 @@ if (attributes.href && localeSlug) {
62
62
  }
63
63
 
64
64
  .stack {
65
- flex-direction: column;
65
+ min-width: 0;
66
+ overflow-wrap: anywhere;
67
+ display: grid;
68
+ align-content: start;
66
69
  gap: 0.5rem;
67
70
  }
68
71
 
@@ -75,6 +78,7 @@ if (attributes.href && localeSlug) {
75
78
  .description {
76
79
  color: var(--sl-color-gray-3);
77
80
  line-height: 1.5;
81
+ margin: 0;
78
82
  }
79
83
 
80
84
  .icon {
@@ -83,10 +87,10 @@ if (attributes.href && localeSlug) {
83
87
 
84
88
  .card-icon {
85
89
  color: var(--sl-color-white);
86
- align-self: stretch;
90
+ align-self: start;
87
91
  position: relative;
88
- aspect-ratio: 1;
89
- max-height: 3rem;
92
+ width: 3rem;
93
+ height: 3rem;
90
94
  }
91
95
 
92
96
  .card-icon :global(svg) {
@@ -96,6 +100,11 @@ if (attributes.href && localeSlug) {
96
100
  height: 100%;
97
101
  }
98
102
 
103
+ .sl-link-card:focus-within {
104
+ outline: 2px solid var(--sl-color-text-accent);
105
+ outline-offset: 3px;
106
+ }
107
+
99
108
  /* Hover state */
100
109
  .sl-link-card:hover {
101
110
  background: var(--sl-color-gray-7, var(--sl-color-gray-6));
@@ -49,6 +49,11 @@ const shouldRenderSearch = config.pagefind || config.components.Search !== '@ast
49
49
  min-width: 0;
50
50
  }
51
51
 
52
+ .right-group { display: none; }
53
+ @media (min-width: 90rem) {
54
+ .right-group { display: flex; }
55
+ }
56
+
52
57
  .right-group,
53
58
  .social-icons {
54
59
  gap: 1rem;
@@ -71,7 +76,7 @@ const shouldRenderSearch = config.pagefind || config.components.Search !== '@ast
71
76
  align-items: center;
72
77
  }
73
78
 
74
- @media (min-width: 50rem) {
79
+ @media (min-width: 90rem) {
75
80
  :global(:root[data-has-sidebar]) {
76
81
  --__sidebar-pad: calc(2 * var(--sl-nav-pad-x));
77
82
  }
@@ -90,9 +95,9 @@ const shouldRenderSearch = config.pagefind || config.components.Search !== '@ast
90
95
  display: grid;
91
96
  grid-template-columns:
92
97
  /* 1 (site title) */
93
- auto
98
+ minmax(0, 1fr)
94
99
  /* 2 (mega-menu nav) */
95
- 1fr
100
+ auto
96
101
  /* 3 (search box) */
97
102
  auto
98
103
  /* 4 (right items) */
@@ -7,6 +7,7 @@ import ThemeSelect from 'virtual:starlight/components/ThemeSelect';
7
7
  import config from 'virtual:starlight/user-config';
8
8
  import MegaMenu from './SharedMegaMenu.tsx';
9
9
  import MegaMenuMobile from './SharedMegaMenuMobile.tsx';
10
+ import { federatedSearchSites } from '../src/utils/search-sites';
10
11
 
11
12
  /**
12
13
  * Render the `Search` component if Pagefind is enabled or the default search component has been overridden.
@@ -14,7 +15,7 @@ import MegaMenuMobile from './SharedMegaMenuMobile.tsx';
14
15
  const shouldRenderSearch = config.pagefind || config.components.Search !== '@astrojs/starlight/components/Search.astro';
15
16
  ---
16
17
 
17
- <div class="header sl-mega-menu-header">
18
+ <div class="header sl-mega-menu-header" data-search-sources={JSON.stringify(federatedSearchSites)}>
18
19
  <div class="title-wrapper sl-flex">
19
20
  <SiteTitle />
20
21
  </div>
@@ -34,8 +35,38 @@ const shouldRenderSearch = config.pagefind || config.components.Search !== '@ast
34
35
  <div class="smm-mobile-toggle">
35
36
  <MegaMenuMobile client:only="react" />
36
37
  </div>
38
+ <details class="compact-preferences">
39
+ <summary aria-label="Display and language">Aa</summary>
40
+ <div class="preferences-panel"><ThemeSelect /><LanguageSelect /></div>
41
+ </details>
37
42
  </div>
38
43
 
44
+ <script>
45
+ import { searchSource } from '../src/utils/search-source';
46
+ const header = document.querySelector<HTMLElement>('[data-search-sources]');
47
+ const sources = JSON.parse(header?.dataset.searchSources || '[]');
48
+ const annotateResults = () => {
49
+ for (const link of document.querySelectorAll<HTMLAnchorElement>('.pagefind-ui__result-link')) {
50
+ if (link.dataset.sourceAnnotated) continue;
51
+ link.dataset.sourceAnnotated = 'true';
52
+ const label = searchSource(link.href, sources);
53
+ if (!label) continue;
54
+ const source = document.createElement('span');
55
+ source.className = 'search-project-source';
56
+ source.textContent = label;
57
+ link.closest('.pagefind-ui__result-title')?.append(source);
58
+ }
59
+ };
60
+ new MutationObserver(annotateResults).observe(document.body, { childList: true, subtree: true });
61
+ document.addEventListener('keydown', (event) => {
62
+ const preferences = document.querySelector<HTMLDetailsElement>('.compact-preferences[open]');
63
+ if (event.key === 'Escape' && preferences) {
64
+ preferences.open = false;
65
+ preferences.querySelector('summary')?.focus();
66
+ }
67
+ });
68
+ </script>
69
+
39
70
  <style>
40
71
  @layer starlight.core {
41
72
  .header {
@@ -55,6 +86,37 @@ const shouldRenderSearch = config.pagefind || config.components.Search !== '@ast
55
86
  min-width: 0;
56
87
  }
57
88
 
89
+ .right-group { display: none; }
90
+ .compact-preferences { position: relative; flex-shrink: 0; }
91
+ .compact-preferences summary {
92
+ cursor: pointer;
93
+ padding: 0.75rem;
94
+ min-height: 44px;
95
+ list-style: none;
96
+ }
97
+ .preferences-panel {
98
+ position: absolute;
99
+ inset-inline-end: 0;
100
+ top: calc(100% + 0.5rem);
101
+ display: grid;
102
+ gap: 0.5rem;
103
+ padding: 0.75rem;
104
+ background: var(--sl-color-bg-nav);
105
+ border: 1px solid var(--sl-color-gray-5);
106
+ border-radius: 0.5rem;
107
+ }
108
+ :global(.search-project-source) {
109
+ display: block;
110
+ font-size: var(--sl-text-sm);
111
+ font-weight: 400;
112
+ color: var(--sl-color-gray-2);
113
+ margin-top: 0.25rem;
114
+ }
115
+ @media (min-width: 90rem) {
116
+ .right-group { display: flex; }
117
+ .compact-preferences { display: none; }
118
+ }
119
+
58
120
  .right-group,
59
121
  .social-icons {
60
122
  gap: 1rem;
@@ -77,7 +139,7 @@ const shouldRenderSearch = config.pagefind || config.components.Search !== '@ast
77
139
  align-items: center;
78
140
  }
79
141
 
80
- @media (min-width: 50rem) {
142
+ @media (min-width: 90rem) {
81
143
  :global(:root[data-has-sidebar]) {
82
144
  --__sidebar-pad: calc(2 * var(--sl-nav-pad-x));
83
145
  }
@@ -96,9 +158,9 @@ const shouldRenderSearch = config.pagefind || config.components.Search !== '@ast
96
158
  display: grid;
97
159
  grid-template-columns:
98
160
  /* 1 (site title) */
99
- auto
161
+ minmax(0, 1fr)
100
162
  /* 2 (mega-menu nav) */
101
- 1fr
163
+ auto
102
164
  /* 3 (search box) */
103
165
  auto
104
166
  /* 4 (right items) */
package/config.ts CHANGED
@@ -20,15 +20,7 @@ import starlightScrollToTop from 'starlight-scroll-to-top';
20
20
  import starlightVideosPlugin from 'starlight-videos';
21
21
  import f5xcDocsTheme from './index.ts';
22
22
  import { defaultLocale as f5xcDefaultLocale, f5xcDefaultLocales } from './src/i18n/locales.ts';
23
- import {
24
- categoryTitles,
25
- footerDescriptions,
26
- footerLabels,
27
- itemDescriptions,
28
- itemLabels,
29
- menuLabels,
30
- mobileLabels,
31
- } from './src/i18n/mega-menu-translations.ts';
23
+ import { mobileLabels } from './src/i18n/mega-menu-translations.ts';
32
24
  import { sidebarTranslations } from './src/i18n/translations.ts';
33
25
  import remarkMermaid from './src/plugins/remark-mermaid.mjs';
34
26
  import canonicalLlms from './src/utils/canonical-llms.mjs';
@@ -37,6 +29,7 @@ import { menuLocalizationPolicy } from './src/utils/menu-localization.ts';
37
29
  import { stagedProviderSections } from './src/utils/provider-sections.ts';
38
30
  import { publicationSidebar, repositoryPublicationProfile } from './src/utils/publication-profiles.ts';
39
31
  import { resolveMegaMenuIcon } from './src/utils/resolve-icon.ts';
32
+ import { federatedSearchSites } from './src/utils/search-sites';
40
33
  import providerSharedAssets from './src/utils/share-provider-assets.mjs';
41
34
  import { statisticsScriptMarkdown } from './src/utils/statistics-script-markdown.mjs';
42
35
  import { buildSubcategorySidebar } from './src/utils/subcategory-sidebar.ts';
@@ -133,130 +126,79 @@ export interface F5xcDocsConfigOptions {
133
126
  };
134
127
  }
135
128
 
136
- const developerAutomationTranslations = Object.fromEntries(
137
- Object.entries(categoryTitles.Automation).map(([locale, automation]) => [
138
- locale,
139
- `${categoryTitles['Developer Tools'][locale]} / ${automation}`,
140
- ]),
141
- );
142
-
143
- const xcshActionDescriptionTranslations = Object.fromEntries(
144
- Object.entries(categoryTitles.Automation).map(([locale, automation]) => [locale, `F5 XC — ${automation}`]),
145
- );
146
-
147
129
  export const defaultMegaMenuItems: MegaMenuItem[] = [
148
130
  {
149
- label: 'Security',
150
- translations: menuLabels.Security,
131
+ label: 'Demos',
151
132
  content: {
152
133
  layout: 'grid',
153
134
  columns: 2,
154
135
  categories: [
155
136
  {
156
- title: 'App & API Security',
157
- translations: categoryTitles['App & API Security'],
137
+ title: 'Security',
158
138
  items: [
159
139
  {
160
140
  label: 'Web App & API Protection',
161
- description: 'Web application and API protection demonstrations',
162
- href: 'https://f5-sales-demo.github.io/webapp-api-protection/',
141
+ description: 'Deploy an application security demo with Terraform and follow its protection walkthroughs.',
142
+ href: 'https://f5-sales-demo.github.io/webapp-api-protection/en/',
163
143
  icon: resolveMegaMenuIcon('f5xc:web-app-and-api-protection'),
164
144
  },
165
145
  {
166
- label: 'API Security',
167
- translations: itemLabels['API Security'],
168
- description: 'API discovery and protection',
169
- descriptionTranslations: itemDescriptions['API discovery and protection'],
170
- href: 'https://f5-sales-demo.github.io/api-protection/',
146
+ label: 'API Protection',
147
+ description: 'Find guides to API discovery, schema validation, and request controls.',
148
+ href: 'https://f5-sales-demo.github.io/api-protection/en/',
171
149
  icon: resolveMegaMenuIcon('f5xc:application-traffic-insight'),
172
150
  },
173
151
  {
174
- label: 'Client-Side Defense',
175
- translations: itemLabels['Client-Side Defense'],
176
- description: 'Browser-based threat protection',
177
- descriptionTranslations: itemDescriptions['Browser-based threat protection'],
178
- href: 'https://f5-sales-demo.github.io/csd/',
179
- icon: resolveMegaMenuIcon('f5xc:client-side-defense'),
152
+ label: 'Bot Defense Advanced',
153
+ description: 'Review behavioral bot detection and mitigation scenarios.',
154
+ href: 'https://f5-sales-demo.github.io/bot-advanced/en/',
155
+ icon: resolveMegaMenuIcon('f5xc:bot-defense'),
180
156
  },
181
157
  {
182
- label: 'Custom Responses',
183
- description: 'Terraform response customization showcase',
184
- href: 'https://f5-sales-demo.github.io/custom-responses/',
185
- icon: resolveMegaMenuIcon('f5xc:web-app-and-api-protection'),
158
+ label: 'Bot Defense Standard',
159
+ description: 'Review bot classification, verified crawlers, and blocking policies.',
160
+ href: 'https://f5-sales-demo.github.io/bot-standard/en/',
161
+ icon: resolveMegaMenuIcon('f5xc:bot-defense'),
186
162
  },
187
163
  {
188
- label: 'Web App Scanning',
189
- translations: itemLabels['Web App Scanning'],
190
- description: 'Vulnerability assessment and scanning',
191
- descriptionTranslations: itemDescriptions['Vulnerability assessment and scanning'],
192
- href: 'https://f5-sales-demo.github.io/was/',
193
- icon: resolveMegaMenuIcon('f5xc:web-app-scanning'),
164
+ label: 'Client-Side Defense',
165
+ description: 'Deploy browser script monitoring with an API or Terraform workflow.',
166
+ href: 'https://f5-sales-demo.github.io/csd/en/',
167
+ icon: resolveMegaMenuIcon('f5xc:client-side-defense'),
194
168
  },
195
- ],
196
- },
197
- {
198
- title: 'Threat Defense',
199
- translations: categoryTitles['Threat Defense'],
200
- items: [
201
169
  {
202
- label: 'Bot Defense Advanced',
203
- translations: itemLabels['Bot Defense Advanced'],
204
- description: 'Behavioral analysis and AI detection',
205
- descriptionTranslations: itemDescriptions['Behavioral analysis and AI detection'],
206
- href: 'https://f5-sales-demo.github.io/bot-advanced/',
207
- icon: resolveMegaMenuIcon('f5xc:bot-defense'),
170
+ label: 'DDoS Mitigation',
171
+ description: 'Review distributed denial-of-service defenses and demo scenarios.',
172
+ href: 'https://f5-sales-demo.github.io/ddos/en/',
173
+ icon: resolveMegaMenuIcon('f5xc:ddos-and-transit-services'),
208
174
  },
209
175
  {
210
- label: 'Bot Defense Standard',
211
- translations: itemLabels['Bot Defense Standard'],
212
- description: 'Signature-based bot detection',
213
- descriptionTranslations: itemDescriptions['Signature-based bot detection'],
214
- href: 'https://f5-sales-demo.github.io/bot-standard/',
215
- icon: resolveMegaMenuIcon('f5xc:bot-defense'),
176
+ label: 'Web App Scanning',
177
+ description: 'Find web application vulnerability scanning guides.',
178
+ href: 'https://f5-sales-demo.github.io/was/en/',
179
+ icon: resolveMegaMenuIcon('f5xc:web-app-scanning'),
216
180
  },
217
181
  {
218
- label: 'DDoS Protection',
219
- translations: itemLabels['DDoS Protection'],
220
- description: 'Distributed denial-of-service mitigation',
221
- descriptionTranslations: itemDescriptions['Distributed denial-of-service mitigation'],
222
- href: 'https://f5-sales-demo.github.io/ddos/',
223
- icon: resolveMegaMenuIcon('f5xc:ddos-and-transit-services'),
182
+ label: 'Custom responses',
183
+ description: 'Configure application replies, redirects, and security response pages.',
184
+ href: 'https://f5-sales-demo.github.io/custom-responses/en/',
185
+ icon: resolveMegaMenuIcon('f5xc:web-app-and-api-protection'),
224
186
  },
225
187
  ],
226
188
  },
227
- ],
228
- footer: {
229
- label: 'F5 Distributed Cloud Console',
230
- translations: footerLabels['F5 Distributed Cloud Console'],
231
- href: 'https://console.ves.volterra.io',
232
- description: 'Open the XC management portal',
233
- descriptionTranslations: footerDescriptions['Open the XC management portal'],
234
- },
235
- },
236
- },
237
- {
238
- label: 'Networking',
239
- translations: menuLabels.Networking,
240
- content: {
241
- layout: 'grid',
242
- columns: 2,
243
- categories: [
244
189
  {
245
- title: 'Connectivity & Delivery',
246
- translations: categoryTitles['Connectivity & Delivery'],
190
+ title: 'Networking and performance',
247
191
  items: [
248
192
  {
249
193
  label: 'Multi-Cloud Networking',
250
- translations: itemLabels['Multi-Cloud Networking'],
251
- description: 'Site connectivity across clouds',
252
- descriptionTranslations: itemDescriptions['Site connectivity across clouds'],
253
- href: 'https://f5-sales-demo.github.io/multi-cloud-networking/',
194
+ description: 'Find site deployment and cross-cloud connectivity guides.',
195
+ href: 'https://f5-sales-demo.github.io/multi-cloud-networking/en/',
254
196
  icon: resolveMegaMenuIcon('f5xc:multi-cloud-network-connect'),
255
197
  },
256
198
  {
257
- label: 'Canada Topology',
258
- description: 'Canadian hosting and regional access control',
259
- href: 'https://f5-sales-demo.github.io/canada/',
199
+ label: 'Canada topology',
200
+ description: 'Deploy Canadian application hosting and demonstrate regional access controls.',
201
+ href: 'https://f5-sales-demo.github.io/canada/en/',
260
202
  icon: {
261
203
  body: fs
262
204
  .readFileSync(new URL('./assets/canada-flag.svg', import.meta.url), 'utf8')
@@ -267,223 +209,180 @@ export const defaultMegaMenuItems: MegaMenuItem[] = [
267
209
  },
268
210
  },
269
211
  {
270
- label: 'Content Delivery',
271
- translations: itemLabels['Content Delivery'],
272
- description: 'Edge caching and distribution',
273
- descriptionTranslations: itemDescriptions['Edge caching and distribution'],
274
- href: 'https://f5-sales-demo.github.io/cdn/',
212
+ label: 'Content Delivery Network',
213
+ description: 'Review content delivery network (CDN) caching and origin configuration.',
214
+ href: 'https://f5-sales-demo.github.io/cdn/en/',
275
215
  icon: resolveMegaMenuIcon('f5xc:content-delivery-network'),
276
216
  },
277
217
  {
278
- label: 'DNS Load Balancing',
279
- translations: itemLabels['DNS Load Balancing'],
280
- description: 'DNS management and zones',
281
- descriptionTranslations: itemDescriptions['DNS management and zones'],
282
- href: 'https://f5-sales-demo.github.io/dns/',
218
+ label: 'DNS Management',
219
+ description: 'Find Domain Name System (DNS) zone and load balancing guides.',
220
+ href: 'https://f5-sales-demo.github.io/dns/en/',
283
221
  icon: resolveMegaMenuIcon('f5xc:dns-management'),
284
222
  },
285
223
  {
286
- label: 'NGINX Management',
287
- translations: itemLabels['NGINX Management'],
288
- description: 'NGINX integration and configuration',
289
- descriptionTranslations: itemDescriptions['NGINX integration and configuration'],
290
- href: 'https://f5-sales-demo.github.io/nginx/',
224
+ label: 'NGINX One',
225
+ description: 'Review NGINX instance visibility and configuration management.',
226
+ href: 'https://f5-sales-demo.github.io/nginx/en/',
291
227
  icon: resolveMegaMenuIcon('f5xc:nginx-one'),
292
228
  },
293
229
  ],
294
230
  },
231
+ ],
232
+ footer: {
233
+ label: 'Demo catalog',
234
+ href: 'https://f5-sales-demo.github.io/en/demos/',
235
+ description: 'All demonstration and capability guides',
236
+ },
237
+ },
238
+ },
239
+ {
240
+ label: 'Demo environment',
241
+ content: {
242
+ layout: 'list',
243
+ categories: [
295
244
  {
296
- title: 'Manage & Monitor',
297
- translations: categoryTitles['Manage & Monitor'],
245
+ title: 'Deployment resources',
298
246
  items: [
299
247
  {
300
- label: 'Observability',
301
- translations: itemLabels.Observability,
302
- description: 'Monitoring, metrics, and insights',
303
- descriptionTranslations: itemDescriptions['Monitoring, metrics, and insights'],
304
- href: 'https://f5-sales-demo.github.io/observability/',
305
- icon: resolveMegaMenuIcon('f5xc:observability'),
248
+ label: 'Origin server',
249
+ description: 'Compare the Azure full-origin stack with the separate AWS Juice Shop deployment.',
250
+ href: 'https://f5-sales-demo.github.io/origin-server/en/',
251
+ icon: resolveMegaMenuIcon('f5xc:distributed-apps'),
306
252
  },
307
253
  {
308
- label: 'Statistics',
309
- description: 'API metrics, access logs, and security events',
310
- href: 'https://f5-sales-demo.github.io/statistics/en/',
311
- icon: resolveMegaMenuIcon('f5xc:data-intelligence'),
254
+ label: 'Traffic generator',
255
+ description:
256
+ 'Choose Azure or AWS deployment guides for controlled security traffic and browser scenarios.',
257
+ href: 'https://f5-sales-demo.github.io/traffic-generator/en/',
258
+ icon: resolveMegaMenuIcon('f5xc:application-traffic-insight'),
312
259
  },
313
260
  {
314
- label: 'Administration',
315
- translations: itemLabels.Administration,
316
- description: 'Tenant management and RBAC',
317
- descriptionTranslations: itemDescriptions['Tenant management and RBAC'],
318
- href: 'https://f5-sales-demo.github.io/administration/',
319
- icon: resolveMegaMenuIcon('f5xc:administration'),
261
+ label: 'CDN simulator',
262
+ description: 'Deploy an Azure proxy that adds CDN headers to origin requests.',
263
+ href: 'https://f5-sales-demo.github.io/cdn-simulator/en/',
264
+ icon: resolveMegaMenuIcon('f5xc:content-delivery-network'),
320
265
  },
321
266
  ],
322
267
  },
323
268
  ],
324
269
  footer: {
325
- label: 'F5 Cloud Documentation',
326
- translations: footerLabels['F5 Cloud Documentation'],
327
- href: 'https://docs.cloud.f5.com',
328
- description: 'Official product documentation',
329
- descriptionTranslations: footerDescriptions['Official product documentation'],
270
+ label: 'Demo resource catalog',
271
+ href: 'https://f5-sales-demo.github.io/demo-resources/en/',
272
+ description: 'Azure and AWS offerings vary by component',
330
273
  },
331
274
  },
332
275
  },
333
276
  {
334
- label: 'Platform',
335
- translations: menuLabels.Platform,
277
+ label: 'Operations',
336
278
  content: {
337
279
  layout: 'list',
338
280
  categories: [
339
281
  {
340
- title: 'Documentation Tools',
341
- translations: categoryTitles['Documentation Tools'],
282
+ title: 'Queries and administration',
342
283
  items: [
343
284
  {
344
- label: 'F5 XC Docs',
345
- description: 'Organization documentation portal',
346
- href: 'https://f5-sales-demo.github.io/',
347
- icon: resolveMegaMenuIcon('f5xc:doc'),
348
- },
349
- {
350
- label: 'Docs Builder',
351
- translations: itemLabels['Docs Builder'],
352
- description: 'Containerized Astro build system',
353
- descriptionTranslations: itemDescriptions['Containerized Astro build system'],
354
- href: 'https://f5-sales-demo.github.io/docs-builder/',
355
- icon: resolveMegaMenuIcon('f5xc:doc'),
356
- },
357
- {
358
- label: 'F5 Docs Corpus',
359
- translations: itemLabels['F5 Docs Corpus'],
360
- description: 'Machine-readable F5 product documentation',
361
- descriptionTranslations: itemDescriptions['Machine-readable F5 product documentation'],
362
- href: 'https://f5-sales-demo.github.io/html-to-markdown/',
285
+ label: 'Statistics',
286
+ description: 'Query access logs, application metrics, and security telemetry through the API.',
287
+ href: 'https://f5-sales-demo.github.io/statistics/en/',
363
288
  icon: resolveMegaMenuIcon('f5xc:doc'),
364
289
  },
365
290
  {
366
- label: 'Docs Theme',
367
- translations: itemLabels['Docs Theme'],
368
- description: 'Shared branding and styling',
369
- descriptionTranslations: itemDescriptions['Shared branding and styling'],
370
- href: 'https://f5-sales-demo.github.io/docs-theme/',
371
- icon: resolveMegaMenuIcon('f5xc:shared-configuration'),
291
+ label: 'Observability',
292
+ description: 'Find monitoring, metrics, tracing, and alerting guides.',
293
+ href: 'https://f5-sales-demo.github.io/observability/en/',
294
+ icon: resolveMegaMenuIcon('f5xc:observability'),
372
295
  },
373
296
  {
374
- label: 'Icon Packages',
375
- translations: itemLabels['Icon Packages'],
376
- description: 'NPM icon component library',
377
- descriptionTranslations: itemDescriptions['NPM icon component library'],
378
- href: 'https://f5-sales-demo.github.io/docs-icons/',
379
- icon: resolveMegaMenuIcon('f5xc:distributed-apps'),
297
+ label: 'Administration',
298
+ description: 'Find tenant, namespace, and role management guides.',
299
+ href: 'https://f5-sales-demo.github.io/administration/en/',
300
+ icon: resolveMegaMenuIcon('f5xc:administration'),
380
301
  },
381
302
  ],
382
303
  },
304
+ ],
305
+ },
306
+ },
307
+ {
308
+ label: 'Developer tools',
309
+ content: {
310
+ layout: 'grid',
311
+ columns: 2,
312
+ categories: [
383
313
  {
384
- title: 'Developer Automation',
385
- translations: developerAutomationTranslations,
314
+ title: 'Tools and automation',
386
315
  items: [
387
316
  {
388
- label: 'Terraform Provider',
389
- translations: itemLabels['Terraform Provider'],
390
- description: 'F5 XC Terraform provider',
391
- descriptionTranslations: itemDescriptions['F5 XC Terraform provider'],
392
- href: 'https://f5-sales-demo.github.io/terraform-provider-xcsh/',
393
- icon: resolveMegaMenuIcon('hashicorp-flight:terraform-color'),
394
- },
395
- {
396
- label: 'API Specs',
397
- translations: itemLabels['API Specs'],
398
- description: 'OpenAPI spec validation and reconciliation',
399
- descriptionTranslations: itemDescriptions['OpenAPI spec validation and reconciliation'],
400
- href: 'https://f5-sales-demo.github.io/api-specs/',
401
- icon: resolveMegaMenuIcon('f5xc:data-intelligence'),
317
+ label: 'xcsh',
318
+ description:
319
+ 'Install the independent terminal assistant for engineering and F5 Distributed Cloud workflows.',
320
+ href: 'https://f5-sales-demo.github.io/xcsh/en/',
321
+ icon: resolveMegaMenuIcon('carbon:terminal'),
402
322
  },
403
323
  {
404
- label: 'API Specs Enriched',
405
- translations: itemLabels['API Specs Enriched'],
406
- description: 'Enriched OpenAPI specifications',
407
- descriptionTranslations: itemDescriptions['Enriched OpenAPI specifications'],
408
- href: 'https://f5-sales-demo.github.io/api-specs-enriched/',
409
- icon: resolveMegaMenuIcon('f5xc:data-intelligence'),
324
+ label: 'Terraform provider',
325
+ description: 'Configure F5 Distributed Cloud resources with Terraform.',
326
+ href: 'https://f5-sales-demo.github.io/terraform-provider-xcsh/',
327
+ icon: resolveMegaMenuIcon('f5xc:doc'),
410
328
  },
411
329
  {
412
- label: 'xcsh Manifest Automation',
413
- description: 'Deterministic F5 XC manifest operations in GitHub Actions',
414
- descriptionTranslations: xcshActionDescriptionTranslations,
415
- href: 'https://f5-sales-demo.github.io/xcsh-action/',
330
+ label: 'xcsh GitHub Action',
331
+ description: 'Run pinned xcsh manifest operations in GitHub Actions.',
332
+ href: 'https://f5-sales-demo.github.io/xcsh-action/en/',
416
333
  icon: resolveMegaMenuIcon('carbon:workflow-automation'),
417
334
  },
418
335
  {
419
- label: 'VS Code Extension',
420
- translations: itemLabels['VS Code Extension'],
421
- description: 'Manage F5 XC resources from VS Code',
422
- descriptionTranslations: itemDescriptions['Manage F5 XC resources from VS Code'],
423
- href: 'https://f5-sales-demo.github.io/vscode-xcsh/',
336
+ label: 'VS Code extension',
337
+ description: 'Author manifests and manage F5 Distributed Cloud resources in Visual Studio Code.',
338
+ href: 'https://f5-sales-demo.github.io/vscode-xcsh/en/',
424
339
  icon: resolveMegaMenuIcon('carbon:code'),
425
340
  },
426
341
  {
427
- label: 'xcsh CLI',
428
- translations: itemLabels['xcsh CLI'],
429
- description: 'AI-powered CLI for F5 XC',
430
- descriptionTranslations: itemDescriptions['AI-powered CLI for F5 XC'],
431
- href: 'https://f5-sales-demo.github.io/xcsh/',
432
- icon: resolveMegaMenuIcon('carbon:terminal'),
342
+ label: 'xcsh Chrome extension',
343
+ description: 'Connect xcsh to the F5 Distributed Cloud console through a local browser bridge.',
344
+ href: 'https://f5-sales-demo.github.io/xcsh-chrome-extension/en/',
345
+ icon: resolveMegaMenuIcon('carbon:application-web'),
433
346
  },
434
347
  {
435
- label: 'xcsh Chrome Extension',
436
- translations: itemLabels['xcsh Chrome Extension'],
437
- description: 'Drive the F5 XC console with xcsh',
438
- descriptionTranslations: itemDescriptions['Drive the F5 XC console with xcsh'],
439
- href: 'https://f5-sales-demo.github.io/xcsh-chrome-extension/',
440
- icon: resolveMegaMenuIcon('carbon:application-web'),
348
+ label: 'APT repository',
349
+ description: 'Install signed Debian and Ubuntu packages for xcsh and supporting tools.',
350
+ href: 'https://f5-sales-demo.github.io/apt-repo/en/',
351
+ icon: resolveMegaMenuIcon('f5xc:doc'),
441
352
  },
442
353
  ],
443
354
  },
444
- ],
445
- footer: {
446
- label: 'GitHub Organization',
447
- translations: footerLabels['GitHub Organization'],
448
- href: 'https://github.com/f5-sales-demo',
449
- description: 'View all repositories',
450
- descriptionTranslations: footerDescriptions['View all repositories'],
451
- },
452
- },
453
- },
454
- {
455
- label: 'AI',
456
- translations: menuLabels.AI,
457
- content: {
458
- layout: 'list',
459
- categories: [
460
355
  {
461
- title: 'AI Tools',
462
- translations: categoryTitles['AI Tools'],
356
+ title: 'Specifications and plugins',
463
357
  items: [
464
358
  {
465
- label: 'Marketplace',
466
- translations: itemLabels.Marketplace,
467
- description: 'AI-powered marketplace for F5 XC',
468
- descriptionTranslations: itemDescriptions['AI-powered marketplace for F5 XC'],
469
- href: 'https://f5-sales-demo.github.io/marketplace/',
470
- icon: resolveMegaMenuIcon('f5xc:ai_assistant_logo'),
359
+ label: 'API specifications',
360
+ description: 'Find validated OpenAPI specifications and the specification update pipeline.',
361
+ href: 'https://f5-sales-demo.github.io/api-specs/en/',
362
+ icon: resolveMegaMenuIcon('f5xc:data-intelligence'),
471
363
  },
472
364
  {
473
- label: 'xcsh',
474
- translations: itemLabels.xcsh,
475
- description: 'AI-powered development CLI with persistent sessions and native Rust tooling',
476
- descriptionTranslations:
477
- itemDescriptions['AI-powered development CLI with persistent sessions and native Rust tooling'],
478
- href: 'https://f5-sales-demo.github.io/xcsh/',
365
+ label: 'Enriched API specifications',
366
+ description: 'Browse OpenAPI schemas with additional constraints and examples.',
367
+ href: 'https://f5-sales-demo.github.io/api-specs-enriched/en/',
368
+ icon: resolveMegaMenuIcon('f5xc:data-intelligence'),
369
+ },
370
+ {
371
+ label: 'xcsh marketplace',
372
+ description: 'Find xcsh plugins for documentation, sales, cloud, security, and desktop work.',
373
+ href: 'https://f5-sales-demo.github.io/marketplace/en/',
479
374
  icon: resolveMegaMenuIcon('f5xc:ai_assistant_logo'),
480
375
  },
481
376
  {
482
- label: 'Console Catalog',
483
- translations: itemLabels['Console Catalog'],
484
- description: 'AI-driven browser automation for F5 XC UI',
485
- descriptionTranslations: itemDescriptions['AI-driven browser automation for F5 XC UI'],
486
- href: 'https://f5-sales-demo.github.io/console/',
377
+ label: 'Marketplace for Claude Code',
378
+ description: 'Find Language Server Protocol integrations and developer tools for Claude Code.',
379
+ href: 'https://f5-sales-demo.github.io/marketplace-claude-code/en/',
380
+ icon: resolveMegaMenuIcon('f5xc:doc'),
381
+ },
382
+ {
383
+ label: 'Console catalog',
384
+ description: 'Browse documented console routes and browser automation workflows.',
385
+ href: 'https://f5-sales-demo.github.io/console/en/',
487
386
  icon: resolveMegaMenuIcon('f5xc:ai_assistant_logo'),
488
387
  },
489
388
  ],
@@ -492,82 +391,58 @@ export const defaultMegaMenuItems: MegaMenuItem[] = [
492
391
  },
493
392
  },
494
393
  {
495
- label: 'Demo Resources',
496
- translations: menuLabels['Demo Resources'],
394
+ label: 'Ecosystem',
497
395
  content: {
498
396
  layout: 'list',
499
397
  categories: [
500
398
  {
501
- title: 'Components',
502
- translations: categoryTitles.Components,
399
+ title: 'Public projects',
503
400
  items: [
504
401
  {
505
- label: 'Origin Server',
506
- translations: itemLabels['Origin Server'],
507
- description: 'Vulnerable web applications for WAF and API testing',
508
- descriptionTranslations: itemDescriptions['Vulnerable web applications for WAF and API testing'],
509
- href: 'https://f5-sales-demo.github.io/origin-server/',
510
- icon: resolveMegaMenuIcon('f5xc:distributed-apps'),
511
- },
512
- {
513
- label: 'Traffic Generator',
514
- translations: itemLabels['Traffic Generator'],
515
- description: 'Security tools and attack suites for traffic generation',
516
- descriptionTranslations: itemDescriptions['Security tools and attack suites for traffic generation'],
517
- href: 'https://f5-sales-demo.github.io/traffic-generator/',
518
- icon: resolveMegaMenuIcon('f5xc:application-traffic-insight'),
402
+ label: 'F5 documentation corpus',
403
+ description: 'Browse curated F5 documentation and its Markdown sources.',
404
+ href: 'https://f5-sales-demo.github.io/html-to-markdown/',
405
+ icon: resolveMegaMenuIcon('f5xc:doc'),
519
406
  },
520
407
  {
521
- label: 'CDN Simulator',
522
- translations: itemLabels['CDN Simulator'],
523
- description: 'NGINX-based CDN edge node simulator',
524
- descriptionTranslations: itemDescriptions['NGINX-based CDN edge node simulator'],
525
- href: 'https://f5-sales-demo.github.io/cdn-simulator/',
526
- icon: resolveMegaMenuIcon('f5xc:content-delivery-network'),
408
+ label: 'Ecosystem directory',
409
+ description: 'Documentation, tools, and public project sources',
410
+ href: 'https://f5-sales-demo.github.io/en/ecosystem/',
411
+ icon: resolveMegaMenuIcon('f5xc:doc'),
527
412
  },
528
413
  ],
529
414
  },
530
415
  ],
531
416
  footer: {
532
- label: 'View All Components',
533
- translations: footerLabels['View All Components'],
534
- href: 'https://f5-sales-demo.github.io/demo-resources/',
535
- description: 'Browse the full demo resource catalog',
536
- descriptionTranslations: footerDescriptions['Browse the full demo resource catalog'],
417
+ label: 'Project sources',
418
+ href: 'https://github.com/f5-sales-demo',
419
+ description: 'Public repositories in the community organization',
537
420
  },
538
421
  },
539
422
  },
540
423
  {
541
- label: 'Resources',
542
- translations: menuLabels.Resources,
424
+ label: 'F5 services',
543
425
  content: {
544
426
  layout: 'list',
545
427
  categories: [
546
428
  {
547
- title: 'F5 Ecosystem',
548
- translations: categoryTitles['F5 Ecosystem'],
429
+ title: 'Official documentation and services',
549
430
  items: [
550
431
  {
551
- label: 'F5 XC Console',
552
- translations: itemLabels['F5 XC Console'],
553
- description: 'Distributed Cloud management portal',
554
- descriptionTranslations: itemDescriptions['Distributed Cloud management portal'],
432
+ label: 'F5 Distributed Cloud console',
433
+ description: 'Management service; sign-in required',
555
434
  href: 'https://console.ves.volterra.io',
556
435
  icon: resolveMegaMenuIcon('f5xc:platform'),
557
436
  },
558
437
  {
559
- label: 'F5 Cloud Docs',
560
- translations: itemLabels['F5 Cloud Docs'],
561
- description: 'Official product documentation',
562
- descriptionTranslations: itemDescriptions['Official product documentation'],
438
+ label: 'F5 Distributed Cloud documentation',
439
+ description: 'Official public product documentation',
563
440
  href: 'https://docs.cloud.f5.com',
564
441
  icon: resolveMegaMenuIcon('f5xc:doc'),
565
442
  },
566
443
  {
567
- label: 'MyF5 Support',
568
- translations: itemLabels['MyF5 Support'],
569
- description: 'Technical support portal',
570
- descriptionTranslations: itemDescriptions['Technical support portal'],
444
+ label: 'MyF5 support',
445
+ description: 'Support portal; sign-in required for account services',
571
446
  href: 'https://my.f5.com/manage/s/',
572
447
  icon: resolveMegaMenuIcon('f5xc:support'),
573
448
  },
@@ -630,38 +505,7 @@ export function federatedSearchBundlePaths(site: string, base: string): string[]
630
505
  );
631
506
  }
632
507
 
633
- export const federatedSearchSites = [
634
- { repo: 'docs-builder', label: 'Docs Builder' },
635
- { repo: 'docs-theme', label: 'Docs Theme' },
636
- { repo: 'f5-sales-demo.github.io', label: 'F5 XC Docs' },
637
- { repo: 'administration', label: 'Administration' },
638
- { repo: 'nginx', label: 'NGINX' },
639
- { repo: 'observability', label: 'Observability' },
640
- { repo: 'statistics', label: 'Statistics' },
641
- { repo: 'was', label: 'Web App Scanning' },
642
- { repo: 'multi-cloud-networking', label: 'Multi-Cloud Networking' },
643
- { repo: 'canada', label: 'Canada Topology' },
644
- { repo: 'dns', label: 'DNS' },
645
- { repo: 'cdn', label: 'CDN' },
646
- { repo: 'bot-standard', label: 'Bot Standard' },
647
- { repo: 'bot-advanced', label: 'Bot Advanced' },
648
- { repo: 'ddos', label: 'DDoS' },
649
- { repo: 'webapp-api-protection', label: 'Web App & API Protection' },
650
- { repo: 'api-protection', label: 'API Security' },
651
- { repo: 'xcsh', label: 'xcsh' },
652
- { repo: 'xcsh-action', label: 'xcsh Manifest Automation' },
653
- { repo: 'csd', label: 'Client-Side Defense' },
654
- { repo: 'docs-icons', label: 'Docs Icons' },
655
- { repo: 'marketplace', label: 'Marketplace' },
656
- { repo: 'api-specs', label: 'API Specs' },
657
- { repo: 'api-specs-enriched', label: 'API Specs Enriched' },
658
- { repo: 'cdn-simulator', label: 'CDN Simulator' },
659
- { repo: 'origin-server', label: 'Origin Server' },
660
- { repo: 'traffic-generator', label: 'Traffic Generator' },
661
- { repo: 'demo-resources', label: 'Demo Resources' },
662
- { repo: 'xcsh-chrome-extension', label: 'xcsh Chrome Extension' },
663
- { repo: 'console', label: 'Console Catalog' },
664
- ];
508
+ export { federatedSearchSites } from './src/utils/search-sites';
665
509
 
666
510
  export function createF5xcDocsConfig(options: F5xcDocsConfigOptions = {}) {
667
511
  const canonicalProvider =
@@ -729,7 +573,9 @@ export function createF5xcDocsConfig(options: F5xcDocsConfigOptions = {}) {
729
573
  (canonicalProvider
730
574
  ? [...defaultMegaMenuItems, ...providerMegaMenu(base, stagedProviderSections(canonicalProvider.manifest))]
731
575
  : defaultMegaMenuItems);
732
- const head = options.head || (canonicalProvider ? [] : defaultHead);
576
+ const head =
577
+ options.head ||
578
+ (canonicalProvider || githubRepository === 'f5-sales-demo/f5-sales-demo.github.io' ? [] : defaultHead);
733
579
  const logo = options.logo || { src: '@f5-sales-demo/docs-theme/assets/f5-distributed-cloud.svg' };
734
580
  const additionalRemarkPlugins = options.additionalRemarkPlugins || [];
735
581
  const additionalIntegrations = options.additionalIntegrations || [];
package/index.ts CHANGED
@@ -25,6 +25,7 @@ export default function f5xcDocsTheme(repository = process.env.GITHUB_REPOSITORY
25
25
  components: {
26
26
  ...config.components,
27
27
  Header: '@f5-sales-demo/docs-theme/components/SharedHeader.astro',
28
+ Head: '@f5-sales-demo/docs-theme/components/Head.astro',
28
29
  ...(process.env.DOCS_PROFILE === 'canonical-provider'
29
30
  ? {
30
31
  Sidebar: '@f5-sales-demo/docs-theme/components/ProviderSidebar.astro',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@f5-sales-demo/docs-theme",
3
- "version": "4.11.5",
3
+ "version": "4.11.7",
4
4
  "description": "F5 Distributed Cloud branded Starlight documentation theme",
5
5
  "type": "module",
6
6
  "engines": {
@@ -46,6 +46,7 @@
46
46
  "./components/CanadaSidebar.astro": "./components/CanadaSidebar.astro",
47
47
  "./components/ProviderHeader.astro": "./components/ProviderHeader.astro",
48
48
  "./components/SharedHeader.astro": "./components/SharedHeader.astro",
49
+ "./components/Head.astro": "./components/Head.astro",
49
50
  "./assets/canada-flag.svg": "./assets/canada-flag.svg",
50
51
  "./assets/canada-favicon.svg": "./assets/canada-favicon.svg"
51
52
  },
@@ -29,6 +29,10 @@ export function localizeEcosystemHref(
29
29
  const segments = url.pathname.split('/').filter(Boolean);
30
30
  if (segments.length === 0) return href;
31
31
 
32
+ // Organization-root routes begin with a locale, not a project slug.
33
+ // Explicit catalog locale links must never become /en/fr/demos/.
34
+ if (VALID_SLUGS.has(segments[0])) return href;
35
+
32
36
  if (NON_LOCALIZED_ECOSYSTEM_SLUGS.has(segments[0])) return href;
33
37
 
34
38
  if (segments.length >= 2 && VALID_SLUGS.has(segments[1])) {
@@ -0,0 +1,7 @@
1
+ import { expect, it } from 'vitest';
2
+ import { pageTitle } from './page-title';
3
+
4
+ it('omits a duplicate site suffix while retaining distinct page titles', () => {
5
+ expect(pageTitle('Sales demos', 'Sales demos', 'Sales demos | Sales demos')).toBe('Sales demos');
6
+ expect(pageTitle('Demo catalog', 'Sales demos', 'Demo catalog | Sales demos')).toBe('Demo catalog | Sales demos');
7
+ });
@@ -0,0 +1,4 @@
1
+ /** Keep the framework title format except when its site suffix repeats the page. */
2
+ export function pageTitle(title: string, siteTitle: string, formattedTitle: string): string {
3
+ return title === siteTitle ? title : formattedTitle;
4
+ }
@@ -0,0 +1,32 @@
1
+ export const federatedSearchSites = [
2
+ { repo: 'docs-builder', label: 'Documentation builder' },
3
+ { repo: 'docs-theme', label: 'Shared documentation theme' },
4
+ { repo: 'f5-sales-demo.github.io', label: 'Sales demo portal' },
5
+ { repo: 'administration', label: 'Administration' },
6
+ { repo: 'nginx', label: 'NGINX One' },
7
+ { repo: 'observability', label: 'Observability' },
8
+ { repo: 'statistics', label: 'Statistics' },
9
+ { repo: 'was', label: 'Web App Scanning' },
10
+ { repo: 'multi-cloud-networking', label: 'Multi-Cloud Networking' },
11
+ { repo: 'canada', label: 'Canada topology' },
12
+ { repo: 'dns', label: 'DNS Management' },
13
+ { repo: 'cdn', label: 'Content Delivery Network' },
14
+ { repo: 'bot-standard', label: 'Bot Defense Standard' },
15
+ { repo: 'bot-advanced', label: 'Bot Defense Advanced' },
16
+ { repo: 'ddos', label: 'DDoS Mitigation' },
17
+ { repo: 'webapp-api-protection', label: 'Web App & API Protection' },
18
+ { repo: 'api-protection', label: 'API Protection' },
19
+ { repo: 'xcsh', label: 'xcsh' },
20
+ { repo: 'xcsh-action', label: 'xcsh GitHub Action' },
21
+ { repo: 'csd', label: 'Client-Side Defense' },
22
+ { repo: 'docs-icons', label: 'Icon packages' },
23
+ { repo: 'marketplace', label: 'xcsh marketplace' },
24
+ { repo: 'api-specs', label: 'API specifications' },
25
+ { repo: 'api-specs-enriched', label: 'Enriched API specifications' },
26
+ { repo: 'cdn-simulator', label: 'CDN simulator' },
27
+ { repo: 'origin-server', label: 'Origin server' },
28
+ { repo: 'traffic-generator', label: 'Traffic generator' },
29
+ { repo: 'demo-resources', label: 'Demo resource catalog' },
30
+ { repo: 'xcsh-chrome-extension', label: 'xcsh Chrome extension' },
31
+ { repo: 'console', label: 'Console catalog' },
32
+ ];
@@ -0,0 +1,9 @@
1
+ import { expect, it } from 'vitest';
2
+ import { searchSource } from './search-source';
3
+
4
+ it('identifies the source project of federated results and root catalog pages', () => {
5
+ const sources = [{ repo: 'origin-server', label: 'Origin server' }];
6
+ expect(searchSource('https://f5-sales-demo.github.io/origin-server/en/03-deploy/', sources)).toBe('Origin server');
7
+ expect(searchSource('https://f5-sales-demo.github.io/en/demos/', sources)).toBe('Sales demo portal');
8
+ expect(searchSource('https://example.com/docs/', sources)).toBeUndefined();
9
+ });
@@ -0,0 +1,11 @@
1
+ export function searchSource(href: string, sources: Array<{ repo: string; label: string }>): string | undefined {
2
+ let url: URL;
3
+ try {
4
+ url = new URL(href);
5
+ } catch {
6
+ return undefined;
7
+ }
8
+ if (url.hostname !== 'f5-sales-demo.github.io') return undefined;
9
+ const project = url.pathname.split('/').filter(Boolean)[0];
10
+ return sources.find((source) => source.repo === project)?.label ?? 'Sales demo portal';
11
+ }
@@ -0,0 +1,34 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { defaultMegaMenuItems } from '../../config';
3
+ import { localizeEcosystemHref } from './localize-ecosystem-href';
4
+
5
+ describe('showcase navigation', () => {
6
+ it('uses the audience taxonomy without duplicate project destinations', () => {
7
+ expect(defaultMegaMenuItems.map((item) => item.label)).toEqual([
8
+ 'Demos',
9
+ 'Demo environment',
10
+ 'Operations',
11
+ 'Developer tools',
12
+ 'Ecosystem',
13
+ 'F5 services',
14
+ ]);
15
+ const links = defaultMegaMenuItems.flatMap((item) => item.content?.categories?.flatMap((c) => c.items) ?? []);
16
+ const urls = links.map((item) => item.href);
17
+ expect(new Set(urls).size).toBe(urls.length);
18
+ expect(urls).not.toContain('https://f5-sales-demo.github.io/');
19
+ expect(links.find((l) => l.href.includes('/api-protection/'))?.label).toBe('API Protection');
20
+ expect(links.find((l) => l.href.includes('/ddos/'))?.label).toBe('DDoS Mitigation');
21
+ expect(defaultMegaMenuItems[1].content?.footer?.label).toBe('Demo resource catalog');
22
+ });
23
+
24
+ it('keeps explicit English catalog destinations valid across locale selection', () => {
25
+ for (const locale of ['en', 'fr', 'ar']) {
26
+ expect(localizeEcosystemHref('https://f5-sales-demo.github.io/en/demos/', locale)).toBe(
27
+ 'https://f5-sales-demo.github.io/en/demos/',
28
+ );
29
+ expect(localizeEcosystemHref('https://f5-sales-demo.github.io/en/ecosystem/', locale)).toBe(
30
+ 'https://f5-sales-demo.github.io/en/ecosystem/',
31
+ );
32
+ }
33
+ });
34
+ });
@@ -4,15 +4,15 @@ import { localizeEcosystemHref } from './localize-ecosystem-href';
4
4
 
5
5
  describe('Statistics discovery', () => {
6
6
  it('lists Statistics beside monitoring tools exactly once', () => {
7
- const networking = defaultMegaMenuItems.find((item) => item.label === 'Networking');
8
- const category = networking?.content?.categories?.find((item) => item.title === 'Manage & Monitor');
7
+ const networking = defaultMegaMenuItems.find((item) => item.label === 'Operations');
8
+ const category = networking?.content?.categories?.find((item) => item.title === 'Queries and administration');
9
9
  const statistics = category?.items?.filter((item) => item.label === 'Statistics');
10
10
  expect(statistics).toHaveLength(1);
11
11
  expect(statistics?.[0]).toMatchObject({
12
12
  href: 'https://f5-sales-demo.github.io/statistics/en/',
13
- description: 'API metrics, access logs, and security events',
13
+ description: 'Query access logs, application metrics, and security telemetry through the API.',
14
14
  });
15
- expect(category?.items?.map((item) => item.label)).toEqual(['Observability', 'Statistics', 'Administration']);
15
+ expect(category?.items?.map((item) => item.label)).toEqual(['Statistics', 'Observability', 'Administration']);
16
16
  });
17
17
  it('adds Statistics to federated search and avoids self-index duplication', () => {
18
18
  expect(federatedSearchSites.filter((item) => item.repo === 'statistics')).toHaveLength(1);
package/styles/custom.css CHANGED
@@ -873,3 +873,33 @@ summary:focus-visible {
873
873
  }
874
874
  }
875
875
 
876
+
877
+ /* The sales showcase prioritizes destinations over decorative hero space. */
878
+ body:has(.sales-showcase) .hero {
879
+ padding-block: 1rem;
880
+ grid-template-columns: 1fr;
881
+ gap: 1rem;
882
+ }
883
+ body:has(.sales-showcase) .hero h1 {
884
+ font-size: clamp(1.75rem, 5vw, 3rem);
885
+ }
886
+ body:has(.sales-showcase) .hero .tagline {
887
+ max-width: 48rem;
888
+ font-size: var(--sl-text-lg);
889
+ }
890
+ body:has(.sales-showcase) .hero > .stack { gap: 1rem; }
891
+ body:has(.sales-showcase) .hero .actions { gap: 0.75rem; }
892
+ .sales-showcase { --showcase-gap: 1rem; }
893
+ .sales-showcase .card-grid { gap: var(--showcase-gap); }
894
+ .sales-showcase .sl-link-card { background: var(--sl-color-gray-7, var(--sl-color-gray-6)); }
895
+ .sales-showcase h2 { margin-block-start: 2rem; }
896
+ .sales-showcase h3 { margin-block-start: 1rem; }
897
+ @media (max-width: 49.99rem) {
898
+ body:has(.sales-showcase) .hero { padding-block: 0.5rem; }
899
+ body:has(.sales-showcase) .hero .actions { flex-wrap: wrap; }
900
+ }
901
+ @media (prefers-reduced-motion: reduce) {
902
+ *, *::before, *::after { scroll-behavior: auto !important; }
903
+ .sl-link-card, .smm-viewport, .smm-content { transition: none !important; animation: none !important; }
904
+ .sl-link-card:hover { transform: none; }
905
+ }