@f5-sales-demo/docs-theme 4.3.15 → 4.4.1

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.
@@ -0,0 +1,40 @@
1
+ import fs from 'node:fs';
2
+ import { defineRouteMiddleware } from '@astrojs/starlight/route-data';
3
+ import type { ProviderPage } from './src/utils/canonical-provider.ts';
4
+ import { collectionNavigation, providerSections } from './src/utils/canonical-provider.ts';
5
+
6
+ let providerNavigation: { collections: Record<string, ProviderPage[]> };
7
+ export const onRequest = defineRouteMiddleware(async (context, next) => {
8
+ const route = context.locals.starlightRoute;
9
+ const entry = route.entry;
10
+ const canonical = process.env.DOCS_PROFILE === 'canonical-provider';
11
+ if (canonical) {
12
+ providerNavigation ||= JSON.parse(fs.readFileSync(process.env.PROVIDER_NAVIGATION || '', 'utf8'));
13
+ const metadata = entry.data.xcsh_docs;
14
+ const base = import.meta.env.BASE_URL;
15
+ const sections = providerSections.map((section) => ({
16
+ type: 'link' as const,
17
+ label: section.label,
18
+ href: `${base.replace(/\/$/, '')}/${section.path}/`,
19
+ isCurrent: route.id === section.path,
20
+ badge: undefined,
21
+ attrs: {},
22
+ }));
23
+ route.sidebar = [
24
+ ...sections,
25
+ ...(metadata
26
+ ? collectionNavigation(
27
+ providerNavigation.collections[metadata.collection_id] || [],
28
+ metadata.collection_id,
29
+ base,
30
+ route.id,
31
+ )
32
+ : []),
33
+ ];
34
+ route.hasSidebar = true;
35
+ route.pagination = { prev: undefined, next: undefined };
36
+ route.siteTitleHref = base;
37
+ }
38
+
39
+ await next();
40
+ });
@@ -1,7 +1,13 @@
1
1
  ---
2
+ import ProviderReference from './ProviderReference.astro';
3
+
4
+ const canonical = process.env.DOCS_PROFILE === 'canonical-provider';
5
+
2
6
  import ImageZoom from 'starlight-image-zoom/components/ImageZoom.astro';
3
7
  import StarlightVideosMarkdownContent from 'starlight-videos/components/MarkdownContent.astro';
4
8
  ---
5
9
 
6
10
  <ImageZoom />
