@levino/shipyard-docs 0.5.2 → 0.6.0-rc-20260106213612
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/astro/Layout.astro +73 -23
- package/package.json +16 -6
- package/src/fallbacks.test.ts +136 -0
- package/src/fallbacks.ts +82 -0
- package/src/index.ts +177 -50
- package/src/pagination.test.ts +115 -15
- package/src/pagination.ts +44 -16
- package/src/schema.test.ts +467 -0
- package/src/sidebarEntries.test.ts +145 -7
- package/src/sidebarEntries.ts +30 -3
package/src/index.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type { AstroIntegration } from 'astro'
|
|
|
4
4
|
import { glob } from 'astro/loaders'
|
|
5
5
|
import { z } from 'astro/zod'
|
|
6
6
|
|
|
7
|
+
// Re-export fallback utilities
|
|
8
|
+
export { extractFirstParagraph } from './fallbacks'
|
|
7
9
|
// Re-export git metadata utilities
|
|
8
10
|
export type { GitMetadata } from './gitMetadata'
|
|
9
11
|
export { getEditUrl, getGitMetadata } from './gitMetadata'
|
|
@@ -20,37 +22,150 @@ export { getDocPath, getRouteParams } from './routeHelpers'
|
|
|
20
22
|
export type { DocsData } from './sidebarEntries'
|
|
21
23
|
export { toSidebarEntries } from './sidebarEntries'
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Schema for sidebar configuration grouped under a single object.
|
|
27
|
+
* Contains all sidebar-related fields for categories and pages.
|
|
28
|
+
*/
|
|
29
|
+
const sidebarSchema = z
|
|
30
|
+
.object({
|
|
31
|
+
/** Sort order in sidebar (default: Infinity - sorted alphabetically after positioned items) */
|
|
32
|
+
position: z.number().optional(),
|
|
33
|
+
/** Display label in sidebar (default: title -> H1 -> filename) */
|
|
34
|
+
label: z.string().optional(),
|
|
35
|
+
/** CSS class(es) for styling the sidebar entry */
|
|
36
|
+
className: z.string().optional(),
|
|
37
|
+
/** Arbitrary metadata for custom sidebar components */
|
|
38
|
+
customProps: z.record(z.any()).optional(),
|
|
39
|
+
/** Can category be collapsed (default: true) */
|
|
40
|
+
collapsible: z.boolean().default(true),
|
|
41
|
+
/** Start collapsed (default: true) */
|
|
42
|
+
collapsed: z.boolean().default(true),
|
|
43
|
+
})
|
|
44
|
+
.refine((data) => !(data.collapsed === true && data.collapsible === false), {
|
|
45
|
+
message:
|
|
46
|
+
'sidebar.collapsed cannot be true when sidebar.collapsible is false',
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
export const docsSchema = z
|
|
50
|
+
.object({
|
|
51
|
+
// === Page Metadata ===
|
|
52
|
+
/** Custom document ID (default: file path) */
|
|
53
|
+
id: z.string().optional(),
|
|
54
|
+
/** Reference title for SEO, pagination, previews (default: H1) */
|
|
55
|
+
title: z.string().optional(),
|
|
56
|
+
/** Override title for SEO/browser tab (default: title) - Docusaurus snake_case convention */
|
|
57
|
+
title_meta: z.string().optional(),
|
|
58
|
+
/** Meta description (default: first paragraph) */
|
|
59
|
+
description: z.string().optional(),
|
|
60
|
+
/** SEO keywords */
|
|
61
|
+
keywords: z.array(z.string()).optional(),
|
|
62
|
+
/** Social preview image (og:image) */
|
|
63
|
+
image: z.string().optional(),
|
|
64
|
+
/** Custom canonical URL */
|
|
65
|
+
canonicalUrl: z.string().optional(),
|
|
66
|
+
/** Custom canonical URL - snake_case alias for Docusaurus compatibility */
|
|
67
|
+
canonical_url: z.string().optional(),
|
|
68
|
+
|
|
69
|
+
// === Page Rendering ===
|
|
70
|
+
/** Whether to render a page (default: true) */
|
|
71
|
+
render: z.boolean().default(true),
|
|
72
|
+
/** Exclude from production builds (default: false) */
|
|
73
|
+
draft: z.boolean().default(false),
|
|
74
|
+
/** Render page but hide from sidebar (default: false) */
|
|
75
|
+
unlisted: z.boolean().default(false),
|
|
76
|
+
/** Custom URL slug */
|
|
77
|
+
slug: z.string().optional(),
|
|
78
|
+
|
|
79
|
+
// === Layout Options ===
|
|
80
|
+
/** Hide the H1 heading (default: false) */
|
|
81
|
+
hideTitle: z.boolean().default(false),
|
|
82
|
+
/** Hide the H1 heading (default: false) - snake_case alias for Docusaurus compatibility */
|
|
83
|
+
hide_title: z.boolean().optional(),
|
|
84
|
+
/** Hide the TOC (default: false) */
|
|
85
|
+
hideTableOfContents: z.boolean().default(false),
|
|
86
|
+
/** Hide the TOC (default: false) - snake_case alias for Docusaurus compatibility */
|
|
87
|
+
hide_table_of_contents: z.boolean().optional(),
|
|
88
|
+
/** Full-width page without sidebar (default: false) */
|
|
89
|
+
hideSidebar: z.boolean().default(false),
|
|
90
|
+
/** Min heading level in TOC (default: 2) */
|
|
91
|
+
tocMinHeadingLevel: z.number().min(1).max(6).default(2),
|
|
92
|
+
/** Max heading level in TOC (default: 3) */
|
|
93
|
+
tocMaxHeadingLevel: z.number().min(1).max(6).default(3),
|
|
94
|
+
|
|
95
|
+
// === Sidebar Configuration (grouped) ===
|
|
96
|
+
sidebar: sidebarSchema.default({ collapsible: true, collapsed: true }),
|
|
97
|
+
|
|
98
|
+
// === Pagination ===
|
|
99
|
+
/** Label shown in prev/next buttons */
|
|
100
|
+
paginationLabel: z.string().optional(),
|
|
101
|
+
/** Label shown in prev/next buttons - snake_case alias for Docusaurus compatibility */
|
|
102
|
+
pagination_label: z.string().optional(),
|
|
103
|
+
/** Next page ID, or null to disable */
|
|
104
|
+
paginationNext: z.string().nullable().optional(),
|
|
105
|
+
/** Next page ID - snake_case alias for Docusaurus compatibility */
|
|
106
|
+
pagination_next: z.string().nullable().optional(),
|
|
107
|
+
/** Previous page ID, or null to disable */
|
|
108
|
+
paginationPrev: z.string().nullable().optional(),
|
|
109
|
+
/** Previous page ID - snake_case alias for Docusaurus compatibility */
|
|
110
|
+
pagination_prev: z.string().nullable().optional(),
|
|
111
|
+
|
|
112
|
+
// === Git Metadata Overrides ===
|
|
113
|
+
/**
|
|
114
|
+
* Override the last update author for this specific page.
|
|
115
|
+
* Set to false to hide the author for this page.
|
|
116
|
+
*/
|
|
117
|
+
lastUpdateAuthor: z.union([z.string(), z.literal(false)]).optional(),
|
|
118
|
+
/**
|
|
119
|
+
* Override the last update timestamp for this specific page.
|
|
120
|
+
* Set to false to hide the timestamp for this page.
|
|
121
|
+
*/
|
|
122
|
+
lastUpdateTime: z.union([z.literal(false), z.coerce.date()]).optional(),
|
|
123
|
+
/**
|
|
124
|
+
* Custom edit URL for this specific page.
|
|
125
|
+
* Set to null to disable edit link for this page.
|
|
126
|
+
*/
|
|
127
|
+
customEditUrl: z.string().nullable().optional(),
|
|
128
|
+
|
|
129
|
+
// === Custom Meta Tags ===
|
|
130
|
+
customMetaTags: z
|
|
131
|
+
.array(
|
|
132
|
+
z.object({
|
|
133
|
+
name: z.string().optional(),
|
|
134
|
+
property: z.string().optional(),
|
|
135
|
+
content: z.string(),
|
|
136
|
+
}),
|
|
137
|
+
)
|
|
138
|
+
.optional(),
|
|
139
|
+
/** Custom meta tags - snake_case alias for Docusaurus compatibility */
|
|
140
|
+
custom_meta_tags: z
|
|
141
|
+
.array(
|
|
142
|
+
z.object({
|
|
143
|
+
name: z.string().optional(),
|
|
144
|
+
property: z.string().optional(),
|
|
145
|
+
content: z.string(),
|
|
146
|
+
}),
|
|
147
|
+
)
|
|
148
|
+
.optional(),
|
|
149
|
+
})
|
|
150
|
+
.transform((data) => ({
|
|
151
|
+
...data,
|
|
152
|
+
// Merge snake_case aliases into camelCase fields
|
|
153
|
+
hideTitle: data.hide_title ?? data.hideTitle,
|
|
154
|
+
hideTableOfContents:
|
|
155
|
+
data.hide_table_of_contents ?? data.hideTableOfContents,
|
|
156
|
+
canonicalUrl: data.canonical_url ?? data.canonicalUrl,
|
|
157
|
+
customMetaTags: data.custom_meta_tags ?? data.customMetaTags,
|
|
158
|
+
paginationLabel: data.pagination_label ?? data.paginationLabel,
|
|
159
|
+
// For nullable fields, we need to check if the snake_case key exists (not just use ??)
|
|
160
|
+
// because null ?? undefined = undefined, but we want null to be preserved
|
|
161
|
+
paginationNext:
|
|
162
|
+
'pagination_next' in data ? data.pagination_next : data.paginationNext,
|
|
163
|
+
paginationPrev:
|
|
164
|
+
'pagination_prev' in data ? data.pagination_prev : data.paginationPrev,
|
|
165
|
+
}))
|
|
166
|
+
.refine((data) => data.tocMinHeadingLevel <= data.tocMaxHeadingLevel, {
|
|
167
|
+
message: 'tocMinHeadingLevel must be <= tocMaxHeadingLevel',
|
|
168
|
+
})
|
|
54
169
|
|
|
55
170
|
/**
|
|
56
171
|
* Configuration for llms.txt generation.
|
|
@@ -276,7 +391,10 @@ import Layout from '@levino/shipyard-docs/astro/Layout.astro'
|
|
|
276
391
|
export async function getStaticPaths() {
|
|
277
392
|
const collectionName = ${JSON.stringify(resolvedCollectionName)}
|
|
278
393
|
const routeBasePath = ${JSON.stringify(normalizedBasePath)}
|
|
279
|
-
const
|
|
394
|
+
const allDocs = await getCollection(collectionName)
|
|
395
|
+
|
|
396
|
+
// Filter out pages with render: false - they should not generate pages
|
|
397
|
+
const docs = allDocs.filter((doc) => doc.data.render !== false)
|
|
280
398
|
|
|
281
399
|
const getParams = (slug) => {
|
|
282
400
|
if (i18n) {
|
|
@@ -309,13 +427,13 @@ const docsConfig = docsConfigs[routeBasePath] ?? {
|
|
|
309
427
|
|
|
310
428
|
const { Content, headings } = await render(entry)
|
|
311
429
|
|
|
312
|
-
const {
|
|
430
|
+
const { customEditUrl, lastUpdateAuthor, lastUpdateTime, hideTableOfContents, hideTitle, keywords, image, canonicalUrl, customMetaTags, title_meta: titleMeta } = entry.data
|
|
313
431
|
|
|
314
432
|
let editUrl
|
|
315
|
-
if (
|
|
433
|
+
if (customEditUrl === null) {
|
|
316
434
|
editUrl = undefined
|
|
317
|
-
} else if (
|
|
318
|
-
editUrl =
|
|
435
|
+
} else if (customEditUrl) {
|
|
436
|
+
editUrl = customEditUrl
|
|
319
437
|
} else {
|
|
320
438
|
editUrl = getEditUrl(docsConfig.editUrl, entry.id)
|
|
321
439
|
}
|
|
@@ -324,32 +442,32 @@ let lastUpdated
|
|
|
324
442
|
let lastAuthor
|
|
325
443
|
|
|
326
444
|
if (
|
|
327
|
-
(docsConfig.showLastUpdateTime &&
|
|
328
|
-
(docsConfig.showLastUpdateAuthor &&
|
|
445
|
+
(docsConfig.showLastUpdateTime && lastUpdateTime !== false) ||
|
|
446
|
+
(docsConfig.showLastUpdateAuthor && lastUpdateAuthor !== false)
|
|
329
447
|
) {
|
|
330
448
|
const filePath = entry.filePath
|
|
331
449
|
|
|
332
450
|
if (filePath) {
|
|
333
451
|
const gitMetadata = getGitMetadata(filePath)
|
|
334
452
|
|
|
335
|
-
if (docsConfig.showLastUpdateTime &&
|
|
453
|
+
if (docsConfig.showLastUpdateTime && lastUpdateTime !== false) {
|
|
336
454
|
lastUpdated =
|
|
337
|
-
|
|
338
|
-
?
|
|
455
|
+
lastUpdateTime instanceof Date
|
|
456
|
+
? lastUpdateTime
|
|
339
457
|
: gitMetadata.lastUpdated
|
|
340
458
|
}
|
|
341
459
|
|
|
342
|
-
if (docsConfig.showLastUpdateAuthor &&
|
|
460
|
+
if (docsConfig.showLastUpdateAuthor && lastUpdateAuthor !== false) {
|
|
343
461
|
lastAuthor =
|
|
344
|
-
typeof
|
|
345
|
-
?
|
|
462
|
+
typeof lastUpdateAuthor === 'string'
|
|
463
|
+
? lastUpdateAuthor
|
|
346
464
|
: gitMetadata.lastAuthor
|
|
347
465
|
}
|
|
348
466
|
}
|
|
349
467
|
}
|
|
350
468
|
---
|
|
351
469
|
|
|
352
|
-
<Layout headings={headings} routeBasePath={routeBasePath} editUrl={editUrl} lastUpdated={lastUpdated} lastAuthor={lastAuthor}>
|
|
470
|
+
<Layout headings={headings} routeBasePath={routeBasePath} editUrl={editUrl} lastUpdated={lastUpdated} lastAuthor={lastAuthor} hideTableOfContents={hideTableOfContents} hideTitle={hideTitle} keywords={keywords} image={image} canonicalUrl={canonicalUrl} customMetaTags={customMetaTags} titleMeta={titleMeta}>
|
|
353
471
|
<Content />
|
|
354
472
|
</Layout>
|
|
355
473
|
`
|
|
@@ -424,10 +542,13 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
|
|
424
542
|
|
|
425
543
|
// When i18n is enabled, only include docs from the default locale
|
|
426
544
|
const defaultLocale = i18n?.defaultLocale
|
|
427
|
-
const
|
|
545
|
+
const localeDocs = defaultLocale
|
|
428
546
|
? allDocs.filter((doc) => doc.id.startsWith(defaultLocale + '/') || doc.id === defaultLocale)
|
|
429
547
|
: allDocs
|
|
430
548
|
|
|
549
|
+
// Filter out unlisted and non-rendered pages
|
|
550
|
+
const docs = localeDocs.filter((doc) => !doc.data.unlisted && doc.data.render !== false)
|
|
551
|
+
|
|
431
552
|
return docs.map((doc) => {
|
|
432
553
|
const cleanId = doc.id.replace(/\\.md$/, '')
|
|
433
554
|
// For i18n, strip the locale prefix from the slug
|
|
@@ -491,10 +612,13 @@ export const GET: APIRoute = async ({ site }) => {
|
|
|
491
612
|
|
|
492
613
|
// When i18n is enabled, only include docs from the default locale
|
|
493
614
|
const defaultLocale = i18n?.defaultLocale
|
|
494
|
-
const
|
|
615
|
+
const localeDocs = defaultLocale
|
|
495
616
|
? allDocs.filter((doc) => doc.id.startsWith(defaultLocale + '/') || doc.id === defaultLocale)
|
|
496
617
|
: allDocs
|
|
497
618
|
|
|
619
|
+
// Filter out unlisted and non-rendered pages
|
|
620
|
+
const docs = localeDocs.filter((doc) => !doc.data.unlisted && doc.data.render !== false)
|
|
621
|
+
|
|
498
622
|
const entries = await Promise.all(
|
|
499
623
|
docs.map(async (doc) => {
|
|
500
624
|
const { headings } = await render(doc)
|
|
@@ -521,7 +645,7 @@ export const GET: APIRoute = async ({ site }) => {
|
|
|
521
645
|
path,
|
|
522
646
|
title: doc.data.title ?? h1?.text ?? doc.id,
|
|
523
647
|
description: doc.data.description,
|
|
524
|
-
position: doc.data.
|
|
648
|
+
position: doc.data.sidebar?.position,
|
|
525
649
|
}
|
|
526
650
|
})
|
|
527
651
|
)
|
|
@@ -556,10 +680,13 @@ export const GET: APIRoute = async ({ site }) => {
|
|
|
556
680
|
|
|
557
681
|
// When i18n is enabled, only include docs from the default locale
|
|
558
682
|
const defaultLocale = i18n?.defaultLocale
|
|
559
|
-
const
|
|
683
|
+
const localeDocs = defaultLocale
|
|
560
684
|
? allDocs.filter((doc) => doc.id.startsWith(defaultLocale + '/') || doc.id === defaultLocale)
|
|
561
685
|
: allDocs
|
|
562
686
|
|
|
687
|
+
// Filter out unlisted and non-rendered pages
|
|
688
|
+
const docs = localeDocs.filter((doc) => !doc.data.unlisted && doc.data.render !== false)
|
|
689
|
+
|
|
563
690
|
const entries = await Promise.all(
|
|
564
691
|
docs.map(async (doc) => {
|
|
565
692
|
const { headings } = await render(doc)
|
|
@@ -589,7 +716,7 @@ export const GET: APIRoute = async ({ site }) => {
|
|
|
589
716
|
path,
|
|
590
717
|
title: doc.data.title ?? h1?.text ?? doc.id,
|
|
591
718
|
description: doc.data.description,
|
|
592
|
-
position: doc.data.
|
|
719
|
+
position: doc.data.sidebar?.position,
|
|
593
720
|
content: rawContent,
|
|
594
721
|
}
|
|
595
722
|
})
|
package/src/pagination.test.ts
CHANGED
|
@@ -31,21 +31,25 @@ describe('getPaginationInfo', () => {
|
|
|
31
31
|
const createDocs = (): DocsData[] => [
|
|
32
32
|
{
|
|
33
33
|
id: 'intro.md',
|
|
34
|
+
fileId: 'intro.md',
|
|
34
35
|
title: 'Introduction',
|
|
35
36
|
path: '/docs/intro',
|
|
36
37
|
},
|
|
37
38
|
{
|
|
38
39
|
id: 'guide/getting-started.md',
|
|
40
|
+
fileId: 'guide/getting-started.md',
|
|
39
41
|
title: 'Getting Started',
|
|
40
42
|
path: '/docs/guide/getting-started',
|
|
41
43
|
},
|
|
42
44
|
{
|
|
43
45
|
id: 'guide/advanced.md',
|
|
46
|
+
fileId: 'guide/advanced.md',
|
|
44
47
|
title: 'Advanced',
|
|
45
48
|
path: '/docs/guide/advanced',
|
|
46
49
|
},
|
|
47
50
|
{
|
|
48
51
|
id: 'api.md',
|
|
52
|
+
fileId: 'api.md',
|
|
49
53
|
title: 'API Reference',
|
|
50
54
|
path: '/docs/api',
|
|
51
55
|
},
|
|
@@ -102,27 +106,31 @@ describe('getPaginationInfo', () => {
|
|
|
102
106
|
expect(pagination).toEqual({})
|
|
103
107
|
})
|
|
104
108
|
|
|
105
|
-
it('should respect
|
|
109
|
+
it('should respect paginationNext override', () => {
|
|
106
110
|
const sidebar = createSidebar()
|
|
107
111
|
const docs: DocsData[] = [
|
|
108
112
|
{
|
|
109
113
|
id: 'intro.md',
|
|
114
|
+
fileId: 'intro.md',
|
|
110
115
|
title: 'Introduction',
|
|
111
116
|
path: '/docs/intro',
|
|
112
|
-
|
|
117
|
+
paginationNext: 'api.md', // Skip directly to api
|
|
113
118
|
},
|
|
114
119
|
{
|
|
115
120
|
id: 'guide/getting-started.md',
|
|
121
|
+
fileId: 'guide/getting-started.md',
|
|
116
122
|
title: 'Getting Started',
|
|
117
123
|
path: '/docs/guide/getting-started',
|
|
118
124
|
},
|
|
119
125
|
{
|
|
120
126
|
id: 'guide/advanced.md',
|
|
127
|
+
fileId: 'guide/advanced.md',
|
|
121
128
|
title: 'Advanced',
|
|
122
129
|
path: '/docs/guide/advanced',
|
|
123
130
|
},
|
|
124
131
|
{
|
|
125
132
|
id: 'api.md',
|
|
133
|
+
fileId: 'api.md',
|
|
126
134
|
title: 'API Reference',
|
|
127
135
|
path: '/docs/api',
|
|
128
136
|
},
|
|
@@ -137,29 +145,33 @@ describe('getPaginationInfo', () => {
|
|
|
137
145
|
})
|
|
138
146
|
})
|
|
139
147
|
|
|
140
|
-
it('should respect
|
|
148
|
+
it('should respect paginationPrev override', () => {
|
|
141
149
|
const sidebar = createSidebar()
|
|
142
150
|
const docs: DocsData[] = [
|
|
143
151
|
{
|
|
144
152
|
id: 'intro.md',
|
|
153
|
+
fileId: 'intro.md',
|
|
145
154
|
title: 'Introduction',
|
|
146
155
|
path: '/docs/intro',
|
|
147
156
|
},
|
|
148
157
|
{
|
|
149
158
|
id: 'guide/getting-started.md',
|
|
159
|
+
fileId: 'guide/getting-started.md',
|
|
150
160
|
title: 'Getting Started',
|
|
151
161
|
path: '/docs/guide/getting-started',
|
|
152
162
|
},
|
|
153
163
|
{
|
|
154
164
|
id: 'guide/advanced.md',
|
|
165
|
+
fileId: 'guide/advanced.md',
|
|
155
166
|
title: 'Advanced',
|
|
156
167
|
path: '/docs/guide/advanced',
|
|
157
168
|
},
|
|
158
169
|
{
|
|
159
170
|
id: 'api.md',
|
|
171
|
+
fileId: 'api.md',
|
|
160
172
|
title: 'API Reference',
|
|
161
173
|
path: '/docs/api',
|
|
162
|
-
|
|
174
|
+
paginationPrev: 'intro.md', // Skip back to intro
|
|
163
175
|
},
|
|
164
176
|
]
|
|
165
177
|
|
|
@@ -172,17 +184,19 @@ describe('getPaginationInfo', () => {
|
|
|
172
184
|
expect(pagination.next).toBeUndefined()
|
|
173
185
|
})
|
|
174
186
|
|
|
175
|
-
it('should disable next pagination when
|
|
187
|
+
it('should disable next pagination when paginationNext is null', () => {
|
|
176
188
|
const sidebar = createSidebar()
|
|
177
189
|
const docs: DocsData[] = [
|
|
178
190
|
{
|
|
179
191
|
id: 'intro.md',
|
|
192
|
+
fileId: 'intro.md',
|
|
180
193
|
title: 'Introduction',
|
|
181
194
|
path: '/docs/intro',
|
|
182
|
-
|
|
195
|
+
paginationNext: null, // Explicitly disable
|
|
183
196
|
},
|
|
184
197
|
{
|
|
185
198
|
id: 'guide/getting-started.md',
|
|
199
|
+
fileId: 'guide/getting-started.md',
|
|
186
200
|
title: 'Getting Started',
|
|
187
201
|
path: '/docs/guide/getting-started',
|
|
188
202
|
},
|
|
@@ -194,19 +208,21 @@ describe('getPaginationInfo', () => {
|
|
|
194
208
|
expect(pagination.next).toBeUndefined()
|
|
195
209
|
})
|
|
196
210
|
|
|
197
|
-
it('should disable prev pagination when
|
|
211
|
+
it('should disable prev pagination when paginationPrev is null', () => {
|
|
198
212
|
const sidebar = createSidebar()
|
|
199
213
|
const docs: DocsData[] = [
|
|
200
214
|
{
|
|
201
215
|
id: 'intro.md',
|
|
216
|
+
fileId: 'intro.md',
|
|
202
217
|
title: 'Introduction',
|
|
203
218
|
path: '/docs/intro',
|
|
204
219
|
},
|
|
205
220
|
{
|
|
206
221
|
id: 'guide/getting-started.md',
|
|
222
|
+
fileId: 'guide/getting-started.md',
|
|
207
223
|
title: 'Getting Started',
|
|
208
224
|
path: '/docs/guide/getting-started',
|
|
209
|
-
|
|
225
|
+
paginationPrev: null, // Explicitly disable
|
|
210
226
|
},
|
|
211
227
|
]
|
|
212
228
|
|
|
@@ -228,10 +244,11 @@ describe('getPaginationInfo', () => {
|
|
|
228
244
|
const docs: DocsData[] = [
|
|
229
245
|
{
|
|
230
246
|
id: 'guide/getting-started.md',
|
|
247
|
+
fileId: 'guide/getting-started.md',
|
|
231
248
|
title: 'Getting Started',
|
|
232
249
|
path: '/docs/guide/getting-started',
|
|
233
|
-
|
|
234
|
-
|
|
250
|
+
paginationNext: null,
|
|
251
|
+
paginationPrev: null,
|
|
235
252
|
},
|
|
236
253
|
]
|
|
237
254
|
|
|
@@ -249,12 +266,14 @@ describe('getPaginationInfo', () => {
|
|
|
249
266
|
const docs: DocsData[] = [
|
|
250
267
|
{
|
|
251
268
|
id: 'intro.md',
|
|
269
|
+
fileId: 'intro.md',
|
|
252
270
|
title: 'Introduction',
|
|
253
271
|
path: '/docs/intro',
|
|
254
272
|
sidebarLabel: 'Intro', // Custom label
|
|
255
273
|
},
|
|
256
274
|
{
|
|
257
275
|
id: 'guide/getting-started.md',
|
|
276
|
+
fileId: 'guide/getting-started.md',
|
|
258
277
|
title: 'Getting Started',
|
|
259
278
|
path: '/docs/guide/getting-started',
|
|
260
279
|
},
|
|
@@ -266,8 +285,39 @@ describe('getPaginationInfo', () => {
|
|
|
266
285
|
docs,
|
|
267
286
|
)
|
|
268
287
|
|
|
288
|
+
// sidebarLabel is used for the title, so it should be 'Intro' not 'Introduction'
|
|
269
289
|
expect(pagination.prev).toEqual({
|
|
270
|
-
title: '
|
|
290
|
+
title: 'Intro',
|
|
291
|
+
href: '/docs/intro',
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('should use paginationLabel when available', () => {
|
|
296
|
+
const sidebar = createSidebar()
|
|
297
|
+
const docs: DocsData[] = [
|
|
298
|
+
{
|
|
299
|
+
id: 'intro.md',
|
|
300
|
+
title: 'Introduction',
|
|
301
|
+
path: '/docs/intro',
|
|
302
|
+
sidebarLabel: 'Intro',
|
|
303
|
+
paginationLabel: 'Start Here', // Custom pagination label takes priority
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
id: 'guide/getting-started.md',
|
|
307
|
+
title: 'Getting Started',
|
|
308
|
+
path: '/docs/guide/getting-started',
|
|
309
|
+
paginationPrev: 'intro.md',
|
|
310
|
+
},
|
|
311
|
+
]
|
|
312
|
+
|
|
313
|
+
const pagination = getPaginationInfo(
|
|
314
|
+
'/docs/guide/getting-started',
|
|
315
|
+
sidebar,
|
|
316
|
+
docs,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
expect(pagination.prev).toEqual({
|
|
320
|
+
title: 'Start Here',
|
|
271
321
|
href: '/docs/intro',
|
|
272
322
|
})
|
|
273
323
|
})
|
|
@@ -306,16 +356,19 @@ describe('getPaginationInfo', () => {
|
|
|
306
356
|
const docs: DocsData[] = [
|
|
307
357
|
{
|
|
308
358
|
id: 'guide/basics/page-1.md',
|
|
359
|
+
fileId: 'guide/basics/page-1.md',
|
|
309
360
|
title: 'Page 1',
|
|
310
361
|
path: '/docs/guide/basics/page-1',
|
|
311
362
|
},
|
|
312
363
|
{
|
|
313
364
|
id: 'guide/basics/page-2.md',
|
|
365
|
+
fileId: 'guide/basics/page-2.md',
|
|
314
366
|
title: 'Page 2',
|
|
315
367
|
path: '/docs/guide/basics/page-2',
|
|
316
368
|
},
|
|
317
369
|
{
|
|
318
370
|
id: 'guide/advanced/page-3.md',
|
|
371
|
+
fileId: 'guide/advanced/page-3.md',
|
|
319
372
|
title: 'Page 3',
|
|
320
373
|
path: '/docs/guide/advanced/page-3',
|
|
321
374
|
},
|
|
@@ -337,17 +390,19 @@ describe('getPaginationInfo', () => {
|
|
|
337
390
|
})
|
|
338
391
|
})
|
|
339
392
|
|
|
340
|
-
it('should handle invalid
|
|
393
|
+
it('should handle invalid paginationNext reference gracefully', () => {
|
|
341
394
|
const sidebar = createSidebar()
|
|
342
395
|
const docs: DocsData[] = [
|
|
343
396
|
{
|
|
344
397
|
id: 'intro.md',
|
|
398
|
+
fileId: 'intro.md',
|
|
345
399
|
title: 'Introduction',
|
|
346
400
|
path: '/docs/intro',
|
|
347
|
-
|
|
401
|
+
paginationNext: 'nonexistent.md', // Invalid reference
|
|
348
402
|
},
|
|
349
403
|
{
|
|
350
404
|
id: 'guide/getting-started.md',
|
|
405
|
+
fileId: 'guide/getting-started.md',
|
|
351
406
|
title: 'Getting Started',
|
|
352
407
|
path: '/docs/guide/getting-started',
|
|
353
408
|
},
|
|
@@ -359,19 +414,21 @@ describe('getPaginationInfo', () => {
|
|
|
359
414
|
expect(pagination.next).toBeUndefined()
|
|
360
415
|
})
|
|
361
416
|
|
|
362
|
-
it('should handle invalid
|
|
417
|
+
it('should handle invalid paginationPrev reference gracefully', () => {
|
|
363
418
|
const sidebar = createSidebar()
|
|
364
419
|
const docs: DocsData[] = [
|
|
365
420
|
{
|
|
366
421
|
id: 'intro.md',
|
|
422
|
+
fileId: 'intro.md',
|
|
367
423
|
title: 'Introduction',
|
|
368
424
|
path: '/docs/intro',
|
|
369
425
|
},
|
|
370
426
|
{
|
|
371
427
|
id: 'guide/getting-started.md',
|
|
428
|
+
fileId: 'guide/getting-started.md',
|
|
372
429
|
title: 'Getting Started',
|
|
373
430
|
path: '/docs/guide/getting-started',
|
|
374
|
-
|
|
431
|
+
paginationPrev: 'nonexistent.md', // Invalid reference
|
|
375
432
|
},
|
|
376
433
|
]
|
|
377
434
|
|
|
@@ -384,4 +441,47 @@ describe('getPaginationInfo', () => {
|
|
|
384
441
|
// Should not have a prev link because the reference is invalid
|
|
385
442
|
expect(pagination.prev).toBeUndefined()
|
|
386
443
|
})
|
|
444
|
+
|
|
445
|
+
it('should exclude unlisted pages from pagination (via sidebar)', () => {
|
|
446
|
+
// Note: unlisted pages are filtered from sidebar entries before being passed
|
|
447
|
+
// to getPaginationInfo, so they won't appear in pagination
|
|
448
|
+
const sidebar: Entry = {
|
|
449
|
+
intro: {
|
|
450
|
+
label: 'Introduction',
|
|
451
|
+
href: '/docs/intro',
|
|
452
|
+
},
|
|
453
|
+
// Hidden page is NOT in sidebar
|
|
454
|
+
visible: {
|
|
455
|
+
label: 'Visible',
|
|
456
|
+
href: '/docs/visible',
|
|
457
|
+
},
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const docs: DocsData[] = [
|
|
461
|
+
{
|
|
462
|
+
id: 'intro.md',
|
|
463
|
+
title: 'Introduction',
|
|
464
|
+
path: '/docs/intro',
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
id: 'hidden.md',
|
|
468
|
+
title: 'Hidden Page',
|
|
469
|
+
path: '/docs/hidden',
|
|
470
|
+
unlisted: true,
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
id: 'visible.md',
|
|
474
|
+
title: 'Visible',
|
|
475
|
+
path: '/docs/visible',
|
|
476
|
+
},
|
|
477
|
+
]
|
|
478
|
+
|
|
479
|
+
const pagination = getPaginationInfo('/docs/intro', sidebar, docs)
|
|
480
|
+
|
|
481
|
+
// Next should be 'visible', not 'hidden'
|
|
482
|
+
expect(pagination.next).toEqual({
|
|
483
|
+
title: 'Visible',
|
|
484
|
+
href: '/docs/visible',
|
|
485
|
+
})
|
|
486
|
+
})
|
|
387
487
|
})
|