@f5-sales-demo/docs-theme 3.9.46 → 3.9.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ # XC Docs Theme
2
+
1
3
  🌐 English |
2
4
  [日本語](https://f5-sales-demo.github.io/docs-theme/ja/) |
3
5
  [한국어](https://f5-sales-demo.github.io/docs-theme/ko/) |
@@ -12,8 +14,6 @@
12
14
  [हिन्दी](https://f5-sales-demo.github.io/docs-theme/hi/) |
13
15
  [ไทย](https://f5-sales-demo.github.io/docs-theme/th/)
14
16
 
15
- # XC Docs Theme
16
-
17
17
  [![GitHub Pages Deploy](https://github.com/f5-sales-demo/docs-theme/actions/workflows/github-pages-deploy.yml/badge.svg)](https://github.com/f5-sales-demo/docs-theme/actions/workflows/github-pages-deploy.yml)
18
18
  [![Repository Settings](https://github.com/f5-sales-demo/docs-theme/actions/workflows/enforce-repo-settings.yml/badge.svg)](https://github.com/f5-sales-demo/docs-theme/actions/workflows/enforce-repo-settings.yml)
19
19
  [![Release](https://github.com/f5-sales-demo/docs-theme/actions/workflows/release.yml/badge.svg)](https://github.com/f5-sales-demo/docs-theme/actions/workflows/release.yml)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@f5-sales-demo/docs-theme",
3
- "version": "3.9.46",
3
+ "version": "3.9.48",
4
4
  "description": "F5 Distributed Cloud branded Starlight documentation theme",
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,5 +1,21 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { filePathToSlug } from './subcategory-sidebar';
1
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import path from 'node:path';
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
+ import { buildSubcategorySidebar, filePathToSlug } from './subcategory-sidebar';
6
+
7
+ const workspaces: string[] = [];
8
+
9
+ afterEach(async () => {
10
+ await Promise.all(workspaces.splice(0).map((workspace) => rm(workspace, { force: true, recursive: true })));
11
+ });
12
+
13
+ async function writeDoc(root: string, relativePath: string, title: string, order?: number): Promise<void> {
14
+ const filePath = path.join(root, relativePath);
15
+ await mkdir(path.dirname(filePath), { recursive: true });
16
+ const sidebar = order === undefined ? '' : `sidebar:\n order: ${order}\n`;
17
+ await writeFile(filePath, `---\ntitle: ${title}\n${sidebar}---\n\nContent.\n`);
18
+ }
3
19
 
4
20
  describe('filePathToSlug', () => {
5
21
  it('lowercases capitalised path segments to match Starlight entry slugs', () => {
@@ -23,3 +39,83 @@ describe('filePathToSlug', () => {
23
39
  expect(filePathToSlug('Enhancements\\foo.mdx')).toBe('/enhancements/foo/');
24
40
  });
25
41
  });
42
+
43
+ describe('buildSubcategorySidebar without subcategories', () => {
44
+ it('preserves three directory levels, index landings, and sidebar order', async () => {
45
+ const workspace = await mkdtemp(path.join(tmpdir(), 'docs-theme-sidebar-'));
46
+ workspaces.push(workspace);
47
+ const contentDir = path.join(workspace, 'content');
48
+ const docsDir = path.join(contentDir, 'en');
49
+
50
+ await writeDoc(docsDir, 'index.mdx', 'Home', 0);
51
+ await writeDoc(docsDir, 'reference/index.mdx', 'Reference', 1);
52
+ await writeDoc(docsDir, 'demo/index.mdx', 'Demo', 2);
53
+ await writeDoc(docsDir, 'demo/verify.mdx', 'Verify', 20);
54
+ await writeDoc(docsDir, 'demo/deploy.mdx', 'Deploy', 10);
55
+ await writeDoc(docsDir, 'demo/troubleshooting/index.mdx', 'Troubleshooting', 5);
56
+ await writeDoc(docsDir, 'demo/troubleshooting/bgp/index.mdx', 'BGP', 1);
57
+ await writeDoc(docsDir, 'demo/troubleshooting/bgp/routes.mdx', 'Routes', 1);
58
+
59
+ const sidebar = buildSubcategorySidebar(contentDir);
60
+
61
+ expect(sidebar?.[0]).toMatchObject({ label: 'Overview', link: '/' });
62
+ expect(sidebar?.slice(1)).toEqual([
63
+ {
64
+ label: 'Reference',
65
+ collapsed: true,
66
+ translations: expect.any(Object),
67
+ items: [{ label: 'Overview', slug: 'reference', translations: expect.any(Object) }],
68
+ },
69
+ {
70
+ label: 'Demo',
71
+ collapsed: true,
72
+ items: [
73
+ { label: 'Overview', slug: 'demo', translations: expect.any(Object) },
74
+ {
75
+ label: 'Troubleshooting',
76
+ collapsed: true,
77
+ translations: expect.any(Object),
78
+ items: [
79
+ { label: 'Overview', slug: 'demo/troubleshooting', translations: expect.any(Object) },
80
+ {
81
+ label: 'BGP',
82
+ collapsed: true,
83
+ items: [
84
+ { label: 'Overview', slug: 'demo/troubleshooting/bgp', translations: expect.any(Object) },
85
+ { slug: 'demo/troubleshooting/bgp/routes' },
86
+ ],
87
+ },
88
+ ],
89
+ },
90
+ { slug: 'demo/deploy' },
91
+ { slug: 'demo/verify' },
92
+ ],
93
+ },
94
+ ]);
95
+ });
96
+
97
+ it('falls back to title ordering when sidebar order is absent', async () => {
98
+ const workspace = await mkdtemp(path.join(tmpdir(), 'docs-theme-sidebar-'));
99
+ workspaces.push(workspace);
100
+ const contentDir = path.join(workspace, 'content');
101
+
102
+ await writeDoc(contentDir, 'guides/index.mdx', 'Guides');
103
+ await writeDoc(contentDir, 'guides/z-last.mdx', 'Zebra');
104
+ await writeDoc(contentDir, 'guides/a-first.mdx', 'Alpha');
105
+
106
+ const sidebar = buildSubcategorySidebar(contentDir);
107
+
108
+ expect(sidebar).toEqual([
109
+ {
110
+ label: 'Guides',
111
+ collapsed: true,
112
+ translations: expect.any(Object),
113
+ items: [
114
+ { label: 'Overview', slug: 'guides', translations: expect.any(Object) },
115
+ { slug: 'guides/a-first' },
116
+ { slug: 'guides/z-last' },
117
+ ],
118
+ },
119
+ ]);
120
+ });
121
+ });
@@ -6,10 +6,17 @@ import { sidebarTranslations } from '../i18n/translations.ts';
6
6
  /**
7
7
  * Starlight sidebar config types (simplified).
8
8
  */
9
- type SidebarLink = { label: string; link?: string; slug?: string; translations?: Record<string, string> };
9
+ type SidebarLink = { label?: string; link?: string; slug?: string; translations?: Record<string, string> };
10
10
  type SidebarGroup = { label: string; items: SidebarItem[]; collapsed?: boolean; translations?: Record<string, string> };
11
11
  type SidebarItem = SidebarLink | SidebarGroup;
12
12
 
13
+ interface OrderedSidebarItem {
14
+ item: SidebarItem;
15
+ order: number | undefined;
16
+ sortLabel: string;
17
+ sortPath: string;
18
+ }
19
+
13
20
  type DocType = 'resource' | 'data-source' | 'guide' | 'function';
14
21
 
15
22
  interface DocEntry {
@@ -77,6 +84,105 @@ function kebabToTitleCase(kebab: string): string {
77
84
  .join(' ');
78
85
  }
79
86
 
87
+ function readNavigationMetadata(filePath: string, fallbackTitle: string): { title: string; order: number | undefined } {
88
+ try {
89
+ const raw = fs.readFileSync(filePath, 'utf-8');
90
+ const frontmatter = matter(raw).data as Record<string, unknown>;
91
+ const title =
92
+ typeof frontmatter.title === 'string' && frontmatter.title.trim()
93
+ ? frontmatter.title.trim()
94
+ : typeof frontmatter.page_title === 'string' && frontmatter.page_title.trim()
95
+ ? frontmatter.page_title.trim()
96
+ : fallbackTitle;
97
+ const sidebar = frontmatter.sidebar;
98
+ const order =
99
+ typeof sidebar === 'object' &&
100
+ sidebar !== null &&
101
+ 'order' in sidebar &&
102
+ typeof sidebar.order === 'number' &&
103
+ Number.isFinite(sidebar.order)
104
+ ? sidebar.order
105
+ : undefined;
106
+ return { title, order };
107
+ } catch {
108
+ return { title: fallbackTitle, order: undefined };
109
+ }
110
+ }
111
+
112
+ function compareOrderedSidebarItems(a: OrderedSidebarItem, b: OrderedSidebarItem): number {
113
+ const orderDifference = (a.order ?? Number.POSITIVE_INFINITY) - (b.order ?? Number.POSITIVE_INFINITY);
114
+ if (orderDifference !== 0) return orderDifference;
115
+ const labelDifference = a.sortLabel.localeCompare(b.sortLabel);
116
+ return labelDifference !== 0 ? labelDifference : a.sortPath.localeCompare(b.sortPath);
117
+ }
118
+
119
+ function buildDirectorySidebar(dirPath: string, scanDir: string): OrderedSidebarItem | undefined {
120
+ let entries: fs.Dirent[];
121
+ try {
122
+ entries = fs.readdirSync(dirPath, { withFileTypes: true });
123
+ } catch {
124
+ return undefined;
125
+ }
126
+
127
+ const relativeDir = path.relative(scanDir, dirPath).replace(/\\/g, '/');
128
+ const directoryName = path.basename(dirPath);
129
+ const fallbackLabel = kebabToTitleCase(directoryName);
130
+ const indexEntry = entries.find((entry) => entry.isFile() && /^index\.mdx?$/.test(entry.name));
131
+ const indexPath = indexEntry ? path.join(dirPath, indexEntry.name) : undefined;
132
+ const metadata = indexPath
133
+ ? readNavigationMetadata(indexPath, fallbackLabel)
134
+ : { title: fallbackLabel, order: undefined };
135
+ const children: OrderedSidebarItem[] = [];
136
+
137
+ for (const entry of entries) {
138
+ if (entry.name.startsWith('.') || entry.name.startsWith('_')) continue;
139
+ const fullPath = path.join(dirPath, entry.name);
140
+
141
+ if (entry.isDirectory()) {
142
+ const group = buildDirectorySidebar(fullPath, scanDir);
143
+ if (group) children.push(group);
144
+ continue;
145
+ }
146
+ if (!entry.isFile() || !/\.mdx?$/.test(entry.name) || /^index\.mdx?$/.test(entry.name)) continue;
147
+
148
+ const relativePath = path.relative(scanDir, fullPath).replace(/\\/g, '/');
149
+ const slug = filePathToSlug(relativePath).replace(/^\/|\/$/g, '');
150
+ const fallbackTitle = path.basename(entry.name, path.extname(entry.name)).replace(/[-_]/g, ' ');
151
+ const page = readNavigationMetadata(fullPath, fallbackTitle);
152
+ children.push({
153
+ item: { slug },
154
+ order: page.order,
155
+ sortLabel: page.title,
156
+ sortPath: relativePath,
157
+ });
158
+ }
159
+
160
+ children.sort(compareOrderedSidebarItems);
161
+ const items: SidebarItem[] = [];
162
+ if (indexPath) {
163
+ items.push({
164
+ label: 'Overview',
165
+ slug: filePathToSlug(path.relative(scanDir, indexPath)).replace(/^\/|\/$/g, ''),
166
+ translations: sidebarTranslations.Overview,
167
+ });
168
+ }
169
+ items.push(...children.map((child) => child.item));
170
+ if (items.length === 0) return undefined;
171
+
172
+ const translations = sidebarTranslations[metadata.title as keyof typeof sidebarTranslations];
173
+ return {
174
+ item: {
175
+ label: metadata.title,
176
+ collapsed: true,
177
+ ...(translations ? { translations } : {}),
178
+ items,
179
+ },
180
+ order: metadata.order,
181
+ sortLabel: metadata.title,
182
+ sortPath: relativeDir,
183
+ };
184
+ }
185
+
80
186
  /**
81
187
  * Convert a file path relative to the content dir into a Starlight link slug.
82
188
  * e.g. "resources/api_crawler.md" → "/resources/api_crawler/"
@@ -170,51 +276,46 @@ export function buildSubcategorySidebar(contentDir: string): SidebarItem[] | und
170
276
 
171
277
  // If no files have subcategory, generate autogenerate entries with translations
172
278
  if (!hasAnySubcategory) {
173
- const topLevelDirs: string[] = [];
279
+ let rootEntries: fs.Dirent[];
174
280
  try {
175
- const dirEntries = fs.readdirSync(scanDir, { withFileTypes: true });
176
- for (const entry of dirEntries) {
177
- if (entry.isDirectory() && !entry.name.startsWith('.') && !entry.name.startsWith('_')) {
178
- topLevelDirs.push(entry.name);
179
- }
180
- }
281
+ rootEntries = fs.readdirSync(scanDir, { withFileTypes: true });
181
282
  } catch {
182
283
  return undefined;
183
284
  }
184
- if (topLevelDirs.length === 0) return undefined;
285
+ const orderedItems: OrderedSidebarItem[] = [];
185
286
 
186
- const sidebar: SidebarItem[] = [];
287
+ for (const entry of rootEntries) {
288
+ if (entry.name.startsWith('.') || entry.name.startsWith('_')) continue;
289
+ const fullPath = path.join(scanDir, entry.name);
187
290
 
188
- if (hasOverview) {
189
- sidebar.push({ label: 'Overview', link: '/', translations: sidebarTranslations.Overview });
291
+ if (entry.isDirectory()) {
292
+ const group = buildDirectorySidebar(fullPath, scanDir);
293
+ if (group) orderedItems.push(group);
294
+ continue;
295
+ }
296
+ if (!entry.isFile() || !/\.mdx?$/.test(entry.name) || /^index\.mdx?$/.test(entry.name)) continue;
297
+
298
+ const slug = filePathToSlug(entry.name).replace(/^\/|\/$/g, '');
299
+ const fallbackTitle = path.basename(entry.name, path.extname(entry.name)).replace(/[-_]/g, ' ');
300
+ const page = readNavigationMetadata(fullPath, fallbackTitle);
301
+ orderedItems.push({ item: { slug }, order: page.order, sortLabel: page.title, sortPath: entry.name });
190
302
  }
191
303
 
192
- for (const dir of topLevelDirs.sort()) {
193
- const label = kebabToTitleCase(dir);
194
- const translations = sidebarTranslations[label as keyof typeof sidebarTranslations];
195
- const dirPath = path.join(scanDir, dir);
196
- const dirFiles = collectMarkdownFiles(dirPath);
197
- const links: SidebarLink[] = dirFiles
198
- .map((filePath) => {
199
- const relativePath = path.relative(scanDir, filePath).replace(/\\/g, '/');
200
- const slug = filePathToSlug(relativePath);
201
- const baseName = path.basename(relativePath, path.extname(relativePath));
202
- if (baseName === 'index') return null;
203
- return { slug: slug.replace(/^\/|\/$/g, '') } as SidebarLink;
204
- })
205
- .filter((item): item is SidebarLink => item !== null);
206
-
207
- if (links.length > 0) {
208
- sidebar.push({
209
- label,
210
- collapsed: false,
211
- ...(translations ? { translations } : {}),
212
- items: links,
213
- });
214
- }
304
+ if (hasOverview) {
305
+ const overviewEntry = rootEntries.find((entry) => entry.isFile() && /^index\.mdx?$/.test(entry.name));
306
+ const overview = overviewEntry
307
+ ? readNavigationMetadata(path.join(scanDir, overviewEntry.name), 'Overview')
308
+ : { title: 'Overview', order: undefined };
309
+ orderedItems.push({
310
+ item: { label: 'Overview', link: '/', translations: sidebarTranslations.Overview },
311
+ order: overview.order ?? Number.NEGATIVE_INFINITY,
312
+ sortLabel: overview.title,
313
+ sortPath: '',
314
+ });
215
315
  }
216
316
 
217
- return sidebar;
317
+ orderedItems.sort(compareOrderedSidebarItems);
318
+ return orderedItems.length > 0 ? orderedItems.map((entry) => entry.item) : undefined;
218
319
  }
219
320
 
220
321
  // Partition docs by type