@f5-sales-demo/docs-theme 4.11.7 → 4.13.0
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/components/Footer.astro +0 -12
- package/components/Head.astro +7 -1
- package/components/LinkCard.astro +0 -87
- package/components/LocalHeader.astro +57 -0
- package/components/ProviderHeader.astro +2 -119
- package/components/SharedHeader.astro +16 -147
- package/components/SiteTitle.astro +3 -27
- package/components/StatisticsSidebar.astro +14 -0
- package/config.ts +16 -340
- package/index.ts +15 -4
- package/package.json +8 -4
- package/src/shared/baseline.json +30 -0
- package/src/shared/bootstrap.mjs +135 -0
- package/src/shared/contract.mjs +110 -0
- package/src/shared/localize.mjs +15 -0
- package/src/shared/runtime.tsx +124 -0
- package/src/shared/search-integration.mjs +16 -0
- package/src/shared/settings.mjs +19 -0
- package/src/utils/canada-sidebar-plugin.test.ts +11 -0
- package/src/utils/publication-profiles.test.ts +30 -6
- package/src/utils/publication-profiles.ts +22 -1
- package/src/utils/search-sites.ts +3 -32
- package/src/utils/showcase-navigation.test.ts +2 -32
- package/src/utils/statistics-navigation.test.ts +5 -29
- package/styles/shell.css +242 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export const ROOT = 'https://f5-sales-demo.github.io/shared/';
|
|
2
|
+
export const CONTRACT = 'v1';
|
|
3
|
+
const hash = /^[a-f0-9]{64}$/;
|
|
4
|
+
export function validateAsset(asset) {
|
|
5
|
+
if (!asset || !hash.test(asset.sha256) || !Number.isSafeInteger(asset.bytes) || asset.bytes < 1) {
|
|
6
|
+
throw new Error('Invalid shared asset receipt');
|
|
7
|
+
}
|
|
8
|
+
const url = new URL(asset.url);
|
|
9
|
+
if (
|
|
10
|
+
url.href !== `${ROOT}assets/${asset.sha256}.${asset.extension}` ||
|
|
11
|
+
!/^(css|js|json|woff2|svg|png|txt)$/.test(asset.extension)
|
|
12
|
+
) {
|
|
13
|
+
throw new Error('Shared asset must use its canonical content-addressed root URL');
|
|
14
|
+
}
|
|
15
|
+
return asset;
|
|
16
|
+
}
|
|
17
|
+
export function validatePointer(pointer) {
|
|
18
|
+
if (pointer?.contract !== CONTRACT) throw new Error('Unsupported shared contract');
|
|
19
|
+
const release = pointer.release;
|
|
20
|
+
if (
|
|
21
|
+
!release ||
|
|
22
|
+
!hash.test(release.sha256) ||
|
|
23
|
+
!Number.isSafeInteger(release.bytes) ||
|
|
24
|
+
release.bytes < 1 ||
|
|
25
|
+
release.url !== `${ROOT}v1/releases/${release.sha256}.json`
|
|
26
|
+
)
|
|
27
|
+
throw new Error('Invalid shared release pointer');
|
|
28
|
+
return pointer;
|
|
29
|
+
}
|
|
30
|
+
export function validateManifest(manifest) {
|
|
31
|
+
if (manifest?.contract !== CONTRACT || !manifest.assets || typeof manifest.assets !== 'object')
|
|
32
|
+
throw new Error('Unsupported shared manifest');
|
|
33
|
+
for (const required of ['css', 'runtime', 'data', 'logo', 'favicon']) {
|
|
34
|
+
if (!manifest.assets[required]) throw new Error(`Missing shared asset: ${required}`);
|
|
35
|
+
}
|
|
36
|
+
const types = { css: 'css', runtime: 'js', data: 'json', logo: 'svg', favicon: 'svg' };
|
|
37
|
+
for (const [name, asset] of Object.entries(manifest.assets)) {
|
|
38
|
+
validateAsset(asset);
|
|
39
|
+
if (types[name] && asset.extension !== types[name]) throw new Error(`Incorrect shared asset type: ${name}`);
|
|
40
|
+
}
|
|
41
|
+
return manifest;
|
|
42
|
+
}
|
|
43
|
+
export function validateConsumer(consumer) {
|
|
44
|
+
if (
|
|
45
|
+
consumer?.contract !== CONTRACT ||
|
|
46
|
+
!/^f5-sales-demo\/[a-z0-9_.-]+$/.test(consumer.repository) ||
|
|
47
|
+
!/^\/(?:[a-zA-Z0-9_.-]+\/)*$/.test(consumer.documentBase) ||
|
|
48
|
+
typeof consumer.locale !== 'string' ||
|
|
49
|
+
!consumer.mounts ||
|
|
50
|
+
!['desktop', 'mobile'].every(
|
|
51
|
+
(key) => typeof consumer.mounts[key] === 'string' && /^#[a-z][a-z0-9-]*$/.test(consumer.mounts[key]),
|
|
52
|
+
)
|
|
53
|
+
) {
|
|
54
|
+
throw new Error('Invalid shared consumer contract');
|
|
55
|
+
}
|
|
56
|
+
return consumer;
|
|
57
|
+
}
|
|
58
|
+
export function validateData(data) {
|
|
59
|
+
if (
|
|
60
|
+
!Array.isArray(data?.menu?.items) ||
|
|
61
|
+
!data.menu.items.length ||
|
|
62
|
+
!Array.isArray(data.searchSources) ||
|
|
63
|
+
!data.routing
|
|
64
|
+
)
|
|
65
|
+
throw new Error('Invalid shared menu data');
|
|
66
|
+
const labels = new Set();
|
|
67
|
+
const destinations = new Set();
|
|
68
|
+
const visit = (value) => {
|
|
69
|
+
if (!value || typeof value !== 'object') return;
|
|
70
|
+
for (const [key, child] of Object.entries(value)) {
|
|
71
|
+
if (key === 'href') {
|
|
72
|
+
const url = new URL(child);
|
|
73
|
+
if (url.protocol !== 'https:' || url.username || url.password)
|
|
74
|
+
throw new Error('Menu destinations require public HTTPS URLs');
|
|
75
|
+
} else if (/translations$/i.test(key)) {
|
|
76
|
+
if (
|
|
77
|
+
!child ||
|
|
78
|
+
typeof child !== 'object' ||
|
|
79
|
+
Object.values(child).some((text) => typeof text !== 'string' || !text.trim())
|
|
80
|
+
)
|
|
81
|
+
throw new Error('Invalid supplied translation');
|
|
82
|
+
} else visit(child);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
visit(data.menu);
|
|
86
|
+
for (const item of data.menu.items) {
|
|
87
|
+
if (!item.label?.trim() || labels.has(item.label)) throw new Error('Duplicate or missing menu label');
|
|
88
|
+
labels.add(item.label);
|
|
89
|
+
for (const category of item.content?.categories || []) {
|
|
90
|
+
const links = new Set();
|
|
91
|
+
for (const link of category.items) {
|
|
92
|
+
if (!link.label?.trim() || links.has(link.href)) throw new Error('Duplicate or missing menu entry');
|
|
93
|
+
links.add(link.href);
|
|
94
|
+
if (destinations.has(link.href)) throw new Error('Duplicate global menu destination');
|
|
95
|
+
destinations.add(link.href);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const sources = new Set();
|
|
100
|
+
for (const source of data.searchSources) {
|
|
101
|
+
if (!source.repo || !source.label || sources.has(source.repo))
|
|
102
|
+
throw new Error('Duplicate or missing Search source');
|
|
103
|
+
sources.add(source.repo);
|
|
104
|
+
}
|
|
105
|
+
for (const key of ['locales', 'unlocalized', 'englishOnly']) {
|
|
106
|
+
if (!Array.isArray(data.routing[key]) || data.routing[key].some((value) => typeof value !== 'string'))
|
|
107
|
+
throw new Error('Invalid locale routing');
|
|
108
|
+
}
|
|
109
|
+
return data;
|
|
110
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function langToSlug(lang) {
|
|
2
|
+
return lang.toLowerCase();
|
|
3
|
+
}
|
|
4
|
+
export function localizeEcosystemHref(href, locale) {
|
|
5
|
+
const routing = globalThis.F5SharedRoutingV1;
|
|
6
|
+
if (!routing?.locales.includes(locale)) return href;
|
|
7
|
+
const url = new URL(href, document.baseURI);
|
|
8
|
+
if (url.hostname !== 'f5-sales-demo.github.io') return href;
|
|
9
|
+
const parts = url.pathname.split('/').filter(Boolean);
|
|
10
|
+
if (!parts.length || routing.locales.includes(parts[0]) || routing.unlocalized.includes(parts[0])) return href;
|
|
11
|
+
if (parts[1] && routing.locales.includes(parts[1])) return href;
|
|
12
|
+
parts.splice(1, 0, routing.englishOnly.includes(parts[0]) ? 'en' : locale);
|
|
13
|
+
url.pathname = `/${parts.join('/')}/`;
|
|
14
|
+
return url.href;
|
|
15
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { MegaMenuConfig } from '@f5-sales-demo/starlight-mega-menu';
|
|
2
|
+
import MegaMenu from '@f5-sales-demo/starlight-mega-menu/components/MegaMenu.tsx';
|
|
3
|
+
import MegaMenuMobile from '@f5-sales-demo/starlight-mega-menu/components/MegaMenuMobile.tsx';
|
|
4
|
+
import { flushSync } from 'react-dom';
|
|
5
|
+
import { createRoot } from 'react-dom/client';
|
|
6
|
+
import { searchSource } from '../utils/search-source.ts';
|
|
7
|
+
|
|
8
|
+
interface Consumer {
|
|
9
|
+
repository: string;
|
|
10
|
+
mounts: { desktop: string; mobile: string };
|
|
11
|
+
}
|
|
12
|
+
interface Asset {
|
|
13
|
+
url: string;
|
|
14
|
+
}
|
|
15
|
+
interface Release {
|
|
16
|
+
data: {
|
|
17
|
+
menu: MegaMenuConfig;
|
|
18
|
+
routing: { locales: string[]; unlocalized: string[]; englishOnly: string[] };
|
|
19
|
+
searchSources: Array<{ repo: string; label: string }>;
|
|
20
|
+
};
|
|
21
|
+
manifest: { assets: Record<string, Asset> };
|
|
22
|
+
}
|
|
23
|
+
interface Prepared {
|
|
24
|
+
commit(): void;
|
|
25
|
+
dispose(): void;
|
|
26
|
+
}
|
|
27
|
+
declare global {
|
|
28
|
+
interface Window {
|
|
29
|
+
F5SharedV1: Map<string, { prepare(consumer: Consumer, release: Release): Prepared }>;
|
|
30
|
+
F5SharedSearchV1: { mergeIndex: Array<{ bundlePath: string }> };
|
|
31
|
+
}
|
|
32
|
+
var F5SharedRoutingV1: Release['data']['routing'];
|
|
33
|
+
}
|
|
34
|
+
// All imports are bundled once by the publisher. No downstream React island is involved.
|
|
35
|
+
window.F5SharedV1 ||= new Map();
|
|
36
|
+
const registry = window.F5SharedV1;
|
|
37
|
+
registry.set(import.meta.url, {
|
|
38
|
+
prepare(consumer: Consumer, release: Release) {
|
|
39
|
+
const desktop = document.querySelector(consumer.mounts.desktop);
|
|
40
|
+
const mobile = document.querySelector(consumer.mounts.mobile);
|
|
41
|
+
if (!desktop || !mobile) throw new Error('Missing ecosystem mount');
|
|
42
|
+
const desktopStage = document.createElement('div');
|
|
43
|
+
const mobileStage = document.createElement('div');
|
|
44
|
+
const roots = [createRoot(desktopStage), createRoot(mobileStage)];
|
|
45
|
+
globalThis.F5SharedRoutingV1 = release.data.routing;
|
|
46
|
+
flushSync(() => {
|
|
47
|
+
roots[0].render(<MegaMenu config={release.data.menu} />);
|
|
48
|
+
roots[1].render(<MegaMenuMobile config={release.data.menu} />);
|
|
49
|
+
});
|
|
50
|
+
let observer: MutationObserver;
|
|
51
|
+
const dismissPreferences = (event: KeyboardEvent) => {
|
|
52
|
+
const preferences = document.querySelector<HTMLDetailsElement>(
|
|
53
|
+
'.compact-preferences[open], .provider-local-nav[open]',
|
|
54
|
+
);
|
|
55
|
+
if (event.key === 'Escape' && preferences) {
|
|
56
|
+
preferences.open = false;
|
|
57
|
+
preferences.querySelector('summary')?.focus();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
commit() {
|
|
62
|
+
desktop.replaceChildren(desktopStage);
|
|
63
|
+
mobile.replaceChildren(mobileStage);
|
|
64
|
+
// Branding URLs switch only with the coordinated shell release.
|
|
65
|
+
for (const element of document.querySelectorAll<HTMLImageElement>('[data-f5-shared-brand]')) {
|
|
66
|
+
const asset = release.manifest.assets[element.dataset.f5SharedBrand || ''];
|
|
67
|
+
if (asset) element.src = asset.url;
|
|
68
|
+
}
|
|
69
|
+
const favicon = document.querySelector<HTMLLinkElement>('[data-f5-shared-favicon]');
|
|
70
|
+
if (favicon) favicon.href = release.manifest.assets.favicon.url;
|
|
71
|
+
const annotate = () => {
|
|
72
|
+
for (const link of document.querySelectorAll<HTMLAnchorElement>('.pagefind-ui__result-link')) {
|
|
73
|
+
const title = link.closest('.pagefind-ui__result-title');
|
|
74
|
+
if (!title || title.querySelector('.search-project-source')) continue;
|
|
75
|
+
const label = searchSource(link.href, release.data.searchSources);
|
|
76
|
+
if (!label) continue;
|
|
77
|
+
const source = document.createElement('span');
|
|
78
|
+
source.className = 'search-project-source';
|
|
79
|
+
source.textContent = label;
|
|
80
|
+
title.append(source);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
observer = new MutationObserver(annotate);
|
|
84
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
85
|
+
annotate();
|
|
86
|
+
// Pagefind remains the local Starlight implementation. Its merge bundles are supplied here.
|
|
87
|
+
const current = consumer.repository.split('/')[1];
|
|
88
|
+
window.F5SharedSearchV1 = {
|
|
89
|
+
mergeIndex: release.data.searchSources
|
|
90
|
+
.filter((source) => source.repo !== current)
|
|
91
|
+
.map((source) => ({
|
|
92
|
+
bundlePath: `https://f5-sales-demo.github.io/${source.repo === 'f5-sales-demo.github.io' ? '' : `${source.repo}/`}pagefind/`,
|
|
93
|
+
})),
|
|
94
|
+
};
|
|
95
|
+
document.addEventListener('keydown', dismissPreferences);
|
|
96
|
+
// Supply the focus trap missing in the archived mobile renderer.
|
|
97
|
+
document.addEventListener('keydown', trap);
|
|
98
|
+
},
|
|
99
|
+
dispose() {
|
|
100
|
+
for (const root of roots) root.unmount();
|
|
101
|
+
observer?.disconnect();
|
|
102
|
+
document.removeEventListener('keydown', dismissPreferences);
|
|
103
|
+
document.removeEventListener('keydown', trap);
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
function trap(event: KeyboardEvent) {
|
|
109
|
+
if (event.key !== 'Tab') return;
|
|
110
|
+
const dialog = document.querySelector<HTMLElement>('.smm-mobile-overlay');
|
|
111
|
+
if (!dialog) return;
|
|
112
|
+
const controls = Array.from(dialog.querySelectorAll<HTMLElement>('button, a, summary, [tabindex="0"]')).filter(
|
|
113
|
+
(element) => element.getClientRects().length,
|
|
114
|
+
);
|
|
115
|
+
const first = controls[0],
|
|
116
|
+
last = controls.at(-1);
|
|
117
|
+
if (event.shiftKey && (document.activeElement === first || document.activeElement === dialog)) {
|
|
118
|
+
event.preventDefault();
|
|
119
|
+
last?.focus();
|
|
120
|
+
} else if (!event.shiftKey && (document.activeElement === last || document.activeElement === dialog)) {
|
|
121
|
+
event.preventDefault();
|
|
122
|
+
first?.focus();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Keep Starlight's Search and Pagefind implementation local; inject only release-owned federation.
|
|
2
|
+
export function sharedSearchIntegration() {
|
|
3
|
+
return {
|
|
4
|
+
name: 'f5-shared-search',
|
|
5
|
+
enforce: 'pre',
|
|
6
|
+
transform(code, id) {
|
|
7
|
+
if (!id.includes('/@astrojs/starlight/components/Search.astro')) return;
|
|
8
|
+
return code
|
|
9
|
+
.replace('...pagefindUserConfig,', '...pagefindUserConfig, ...(window.F5SharedSearchV1 || {}),')
|
|
10
|
+
.replace(
|
|
11
|
+
'const { PagefindUI } = await import',
|
|
12
|
+
'await window.F5SharedReadyV1; const { PagefindUI } = await import',
|
|
13
|
+
);
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import baseline from './baseline.json' with { type: 'json' };
|
|
2
|
+
|
|
3
|
+
export { baseline };
|
|
4
|
+
export function sharedMode(repository = process.env.GITHUB_REPOSITORY || '') {
|
|
5
|
+
const requested = process.env.DOCS_SHARED_MODE || 'auto';
|
|
6
|
+
if (!['auto', 'local', 'publish', 'consume'].includes(requested)) throw new Error('Invalid DOCS_SHARED_MODE');
|
|
7
|
+
const root = repository === 'f5-sales-demo/f5-sales-demo.github.io';
|
|
8
|
+
const mode =
|
|
9
|
+
requested === 'auto'
|
|
10
|
+
? root
|
|
11
|
+
? 'publish'
|
|
12
|
+
: repository.startsWith('f5-sales-demo/')
|
|
13
|
+
? 'consume'
|
|
14
|
+
: 'local'
|
|
15
|
+
: requested;
|
|
16
|
+
if ((mode === 'publish') !== root && mode !== 'local')
|
|
17
|
+
throw new Error('Only the landing repository publishes the shared tree');
|
|
18
|
+
return mode;
|
|
19
|
+
}
|
|
@@ -25,3 +25,14 @@ describe('Canada-only sidebar override', () => {
|
|
|
25
25
|
expect(configuredComponents('f5-sales-demo/multi-cloud-networking')).not.toHaveProperty('Sidebar');
|
|
26
26
|
});
|
|
27
27
|
});
|
|
28
|
+
|
|
29
|
+
describe('Statistics sidebar override', () => {
|
|
30
|
+
it('selects the sidebar that restores the active reading path', () => {
|
|
31
|
+
expect(configuredComponents('f5-sales-demo/statistics').Sidebar).toBe(
|
|
32
|
+
'@f5-sales-demo/docs-theme/components/StatisticsSidebar.astro',
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
it('preserves unrelated repository sidebars', () => {
|
|
36
|
+
expect(configuredComponents('f5-sales-demo/statistics-extra')).not.toHaveProperty('Sidebar');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -66,9 +66,9 @@ describe('Statistics publication', () => {
|
|
|
66
66
|
});
|
|
67
67
|
expect(repositoryPublicationProfile('statistics')?.favicon).toBeUndefined();
|
|
68
68
|
});
|
|
69
|
-
it('preserves
|
|
69
|
+
it('preserves existing routes and places Shell Scripts before Infrastructure', () => {
|
|
70
70
|
const sidebar = publicationSidebar('f5-sales-demo/statistics', []);
|
|
71
|
-
expect(sidebar?.slice(0,
|
|
71
|
+
expect(sidebar?.slice(0, 9)).toEqual([
|
|
72
72
|
{ slug: 'index' },
|
|
73
73
|
{ slug: 'access-logs' },
|
|
74
74
|
{ slug: 'service-graph' },
|
|
@@ -78,9 +78,33 @@ describe('Statistics publication', () => {
|
|
|
78
78
|
{ slug: 'firewall-metrics' },
|
|
79
79
|
{ slug: 'security-events' },
|
|
80
80
|
{ slug: 'troubleshooting' },
|
|
81
|
-
{ slug: 'api-catalog' },
|
|
82
|
-
{ slug: 'setup' },
|
|
83
81
|
]);
|
|
82
|
+
const catalog = sidebar?.[9];
|
|
83
|
+
expect(catalog).toMatchObject({ label: 'Statistics APIs', collapsed: true });
|
|
84
|
+
if (!catalog || !('items' in catalog)) throw new Error('Statistics APIs must be a submenu');
|
|
85
|
+
const expected = [
|
|
86
|
+
'api-catalog',
|
|
87
|
+
'api-catalog/query-concepts',
|
|
88
|
+
'api-catalog/application-traffic',
|
|
89
|
+
'api-catalog/api-analytics',
|
|
90
|
+
'api-catalog/application-security',
|
|
91
|
+
'api-catalog/bot-defense',
|
|
92
|
+
'api-catalog/client-side-defense',
|
|
93
|
+
'api-catalog/device-data-intelligence',
|
|
94
|
+
'api-catalog/ddos-protection',
|
|
95
|
+
'api-catalog/dns',
|
|
96
|
+
'api-catalog/cdn',
|
|
97
|
+
'api-catalog/networking',
|
|
98
|
+
'api-catalog/kubernetes-storage',
|
|
99
|
+
'api-catalog/logs-events-alerts',
|
|
100
|
+
'api-catalog/synthetic-monitoring',
|
|
101
|
+
'api-catalog/billing-usage',
|
|
102
|
+
];
|
|
103
|
+
expect(catalog.items.map((item) => ('slug' in item ? item.slug : ''))).toEqual(expected);
|
|
104
|
+
expect(catalog.items.every((item) => 'label' in item && item.label.split(/\s+/).length <= 3)).toBe(true);
|
|
105
|
+
expect(catalog.items[0]).toEqual({ label: 'Overview', slug: 'api-catalog' });
|
|
106
|
+
expect(catalog.items[1]).toEqual({ label: 'Query concepts', slug: 'api-catalog/query-concepts' });
|
|
107
|
+
expect(sidebar?.[10]).toEqual({ slug: 'setup' });
|
|
84
108
|
expect(sidebar?.[11]).toEqual({ slug: 'shell-scripts' });
|
|
85
109
|
expect(sidebar?.[12]).toEqual({
|
|
86
110
|
label: 'Infrastructure',
|
|
@@ -95,8 +119,8 @@ describe('Statistics publication', () => {
|
|
|
95
119
|
? item.items.map((child) => ('slug' in child ? child.slug : ''))
|
|
96
120
|
: [],
|
|
97
121
|
);
|
|
98
|
-
expect(routes).toHaveLength(
|
|
99
|
-
expect(new Set(routes).size).toBe(
|
|
122
|
+
expect(routes).toHaveLength(29);
|
|
123
|
+
expect(new Set(routes).size).toBe(29);
|
|
100
124
|
});
|
|
101
125
|
it('preserves supplied fallback navigation for other repositories', () => {
|
|
102
126
|
const fallback = [{ slug: 'index' }, { label: 'Existing', items: [{ slug: 'existing' }] }];
|
|
@@ -24,7 +24,28 @@ export const publicationProfiles: Record<string, PublicationProfile> = {
|
|
|
24
24
|
{ slug: 'firewall-metrics' },
|
|
25
25
|
{ slug: 'security-events' },
|
|
26
26
|
{ slug: 'troubleshooting' },
|
|
27
|
-
{
|
|
27
|
+
{
|
|
28
|
+
label: 'Statistics APIs',
|
|
29
|
+
collapsed: true,
|
|
30
|
+
items: [
|
|
31
|
+
{ label: 'Overview', slug: 'api-catalog' },
|
|
32
|
+
{ label: 'Query concepts', slug: 'api-catalog/query-concepts' },
|
|
33
|
+
{ label: 'Application traffic', slug: 'api-catalog/application-traffic' },
|
|
34
|
+
{ label: 'API analytics', slug: 'api-catalog/api-analytics' },
|
|
35
|
+
{ label: 'Application security', slug: 'api-catalog/application-security' },
|
|
36
|
+
{ label: 'Bot defense', slug: 'api-catalog/bot-defense' },
|
|
37
|
+
{ label: 'Client-side defense', slug: 'api-catalog/client-side-defense' },
|
|
38
|
+
{ label: 'Device intelligence', slug: 'api-catalog/device-data-intelligence' },
|
|
39
|
+
{ label: 'DDoS protection', slug: 'api-catalog/ddos-protection' },
|
|
40
|
+
{ label: 'DNS', slug: 'api-catalog/dns' },
|
|
41
|
+
{ label: 'CDN', slug: 'api-catalog/cdn' },
|
|
42
|
+
{ label: 'Networking', slug: 'api-catalog/networking' },
|
|
43
|
+
{ label: 'Kubernetes & storage', slug: 'api-catalog/kubernetes-storage' },
|
|
44
|
+
{ label: 'Logs & events', slug: 'api-catalog/logs-events-alerts' },
|
|
45
|
+
{ label: 'Synthetic monitoring', slug: 'api-catalog/synthetic-monitoring' },
|
|
46
|
+
{ label: 'Billing & usage', slug: 'api-catalog/billing-usage' },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
28
49
|
{ slug: 'setup' },
|
|
29
50
|
{ slug: 'shell-scripts' },
|
|
30
51
|
{
|
|
@@ -1,32 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
];
|
|
1
|
+
// Explicit local builds may supply their own federation in createF5xcDocsConfig.
|
|
2
|
+
export const localSearchSites: Array<{ repo: string; label: string }> = [];
|
|
3
|
+
export const federatedSearchSites = localSearchSites;
|
|
@@ -1,34 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { expect, it } from 'vitest';
|
|
2
2
|
import { defaultMegaMenuItems } from '../../config';
|
|
3
|
-
import { localizeEcosystemHref } from './localize-ecosystem-href';
|
|
4
3
|
|
|
5
|
-
|
|
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
|
+
it('leaves production menu content to the root publisher', () => expect(defaultMegaMenuItems).toEqual([]));
|
|
@@ -1,33 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { defaultMegaMenuItems, federatedSearchBundlePaths, federatedSearchSites } from '../../config';
|
|
1
|
+
import { expect, it } from 'vitest';
|
|
3
2
|
import { localizeEcosystemHref } from './localize-ecosystem-href';
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const statistics = category?.items?.filter((item) => item.label === 'Statistics');
|
|
10
|
-
expect(statistics).toHaveLength(1);
|
|
11
|
-
expect(statistics?.[0]).toMatchObject({
|
|
12
|
-
href: 'https://f5-sales-demo.github.io/statistics/en/',
|
|
13
|
-
description: 'Query access logs, application metrics, and security telemetry through the API.',
|
|
14
|
-
});
|
|
15
|
-
expect(category?.items?.map((item) => item.label)).toEqual(['Statistics', 'Observability', 'Administration']);
|
|
16
|
-
});
|
|
17
|
-
it('adds Statistics to federated search and avoids self-index duplication', () => {
|
|
18
|
-
expect(federatedSearchSites.filter((item) => item.repo === 'statistics')).toHaveLength(1);
|
|
19
|
-
expect(federatedSearchBundlePaths('https://f5-sales-demo.github.io', '/')).toContain(
|
|
20
|
-
'https://f5-sales-demo.github.io/statistics/pagefind/',
|
|
4
|
+
it('retains explicit English Statistics destinations', () => {
|
|
5
|
+
for (const locale of ['en', 'fr', 'ar'])
|
|
6
|
+
expect(localizeEcosystemHref('https://f5-sales-demo.github.io/statistics/en/', locale)).toBe(
|
|
7
|
+
'https://f5-sales-demo.github.io/statistics/en/',
|
|
21
8
|
);
|
|
22
|
-
expect(federatedSearchBundlePaths('https://f5-sales-demo.github.io', '/statistics')).not.toContain(
|
|
23
|
-
'https://f5-sales-demo.github.io/statistics/pagefind/',
|
|
24
|
-
);
|
|
25
|
-
});
|
|
26
|
-
it('keeps the published English destination for every reader locale', () => {
|
|
27
|
-
for (const locale of ['en', 'fr', 'ja']) {
|
|
28
|
-
expect(localizeEcosystemHref('https://f5-sales-demo.github.io/statistics/en/', locale)).toBe(
|
|
29
|
-
'https://f5-sales-demo.github.io/statistics/en/',
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
9
|
});
|