@f5-sales-demo/docs-theme 4.11.3 → 4.11.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@f5-sales-demo/docs-theme",
3
- "version": "4.11.3",
3
+ "version": "4.11.5",
4
4
  "description": "F5 Distributed Cloud branded Starlight documentation theme",
5
5
  "type": "module",
6
6
  "engines": {
@@ -119,3 +119,79 @@ describe('buildSubcategorySidebar without subcategories', () => {
119
119
  ]);
120
120
  });
121
121
  });
122
+
123
+ describe('authored sidebar metadata', () => {
124
+ async function fixture(docs: Record<string, string>) {
125
+ const root = await mkdtemp(path.join(tmpdir(), 'docs-theme-metadata-'));
126
+ workspaces.push(root);
127
+ for (const [file, metadata] of Object.entries(docs)) {
128
+ const destination = path.join(root, file);
129
+ await mkdir(path.dirname(destination), { recursive: true });
130
+ await writeFile(destination, `---\ntitle: Article title\n${metadata}\n---\nContent.\n`);
131
+ }
132
+ return buildSubcategorySidebar(root);
133
+ }
134
+
135
+ it('uses authored root and nested index labels without Overview translations', async () => {
136
+ const sidebar = await fixture({
137
+ 'index.mdx': 'sidebar:\n label: Home\n order: 0',
138
+ 'plugins/index.mdx': 'sidebar:\n label: Plugin catalog\n order: 1',
139
+ 'plugins/tool.mdx': 'sidebar:\n label: Review documents',
140
+ });
141
+ expect(sidebar).toEqual([
142
+ { label: 'Home', link: '/' },
143
+ {
144
+ label: 'Plugin catalog',
145
+ collapsed: true,
146
+ items: [
147
+ { label: 'Plugin catalog', slug: 'plugins' },
148
+ { label: 'Review documents', slug: 'plugins/tool' },
149
+ ],
150
+ },
151
+ ]);
152
+ });
153
+
154
+ it('omits hidden root and nested pages while keeping children of hidden indexes', async () => {
155
+ const sidebar = await fixture({
156
+ 'index.mdx': 'sidebar:\n hidden: true',
157
+ 'hidden.mdx': 'sidebar:\n hidden: true',
158
+ 'plugins/index.mdx': 'sidebar:\n label: Plugins\n hidden: true',
159
+ 'plugins/hidden.mdx': 'sidebar:\n hidden: true',
160
+ 'plugins/deep/index.mdx': 'sidebar:\n hidden: true',
161
+ 'plugins/deep/visible.mdx': '',
162
+ 'empty/index.mdx': 'sidebar:\n hidden: true',
163
+ });
164
+ expect(sidebar).toEqual([
165
+ {
166
+ label: 'Plugins',
167
+ collapsed: true,
168
+ translations: expect.any(Object),
169
+ items: [{ label: 'Article title', collapsed: true, items: [{ slug: 'plugins/deep/visible' }] }],
170
+ },
171
+ ]);
172
+ });
173
+
174
+ it('honors explicit order and alphabetic fallback independently of filenames', async () => {
175
+ const sidebar = await fixture({
176
+ 'index.mdx': 'sidebar:\n label: Home\n order: 2',
177
+ 'last.mdx': 'sidebar:\n label: First\n order: 1',
178
+ 'alpha.mdx': 'sidebar:\n label: Zebra',
179
+ 'zebra.mdx': 'sidebar:\n label: Alpha',
180
+ });
181
+ expect(sidebar).toEqual([
182
+ { label: 'First', slug: 'last' },
183
+ { label: 'Home', link: '/' },
184
+ { label: 'Alpha', slug: 'zebra' },
185
+ { label: 'Zebra', slug: 'alpha' },
186
+ ]);
187
+ });
188
+
189
+ it('ignores blank labels and nonboolean hidden values', async () => {
190
+ const sidebar = await fixture({
191
+ 'index.mdx': 'sidebar:\n label: " "\n hidden: "true"',
192
+ 'page.mdx': 'sidebar:\n label: " "\n hidden: false',
193
+ });
194
+ expect(sidebar?.[0]).toMatchObject({ label: 'Overview', link: '/', translations: expect.any(Object) });
195
+ expect(sidebar?.[1]).toEqual({ slug: 'page' });
196
+ });
197
+ });
@@ -84,7 +84,14 @@ function kebabToTitleCase(kebab: string): string {
84
84
  .join(' ');
85
85
  }
