@f5-sales-demo/docs-theme 4.11.4 → 4.11.6
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 +12 -36
- package/components/Head.astro +10 -0
- package/components/LinkCard.astro +17 -8
- package/components/ProviderHeader.astro +8 -3
- package/components/SharedHeader.astro +66 -4
- package/config.ts +172 -326
- package/index.ts +1 -0
- package/package.json +2 -1
- package/src/utils/localize-ecosystem-href.ts +4 -0
- package/src/utils/page-title.test.ts +7 -0
- package/src/utils/page-title.ts +4 -0
- package/src/utils/search-sites.ts +32 -0
- package/src/utils/search-source.test.ts +9 -0
- package/src/utils/search-source.ts +11 -0
- package/src/utils/showcase-navigation.test.ts +34 -0
- package/src/utils/statistics-navigation.test.ts +4 -4
- package/src/utils/subcategory-sidebar.test.ts +76 -0
- package/src/utils/subcategory-sidebar.ts +56 -23
- package/styles/custom.css +30 -0
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.
|
|
3
|
+
"version": "4.11.6",
|
|
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,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 === '
|
|
8
|
-
const category = networking?.content?.categories?.find((item) => item.title === '
|
|
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: '
|
|
13
|
+
description: 'Query access logs, application metrics, and security telemetry through the API.',
|
|
14
14
|
});
|
|
15
|
-
expect(category?.items?.map((item) => item.label)).toEqual(['
|
|
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);
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
189
|
+
const groupLabel = metadata.label ?? metadata.title;
|
|
190
|
+
const translations = sidebarTranslations[groupLabel as keyof typeof sidebarTranslations];
|
|
173
191
|
return {
|
|
174
192
|
item: {
|
|
175
|
-
label:
|
|
193
|
+
label: groupLabel,
|
|
176
194
|
collapsed: true,
|
|
177
195
|
...(translations ? { translations } : {}),
|
|
178
196
|
items,
|
|
179
197
|
},
|
|
180
198
|
order: metadata.order,
|
|
181
|
-
sortLabel:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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: {
|
|
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({
|
|
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
|
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
|
+
}
|