7
- <StarlightVideosMarkdownContent><slot /></StarlightVideosMarkdownContent>
11
+ <StarlightVideosMarkdownContent>
12
+ {canonical ? <ProviderReference><slot /></ProviderReference> : <slot />}
13
+ </StarlightVideosMarkdownContent>
@@ -0,0 +1,55 @@
1
+ ---
2
+ import { CardGrid } from '@astrojs/starlight/components';
3
+ import { providerSections } from '../src/utils/canonical-provider';
4
+ import LinkCard from './LinkCard.astro';
5
+
6
+ const { entry } = Astro.locals.starlightRoute;
7
+ const metadata = entry.data.xcsh_docs;
8
+ const base = import.meta.env.BASE_URL.replace(/\/$/, '');
9
+ ---
10
+ {!metadata && (entry.id === 'index' || entry.id === '') && <CardGrid>
11
+ {providerSections.map(section => <LinkCard title={section.label} href={`${base}/${section.path}/`} />)}
12
+ </CardGrid>}
13
+ <label class="version-control">Documentation version
14
+ <select id="provider-version" aria-label="Documentation version"><option>{process.env.DOCUMENTATION_LABEL || 'Latest stable'}</option></select>
15
+ </label>
16
+ <slot />
17
+ {metadata && <footer class="provider-provenance">
18
+ <p>Schema path: <code>{metadata.schema_path.join('.') || 'root'}</code> · <a href={metadata.source_url}>Complete Markdown</a></p>
19
+ <small>{metadata.id} · {metadata.body_sha256}</small>
20
+ </footer>}
21
+ <style>
22
+ .provider-provenance { border-top: 1px solid var(--sl-color-gray-5); margin-top: 2rem; padding-top: 1rem; overflow-wrap: anywhere; }
23
+ .provider-provenance p { margin-bottom: .5rem; }
24
+ .provider-provenance small { color: var(--sl-color-gray-3); }
25
+ </style>
26
+
27
+ <script>
28
+ const select = document.querySelector<HTMLSelectElement>('#provider-version');
29
+ const publicationRoot = '/terraform-provider-xcsh/';
30
+ if (select) {
31
+ fetch(`${publicationRoot}documentation-versions.json`).then(response => response.json()).then(data => {
32
+ const path = window.location.pathname.slice(publicationRoot.length);
33
+ const prefix = path.startsWith('versions/') ? path.split('/').slice(0, 2).join('/') + '/' : path.startsWith('preview/main/') ? 'preview/main/' : '';
34
+ const equivalent = path.slice(prefix.length);
35
+ const versions = [{ label: `Latest stable (${data.latest})`, prefix: '' }, ...data.versions.map((version: string) => ({ label: version, prefix: `versions/${version}/` })), { label: 'Main preview', prefix: 'preview/main/' }];
36
+ select.replaceChildren(...versions.map(version => {
37
+ const option = document.createElement('option');
38
+ option.textContent = version.label;
39
+ option.value = version.prefix;
40
+ option.selected = prefix === version.prefix;
41
+ return option;
42
+ }));
43
+ select.addEventListener('change', async () => {
44
+ const landing = `${publicationRoot}${select.value}`;
45
+ const destination = `${landing}${equivalent}`;
46
+ const response = await fetch(destination, { method: 'HEAD' });
47
+ window.location.assign(response.ok ? destination : landing);
48
+ });
49
+ }).catch(() => { select.disabled = true; });
50
+ }
51
+ </script>
52
+ <style>
53
+ .version-control { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: 1rem; font-size: var(--sl-text-sm); }
54
+ select { max-width: 100%; padding: .25rem; }
55
+ </style>
package/config.ts CHANGED
@@ -31,6 +31,7 @@ import {
31
31
  } from './src/i18n/mega-menu-translations.ts';
32
32
  import { sidebarTranslations } from './src/i18n/translations.ts';
33
33
  import remarkMermaid from './src/plugins/remark-mermaid.mjs';
34
+ import { providerMegaMenu } from './src/utils/canonical-provider.ts';
34
35
  import { resolveMegaMenuIcon } from './src/utils/resolve-icon.ts';
35
36
  import { buildSubcategorySidebar } from './src/utils/subcategory-sidebar.ts';
36
37
 
@@ -100,6 +101,7 @@ interface HeadEntry {
100
101
  }
101
102
 