86
86
 
87
- function readNavigationMetadata(filePath: string, fallbackTitle: string): { title: string; order: number | undefined } {
87
+ interface NavigationMetadata {
88
+ title: string;
89
+ label?: string;
90
+ hidden: boolean;
91
+ order: number | undefined;
92
+ }
93
+
94
+ function readNavigationMetadata(filePath: string, fallbackTitle: string): NavigationMetadata {
88
95
  try {
89
96
  const raw = fs.readFileSync(filePath, 'utf-8');
90
97
  const frontmatter = matter(raw).data as Record<string, unknown>;
@@ -103,15 +110,24 @@ function readNavigationMetadata(filePath: string, fallbackTitle: string): { titl
103
110
  Number.isFinite(sidebar.order)
104
111
  ? sidebar.order
105
112
  : undefined;
106
- return { title, order };
113
+ const label =
114
+ typeof sidebar === 'object' &&
115
+ sidebar !== null &&
116
+ 'label' in sidebar &&
117
+ typeof sidebar.label === 'string' &&
118
+ sidebar.label.trim()
119
+ ? sidebar.label.trim()
120
+ : undefined;
121
+ const hidden = typeof sidebar === 'object' && sidebar !== null && 'hidden' in sidebar && sidebar.hidden === true;
122
+ return { title, label, hidden, order };
107
123
  } catch {
108
- return { title: fallbackTitle, order: undefined };
124
+ return { title: fallbackTitle, hidden: false, order: undefined };
109
125
  }
110
126
  }
111
127
 
112
128
  function compareOrderedSidebarItems(a: OrderedSidebarItem, b: OrderedSidebarItem): number {
113
129
  const orderDifference = (a.order ?? Number.POSITIVE_INFINITY) - (b.order ?? Number.POSITIVE_INFINITY);
114
- if (orderDifference !== 0) return orderDifference;
130
+ if (!Number.isNaN(orderDifference) && orderDifference !== 0) return orderDifference;
115
131
  const labelDifference = a.sortLabel.localeCompare(b.sortLabel);
116
132
  return labelDifference !== 0 ? labelDifference : a.sortPath.localeCompare(b.sortPath);
117
133
  }
@@ -131,7 +147,7 @@ function buildDirectorySidebar(dirPath: string, scanDir: string): OrderedSidebar
131
147
  const indexPath = indexEntry ? path.join(dirPath, indexEntry.name) : undefined;
132
148
  const metadata = indexPath
133
149
  ? readNavigationMetadata(indexPath, fallbackLabel)
134
- : { title: fallbackLabel, order: undefined };
150
+ : { title: fallbackLabel, hidden: false, order: undefined };
135
151
  const children: OrderedSidebarItem[] = [];
136
152
 
137
153
  for (const entry of entries) {
@@ -149,36 +165,38 @@ function buildDirectorySidebar(dirPath: string, scanDir: string): OrderedSidebar
149
165
  const slug = filePathToSlug(relativePath).replace(/^\/|\/$/g, '');
150
166
  const fallbackTitle = path.basename(entry.name, path.extname(entry.name)).replace(/[-_]/g, ' ');
151
167
  const page = readNavigationMetadata(fullPath, fallbackTitle);
168
+ if (page.hidden) continue;
152
169
  children.push({
153
- item: { slug },
170
+ item: { slug, ...(page.label ? { label: page.label } : {}) },
154
171
  order: page.order,
155
- sortLabel: page.title,
172
+ sortLabel: page.label ?? page.title,
156
173
  sortPath: relativePath,
157
174
  });
158
175
  }
159
176
 
160
177
  children.sort(compareOrderedSidebarItems);
161
178
  const items: SidebarItem[] = [];
162
- if (indexPath) {
179
+ if (indexPath && !metadata.hidden) {
163
180
  items.push({
164
- label: 'Overview',
181
+ label: metadata.label ?? 'Overview',
165
182
  slug: filePathToSlug(path.relative(scanDir, indexPath)).replace(/^\/|\/$/g, ''),
166
- translations: sidebarTranslations.Overview,
183
+ ...(!metadata.label ? { translations: sidebarTranslations.Overview } : {}),
167
184
  });
168
185
  }
169
186
  items.push(...children.map((child) => child.item));
170
187
  if (items.length === 0) return undefined;
171
188
 
172
- const translations = sidebarTranslations[metadata.title as keyof typeof sidebarTranslations];
189
+ const groupLabel = metadata.label ?? metadata.title;
190
+ const translations = sidebarTranslations[groupLabel as keyof typeof sidebarTranslations];
173
191
  return {
174
192
  item: {
175
- label: metadata.title,
193
+ label: groupLabel,
176
194
  collapsed: true,
177
195
  ...(translations ? { translations } : {}),
178
196
  items,
179
197
  },
180
198
  order: metadata.order,
181
- sortLabel: metadata.title,
199
+ sortLabel: groupLabel,
182
200
  sortPath: relativeDir,
183
201
  };
184
202
  }
@@ -225,6 +243,7 @@ export function buildSubcategorySidebar(contentDir: string): SidebarItem[] | und
225
243
  const docs: DocEntry[] = [];
226
244
  let hasAnySubcategory = false;
227
245
  let hasOverview = false;
246
+ let overviewMetadata: NavigationMetadata = { title: 'Overview', hidden: false, order: undefined };
228
247
 
229
248
  for (const filePath of files) {
230
249
  const relativePath = path.relative(scanDir, filePath).replace(/\\/g, '/');
@@ -232,7 +251,8 @@ export function buildSubcategorySidebar(contentDir: string): SidebarItem[] | und
232
251
 
233
252
  // Track overview page separately
234
253
  if (slug === '/') {
235
- hasOverview = true;
254
+ overviewMetadata = readNavigationMetadata(filePath, 'Overview');
255
+ hasOverview = !overviewMetadata.hidden;
236
256
  continue;
237
257
  }
238
258
 
@@ -262,7 +282,9 @@ export function buildSubcategorySidebar(contentDir: string): SidebarItem[] | und
262
282
  } else {
263
283
  title = path.basename(filePath, path.extname(filePath)).replace(/[-_]/g, ' ');
264
284
  }
265
- title = cleanPageTitle(title, docType);
285
+ const navigation = readNavigationMetadata(filePath, title);
286
+ if (navigation.hidden) continue;
287
+ title = navigation.label ?? cleanPageTitle(title, docType);
266
288
 
267
289
  const subcategory =
268
290
  typeof frontmatter.subcategory === 'string' && frontmatter.subcategory.trim()
@@ -298,18 +320,25 @@ export function buildSubcategorySidebar(contentDir: string): SidebarItem[] | und
298
320
  const slug = filePathToSlug(entry.name).replace(/^\/|\/$/g, '');
299
321
  const fallbackTitle = path.basename(entry.name, path.extname(entry.name)).replace(/[-_]/g, ' ');
300
322
  const page = readNavigationMetadata(fullPath, fallbackTitle);
301
- orderedItems.push({ item: { slug }, order: page.order, sortLabel: page.title, sortPath: entry.name });
323
+ if (page.hidden) continue;
324
+ orderedItems.push({
325
+ item: { slug, ...(page.label ? { label: page.label } : {}) },
326
+ order: page.order,
327
+ sortLabel: page.label ?? page.title,
328
+ sortPath: entry.name,
329
+ });
302
330
  }
303
331
 
304
332
  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 };
333
+ const overview = overviewMetadata;
309
334
  orderedItems.push({
310
- item: { label: 'Overview', link: '/', translations: sidebarTranslations.Overview },
335
+ item: {
336
+ label: overview.label ?? 'Overview',
337
+ link: '/',
338
+ ...(!overview.label ? { translations: sidebarTranslations.Overview } : {}),
339
+ },
311
340
  order: overview.order ?? Number.NEGATIVE_INFINITY,
312
- sortLabel: overview.title,
341
+ sortLabel: overview.label ?? overview.title,
313
342
  sortPath: '',
314
343
  });
315
344
  }
@@ -332,7 +361,11 @@ export function buildSubcategorySidebar(contentDir: string): SidebarItem[] | und
332
361
 
333
362
  // 1. Overview link
334
363
  if (hasOverview) {
335
- sidebar.push({ label: 'Overview', link: '/', translations: sidebarTranslations.Overview });
364
+ sidebar.push({
365
+ label: overviewMetadata.label ?? 'Overview',
366
+ link: '/',
367
+ ...(!overviewMetadata.label ? { translations: sidebarTranslations.Overview } : {}),
368
+ });
336
369
  }
337
370
 
338
371
  // 2. Guides group