102
103
  export interface F5xcDocsConfigOptions {
104
+ canonicalProvider?: { contentRoot: string; manifest: string; navigation: string; version: string };
103
105
  site?: string;
104
106
  base?: string;
105
107
  title?: string;
@@ -651,6 +653,19 @@ export const federatedSearchSites = [
651
653
  ];
652
654
 
653
655
  export function createF5xcDocsConfig(options: F5xcDocsConfigOptions = {}) {
656
+ const canonicalProvider =
657
+ options.canonicalProvider ||
658
+ (process.env.DOCS_PROFILE === 'canonical-provider'
659
+ ? {
660
+ contentRoot: process.env.CONTENT_DIR || 'src/content/docs',
661
+ manifest: process.env.CANONICAL_MANIFEST || '',
662
+ navigation: process.env.PROVIDER_NAVIGATION || '',
663
+ version: process.env.DOCUMENTATION_LABEL || 'Latest stable',
664
+ }
665
+ : undefined);
666
+ if (canonicalProvider && (!canonicalProvider.manifest || !canonicalProvider.navigation)) {
667
+ throw new Error('[docs-theme] canonical-provider requires manifest and navigation');
668
+ }
654
669
  const site = options.site || process.env.DOCS_SITE || 'https://f5-sales-demo.github.io';
655
670
  const base = options.base || process.env.DOCS_BASE || '/';
656
671
  const title = options.title || process.env.DOCS_TITLE || 'Documentation';
@@ -696,13 +711,13 @@ export function createF5xcDocsConfig(options: F5xcDocsConfigOptions = {}) {
696
711
  console.warn('[docs-theme] OPENAPI_SPECS_CONFIG contains invalid JSON; skipping OpenAPI plugin.', e);
697
712
  }
698
713
  }
699
- const megaMenuItems = options.megaMenuItems || defaultMegaMenuItems;
714
+ const megaMenuItems = options.megaMenuItems || (canonicalProvider ? providerMegaMenu(base) : defaultMegaMenuItems);
700
715
  const head = options.head || defaultHead;
701
716
  const logo = options.logo || { src: '@f5-sales-demo/docs-theme/assets/f5-distributed-cloud.svg' };
702
717
  const additionalRemarkPlugins = options.additionalRemarkPlugins || [];
703
718
  const additionalIntegrations = options.additionalIntegrations || [];
704
719
 
705
- const federatedSearch = options.federatedSearch !== false;
720
+ const federatedSearch = !canonicalProvider && options.federatedSearch !== false;
706
721
  const normalizedBase = base.replace(/\/+$/, '');
707
722
  const repositoryCorpusPolicy = progressiveCorpusPolicy(llmsConfig);
708
723
  const progressiveCorpus =
@@ -776,29 +791,35 @@ export function createF5xcDocsConfig(options: F5xcDocsConfigOptions = {}) {
776
791
  ),
777
792
  ]
778
793
  : []),
779
- starlightLlmsTxt({
780
- projectName: title,
781
- description,
782
- rawContent: false,
783
- optionalLinks: llmsOptionalLinks,
784
- sidebarNav: true,
785
- tieredHierarchy: true,
786
- promote: llmsConfig.promote || ['index*', 'overview*'],
787
- demote: llmsConfig.demote || ['references*'],
788
- ...(llmsFederatedSites.length > 0 ? { federatedSites: llmsFederatedSites } : {}),
789
- ...(llmsFederatedSiteCategories.length > 0 ? { federatedSiteCategories: llmsFederatedSiteCategories } : {}),
790
- ...(progressiveCorpus ? { progressiveCorpus } : {}),
791
- }),
794
+ ...(!canonicalProvider
795
+ ? [
796
+ starlightLlmsTxt({
797
+ projectName: title,
798
+ description,
799
+ rawContent: false,
800
+ optionalLinks: llmsOptionalLinks,
801
+ sidebarNav: true,
802
+ tieredHierarchy: true,
803
+ promote: llmsConfig.promote || ['index*', 'overview*'],
804
+ demote: llmsConfig.demote || ['references*'],
805
+ ...(llmsFederatedSites.length > 0 ? { federatedSites: llmsFederatedSites } : {}),
806
+ ...(llmsFederatedSiteCategories.length > 0 ? { federatedSiteCategories: llmsFederatedSiteCategories } : {}),
807
+ ...(progressiveCorpus ? { progressiveCorpus } : {}),
808
+ }),
809
+ ]
810
+ : []),
792
811
  ];
793
812
 
794
813
  const contentDir = process.env.CONTENT_DIR || 'src/content/docs';
795
- const subcategorySidebar = buildSubcategorySidebar(contentDir);
814
+ const subcategorySidebar = canonicalProvider ? [] : buildSubcategorySidebar(contentDir);
796
815
 
797
816
  // Auto-detect i18n: enable locales only when content has an en/ subdirectory.
798
817
  // Repos that haven't migrated to docs/en/ won't get a broken language selector.
799
818
  const hasEnSubdir = fs.existsSync(path.resolve(contentDir, 'en'));
800
819
  const resolvedLocales =
801
- options.locales === false ? undefined : options.locales || (hasEnSubdir ? f5xcDefaultLocales : undefined);
820
+ canonicalProvider || options.locales === false
821
+ ? undefined
822
+ : options.locales || (hasEnSubdir ? f5xcDefaultLocales : undefined);
802
823
  const resolvedDefaultLocale = options.defaultLocale || f5xcDefaultLocale;
803
824
 
804
825
  const localeHeadScripts: HeadEntry[] = [];
@@ -848,6 +869,9 @@ export function createF5xcDocsConfig(options: F5xcDocsConfigOptions = {}) {
848
869
  return defineConfig({
849
870
  site,
850
871
  base,
872
+ ...(canonicalProvider
873
+ ? { experimental: { collectionStorage: { type: 'chunked' as const, chunkSize: 1024 * 1024 } } }
874
+ : {}),
851
875
  ...(resolvedLocales ? { redirects: { '/': `${normalizedBase}/${resolvedDefaultLocale}/` } } : {}),
852
876
  markdown: {
853
877
  processor: unified({
package/index.ts CHANGED
@@ -10,8 +10,11 @@ export default function f5xcDocsTheme(): StarlightPlugin {
10
10
  },
11
11
  'config:setup'({ config, updateConfig, addRouteMiddleware, logger }) {
12
12
  addRouteMiddleware({
13
- entrypoint: '@f5-sales-demo/docs-theme/route-middleware',
14
- order: 'pre',
13
+ entrypoint:
14
+ process.env.DOCS_PROFILE === 'canonical-provider'
15
+ ? '@f5-sales-demo/docs-theme/canonical-route-middleware'
16
+ : '@f5-sales-demo/docs-theme/route-middleware',
17
+ order: process.env.DOCS_PROFILE === 'canonical-provider' ? 'post' : 'pre',
15
18
  });
16
19
  updateConfig({
17
20
  customCss: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@f5-sales-demo/docs-theme",
3
- "version": "4.3.15",
3
+ "version": "4.4.1",
4
4
  "description": "F5 Distributed Cloud branded Starlight documentation theme",
5
5
  "type": "module",
6
6
  "engines": {
@@ -40,7 +40,8 @@
40
40
  "./src/i18n/locales.ts": "./src/i18n/locales.ts",
41
41
  "./src/i18n/translator.ts": "./src/i18n/translator.ts",
42
42
  "./src/i18n/prompt.ts": "./src/i18n/prompt.ts",
43
- "./src/utils/localize-ecosystem-href.ts": "./src/utils/localize-ecosystem-href.ts"
43
+ "./src/utils/localize-ecosystem-href.ts": "./src/utils/localize-ecosystem-href.ts",
44
+ "./canonical-route-middleware": "./canonical-route-middleware.ts"
44
45
  },
45
46
  "files": [
46
47
  "index.ts",
@@ -52,7 +53,8 @@
52
53
  "styles/",
53
54
  "assets/",
54
55
  "components/",
55
- "src/"
56
+ "src/",
57
+ "canonical-route-middleware.ts"
56
58
  ],
57
59
  "keywords": [
58
60
  "starlight",
@@ -70,7 +72,7 @@
70
72
  },
71
73
  "devDependencies": {
72
74
  "@astrojs/markdown-remark": "^7.2.4",
73
- "@astrojs/starlight": "^0.41.9",
75
+ "@astrojs/starlight": "0.41.9",
74
76
  "@f5-sales-demo/icons-f5-brand": "^0.2.0",
75
77
  "@f5-sales-demo/icons-f5xc": "^0.3.2",
76
78
  "@f5-sales-demo/icons-hashicorp-flight": "^0.3.0",
@@ -82,7 +84,7 @@
82
84
  "@playwright/test": "^1.62.1",
83
85
  "@types/react": "^19.2.18",
84
86
  "@types/react-dom": "^19.2.7",
85
- "astro": "^7.2.8",
87
+ "astro": "7.2.8",
86
88
  "react": "^19.2.8",
87
89
  "react-dom": "^19.2.8",
88
90
  "vitest": "^4.1.11"
@@ -1,6 +1,7 @@
1
1
  import type { StarlightRouteData } from '@astrojs/starlight/route-data';
2
2
  import { defineRouteMiddleware } from '@astrojs/starlight/route-data';
3
3
  import { itemLabels, menuLabels } from './src/i18n/mega-menu-translations.ts';
4
+ import { isCanonicalReference } from './src/utils/canonical-provider.ts';
4
5
 
5
6
  const titleTranslations: Record<string, Record<string, string>> = { ...menuLabels, ...itemLabels };
6
7
 
@@ -55,7 +56,7 @@ export const onRequest = defineRouteMiddleware(async (context, next) => {
55
56
  const route = context.locals.starlightRoute;
56
57
  const entry = route.entry;
57
58
 
58
- if (isIndexPage(entry.filePath)) {
59
+ if (!isCanonicalReference(entry.data) && isIndexPage(entry.filePath)) {
59
60
  // Unconditionally hide TOC on index pages
60
61
  route.toc = undefined;
61
62
  }
@@ -5,6 +5,24 @@ import { docsSchema } from '@astrojs/starlight/schema';
5
5
  export const collections = {
6
6
  docs: defineCollection({
7
7
  loader: docsLoader(),
8
- schema: docsSchema({ extend: z.object({ subcategory: z.string().optional() }) }),
8
+ schema: docsSchema({
9
+ extend: z.object({
10
+ subcategory: z.string().optional(),
11
+ xcsh_docs: z
12
+ .object({
13
+ id: z.string(),
14
+ collection_id: z.string(),
15
+ role: z.string(),
16
+ parent_id: z.string().nullable(),
17
+ child_ids: z.array(z.string()),
18
+ schema_path: z.array(z.string()),
19
+ provider_type: z.string(),
20
+ body_sha256: z.string(),
21
+ source_url: z.string().optional(),
22
+ })
23
+ .passthrough()
24
+ .optional(),
25
+ }),
26
+ }),
9
27
  }),
10
28
  };
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { collectionNavigation, isCanonicalReference, providerSections } from './canonical-provider';
3
+
4
+ describe('canonical provider navigation', () => {
5
+ it('includes all four provider families and setup and guides', () => {
6
+ expect(providerSections.map((s) => s.path)).toEqual([
7
+ 'provider/setup',
8
+ 'guides',
9
+ 'resources',
10
+ 'data-sources',
11
+ 'actions',
12
+ 'ephemeral-resources',
13
+ ]);
14
+ });
15
+ it('retains canonical index navigation and TOC', () => {
16
+ expect(isCanonicalReference({ xcsh_docs: { role: 'reference' } })).toBe(true);
17
+ expect(isCanonicalReference({})).toBe(false);
18
+ });
19
+ it('includes only the active collection, with parent relationships', () => {
20
+ const pages = [
21
+ { id: 'a', collection_id: 'one', parent_id: null, title: 'One', slug: 'resources/one' },
22
+ { id: 'b', collection_id: 'one', parent_id: 'a', title: 'Property', slug: 'resources/one/properties' },
23
+ { id: 'c', collection_id: 'two', parent_id: null, title: 'Two', slug: 'resources/two' },
24
+ ];
25
+ const nav = collectionNavigation(pages, 'one', '/version/', 'resources/one/properties');
26
+ expect(JSON.stringify(nav)).not.toContain('Two');
27
+ expect(nav[0].entries[1].href).toBe('/version/resources/one/properties/');
28
+ expect(nav[0].entries[1].isCurrent).toBe(true);
29
+ });
30
+ });
31
+
32
+ describe('bounded collection expansion', () => {
33
+ it('keeps sibling references and expands only the exact active ancestry', () => {
34
+ const pages = [
35
+ { id: 'root', collection_id: 'c', parent_id: null, title: 'Root', slug: 'resources/demo' },
36
+ { id: 'a', collection_id: 'c', parent_id: 'root', title: 'A', slug: 'resources/demo/properties/a' },
37
+ { id: 'aa', collection_id: 'c', parent_id: 'a', title: 'AA', slug: 'resources/demo/properties/a/child' },
38
+ { id: 'b', collection_id: 'c', parent_id: 'root', title: 'B', slug: 'resources/demo/properties/ab' },
39
+ { id: 'bb', collection_id: 'c', parent_id: 'b', title: 'BB', slug: 'resources/demo/properties/ab/child' },
40
+ ];
41
+ const nav = collectionNavigation(pages, 'c', '/', 'resources/demo/properties/a/child');
42
+ const text = JSON.stringify(nav);
43
+ expect(text).toContain('AA');
44
+ expect(text).toContain('"label":"B"');
45
+ expect(text).not.toContain('BB');
46
+ expect(nav[0].type).toBe('group');
47
+ });
48
+ });
@@ -0,0 +1,76 @@
1
+ import type { StarlightRouteData } from '@astrojs/starlight/route-data';
2
+ export const providerSections = [
3
+ { label: 'Setup', path: 'provider/setup', icon: 'seti:terraform' },
4
+ { label: 'Guides', path: 'guides', icon: 'open-book' },
5
+ { label: 'Resources', path: 'resources', icon: 'puzzle' },
6
+ { label: 'Data sources', path: 'data-sources', icon: 'document' },
7
+ { label: 'Actions', path: 'actions', icon: 'rocket' },
8
+ { label: 'Ephemeral resources', path: 'ephemeral-resources', icon: 'random' },
9
+ ];
10
+
11
+ export interface ProviderPage {
12
+ id: string;
13
+ collection_id: string;
14
+ parent_id: string | null;
15
+ title: string;
16
+ slug: string;
17
+ }
18
+
19
+ export function isCanonicalReference(data: { xcsh_docs?: { role?: string } }) {
20
+ return !!data.xcsh_docs?.role;
21
+ }
22
+
23
+ export function collectionNavigation(pages: ProviderPage[], collection: string, base: string, current: string) {
24
+ const active = pages.filter((page) => page.collection_id === collection);
25
+ const ids = new Set(active.map((page) => page.id));
26
+ const children = new Map<string | null, ProviderPage[]>();
27
+ for (const page of active) {
28
+ const parent = page.parent_id && ids.has(page.parent_id) ? page.parent_id : null;
29
+ children.set(parent, [...(children.get(parent) || []), page]);
30
+ }
31
+ const link = (page: ProviderPage) => ({
32
+ type: 'link' as const,
33
+ label: page.title,
34
+ href: `${base.replace(/\/$/, '')}/${page.slug}/`,
35
+ isCurrent: page.slug === current,
36
+ badge: undefined,
37
+ attrs: {},
38
+ });
39
+ const tree = (page: ProviderPage, seen = new Set<string>()): StarlightRouteData['sidebar'][number] => {
40
+ if (seen.has(page.id)) throw new Error(`Cyclic canonical navigation: ${page.id}`);
41
+ const descendants = children.get(page.id) || [];
42
+ // Other branches remain navigable through their reference page, whose
43
+ // content lists every direct child. Expand only the active ancestry.
44
+ if (!descendants.length || !(current === page.slug || current.startsWith(`${page.slug}/`))) return link(page);
45
+ const next = new Set([...seen, page.id]);
46
+ return {
47
+ type: 'group',
48
+ label: page.title,
49
+ collapsed: !current.startsWith(page.slug),
50
+ badge: undefined,
51
+ entries: [link(page), ...descendants.map((child) => tree(child, next))],
52
+ };
53
+ };
54
+ return (children.get(null) || []).map((page) => tree(page));
55
+ }
56
+
57
+ export function providerMegaMenu(base: string) {
58
+ return [
59
+ {
60
+ label: 'Provider documentation',
61
+ content: {
62
+ layout: 'grid' as const,
63
+ columns: 2 as const,
64
+ categories: [
65
+ {
66
+ title: 'Terraform xcsh',
67
+ items: providerSections.map((section) => ({
68
+ label: section.label,
69
+ href: `${base.replace(/\/$/, '')}/${section.path}/`,
70
+ })),
71
+ },
72
+ ],
73
+ },
74
+ },
75
+ ];
76
+ }