@levino/shipyard-docs 0.4.7 → 0.5.0-rc-20251126212334

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,97 @@
1
+ ---
2
+ import type { PaginationLink } from '../src/pagination'
3
+
4
+ interface Props {
5
+ prev?: PaginationLink
6
+ next?: PaginationLink
7
+ prevLabel?: string
8
+ nextLabel?: string
9
+ }
10
+
11
+ const { prev, next, prevLabel = 'Previous', nextLabel = 'Next' } = Astro.props
12
+
13
+ // Only render if there's at least one link
14
+ const shouldRender = prev || next
15
+ ---
16
+
17
+ {
18
+ shouldRender && (
19
+ <nav
20
+ class="pagination-nav mt-8 border-t border-base-300 pt-6"
21
+ aria-label="Docs navigation"
22
+ >
23
+ {prev ? (
24
+ <a
25
+ href={prev.href}
26
+ class="pagination-nav__link pagination-nav__link--prev"
27
+ rel="prev"
28
+ >
29
+ <span class="pagination-nav__sublabel">{prevLabel}</span>
30
+ <span class="pagination-nav__label">{prev.title}</span>
31
+ </a>
32
+ ) : (
33
+ <span />
34
+ )}
35
+
36
+ {next ? (
37
+ <a
38
+ href={next.href}
39
+ class="pagination-nav__link pagination-nav__link--next"
40
+ rel="next"
41
+ >
42
+ <span class="pagination-nav__sublabel">{nextLabel}</span>
43
+ <span class="pagination-nav__label">{next.title}</span>
44
+ </a>
45
+ ) : (
46
+ <span />
47
+ )}
48
+ </nav>
49
+ )
50
+ }
51
+
52
+ <style>
53
+ .pagination-nav {
54
+ display: grid;
55
+ grid-template-columns: repeat(2, 1fr);
56
+ gap: 1rem;
57
+ }
58
+
59
+ .pagination-nav__link {
60
+ display: block;
61
+ padding: 1rem;
62
+ border: 1px solid oklch(var(--bc) / 0.2);
63
+ border-radius: var(--rounded-box, 1rem);
64
+ transition: border-color 0.2s;
65
+ line-height: 1.25;
66
+ }
67
+
68
+ .pagination-nav__link:hover {
69
+ border-color: oklch(var(--p));
70
+ text-decoration: none;
71
+ }
72
+
73
+ .pagination-nav__link--next {
74
+ text-align: right;
75
+ }
76
+
77
+ .pagination-nav__sublabel {
78
+ display: block;
79
+ font-size: 0.75rem;
80
+ opacity: 0.7;
81
+ margin-bottom: 0.25rem;
82
+ }
83
+
84
+ .pagination-nav__label {
85
+ display: block;
86
+ font-weight: 600;
87
+ word-break: break-word;
88
+ }
89
+
90
+ .pagination-nav__link--prev .pagination-nav__label::before {
91
+ content: '« ';
92
+ }
93
+
94
+ .pagination-nav__link--next .pagination-nav__label::after {
95
+ content: ' »';
96
+ }
97
+ </style>
@@ -5,8 +5,10 @@ import type { NavigationTree } from '@levino/shipyard-base'
5
5
  import { Breadcrumbs, TableOfContents } from '@levino/shipyard-base/components'
6
6
  import BaseLayout from '@levino/shipyard-base/layouts/Page.astro'
7
7
  import { Array as EffectArray, Option } from 'effect'
8
+ import { getPaginationInfo } from '../src/pagination'
8
9
  import type { DocsData } from '../src/sidebarEntries'
9
10
  import { toSidebarEntries } from '../src/sidebarEntries'
11
+ import DocPagination from './DocPagination.astro'
10
12
 
11
13
  interface Props {
12
14
  headings?: { depth: number; text: string; slug: string }[]
@@ -28,10 +30,21 @@ const { headings = [], routeBasePath = 'docs', docsData } = Astro.props
28
30
  // Normalize the route base path
29
31
  const normalizedBasePath = routeBasePath.replace(/^\/+|\/+$/g, '')
30
32
 
31
- const getPath = (id: string) =>
32
- i18n
33
- ? `/${Astro.currentLocale}/${normalizedBasePath}/${id.slice(3)}`
34
- : `/${normalizedBasePath}/${id}`
33
+ const getPath = (id: string) => {
34
+ // Remove the .md extension if present and handle index pages
35
+ const cleanId = id.replace(/\.md$/, '')
36
+ const isIndex = cleanId.endsWith('/index') || cleanId === 'index'
37
+
38
+ if (i18n) {
39
+ // For i18n, slice off the locale prefix (e.g., 'en/')
40
+ const pathPart = cleanId.slice(3)
41
+ const finalPath = isIndex ? pathPart.replace(/\/?index$/, '') : pathPart
42
+ return `/${Astro.currentLocale}/${normalizedBasePath}/${finalPath}`
43
+ }
44
+
45
+ const finalPath = isIndex ? cleanId.replace(/\/?index$/, '') : cleanId
46
+ return `/${normalizedBasePath}/${finalPath}`
47
+ }
35
48
 
36
49
  // Use provided docsData or fetch from the default 'docs' collection
37
50
  const docs =
@@ -48,6 +61,8 @@ const docs =
48
61
  sidebar_label,
49
62
  sidebar_class_name,
50
63
  sidebar_custom_props,
64
+ pagination_next,
65
+ pagination_prev,
51
66
  },
52
67
  } = doc
53
68
  return {
@@ -68,6 +83,8 @@ const docs =
68
83
  sidebarLabel: sidebar_label,
69
84
  sidebarClassName: sidebar_class_name,
70
85
  sidebarCustomProps: sidebar_custom_props,
86
+ pagination_next,
87
+ pagination_prev,
71
88
  }
72
89
  }),
73
90
  )
@@ -79,6 +96,9 @@ const entries: NavigationTree =
79
96
  i18n && Astro.currentLocale
80
97
  ? (fullTree[Astro.currentLocale]?.subEntry ?? {})
81
98
  : fullTree
99
+
100
+ // Compute pagination info for the current page
101
+ const pagination = getPaginationInfo(Astro.url.pathname, entries, docs)
82
102
  ---
83
103
 
84
104
  <BaseLayout sidebarNavigation={entries}>
@@ -88,6 +108,7 @@ const entries: NavigationTree =
88
108
  <Breadcrumbs navigation={entries} />
89
109
  <TableOfContents links={headings} class="xl:hidden" />
90
110
  <slot />
111
+ <DocPagination prev={pagination.prev} next={pagination.next} />
91
112
  </div>
92
113
  </div>
93
114
  <div class="hidden xl:block col-span-3">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@levino/shipyard-docs",
3
- "version": "0.4.7",
3
+ "version": "0.5.0-rc-20251126212334",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/index.ts CHANGED
@@ -2,6 +2,9 @@ import type { AstroIntegration } from 'astro'
2
2
  import { glob } from 'astro/loaders'
3
3
  import { z } from 'astro/zod'
4
4
 
5
+ // Re-export pagination types and utilities
6
+ export type { PaginationInfo, PaginationLink } from './pagination'
7
+ export { getPaginationInfo } from './pagination'
5
8
  export type { DocsRouteConfig } from './routeHelpers'
6
9
  // Re-export route helpers
7
10
  export { getDocPath, getRouteParams } from './routeHelpers'
@@ -21,6 +24,8 @@ export const docsSchema = z.object({
21
24
  sidebar_position: z.number().optional(),
22
25
  sidebar_label: z.string().optional(),
23
26
  sidebar_class_name: z.string().optional(),
27
+ pagination_next: z.string().nullable().optional(),
28
+ pagination_prev: z.string().nullable().optional(),
24
29
  })
25
30
 
26
31
  /**
@@ -0,0 +1,387 @@
1
+ import type { Entry } from '@levino/shipyard-base'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { getPaginationInfo } from './pagination'
4
+ import type { DocsData } from './sidebarEntries'
5
+
6
+ describe('getPaginationInfo', () => {
7
+ const createSidebar = (): Entry => ({
8
+ intro: {
9
+ label: 'Introduction',
10
+ href: '/docs/intro',
11
+ },
12
+ guide: {
13
+ label: 'Guide',
14
+ subEntry: {
15
+ 'getting-started': {
16
+ label: 'Getting Started',
17
+ href: '/docs/guide/getting-started',
18
+ },
19
+ advanced: {
20
+ label: 'Advanced',
21
+ href: '/docs/guide/advanced',
22
+ },
23
+ },
24
+ },
25
+ api: {
26
+ label: 'API Reference',
27
+ href: '/docs/api',
28
+ },
29
+ })
30
+
31
+ const createDocs = (): DocsData[] => [
32
+ {
33
+ id: 'intro.md',
34
+ title: 'Introduction',
35
+ path: '/docs/intro',
36
+ },
37
+ {
38
+ id: 'guide/getting-started.md',
39
+ title: 'Getting Started',
40
+ path: '/docs/guide/getting-started',
41
+ },
42
+ {
43
+ id: 'guide/advanced.md',
44
+ title: 'Advanced',
45
+ path: '/docs/guide/advanced',
46
+ },
47
+ {
48
+ id: 'api.md',
49
+ title: 'API Reference',
50
+ path: '/docs/api',
51
+ },
52
+ ]
53
+
54
+ it('should return prev and next for a middle page', () => {
55
+ const sidebar = createSidebar()
56
+ const docs = createDocs()
57
+ const pagination = getPaginationInfo(
58
+ '/docs/guide/getting-started',
59
+ sidebar,
60
+ docs,
61
+ )
62
+
63
+ expect(pagination.prev).toEqual({
64
+ title: 'Introduction',
65
+ href: '/docs/intro',
66
+ })
67
+ expect(pagination.next).toEqual({
68
+ title: 'Advanced',
69
+ href: '/docs/guide/advanced',
70
+ })
71
+ })
72
+
73
+ it('should return only next for first page', () => {
74
+ const sidebar = createSidebar()
75
+ const docs = createDocs()
76
+ const pagination = getPaginationInfo('/docs/intro', sidebar, docs)
77
+
78
+ expect(pagination.prev).toBeUndefined()
79
+ expect(pagination.next).toEqual({
80
+ title: 'Getting Started',
81
+ href: '/docs/guide/getting-started',
82
+ })
83
+ })
84
+
85
+ it('should return only prev for last page', () => {
86
+ const sidebar = createSidebar()
87
+ const docs = createDocs()
88
+ const pagination = getPaginationInfo('/docs/api', sidebar, docs)
89
+
90
+ expect(pagination.prev).toEqual({
91
+ title: 'Advanced',
92
+ href: '/docs/guide/advanced',
93
+ })
94
+ expect(pagination.next).toBeUndefined()
95
+ })
96
+
97
+ it('should return empty object for page not in sidebar', () => {
98
+ const sidebar = createSidebar()
99
+ const docs = createDocs()
100
+ const pagination = getPaginationInfo('/docs/unknown', sidebar, docs)
101
+
102
+ expect(pagination).toEqual({})
103
+ })
104
+
105
+ it('should respect pagination_next override', () => {
106
+ const sidebar = createSidebar()
107
+ const docs: DocsData[] = [
108
+ {
109
+ id: 'intro.md',
110
+ title: 'Introduction',
111
+ path: '/docs/intro',
112
+ pagination_next: 'api.md', // Skip directly to api
113
+ },
114
+ {
115
+ id: 'guide/getting-started.md',
116
+ title: 'Getting Started',
117
+ path: '/docs/guide/getting-started',
118
+ },
119
+ {
120
+ id: 'guide/advanced.md',
121
+ title: 'Advanced',
122
+ path: '/docs/guide/advanced',
123
+ },
124
+ {
125
+ id: 'api.md',
126
+ title: 'API Reference',
127
+ path: '/docs/api',
128
+ },
129
+ ]
130
+
131
+ const pagination = getPaginationInfo('/docs/intro', sidebar, docs)
132
+
133
+ expect(pagination.prev).toBeUndefined()
134
+ expect(pagination.next).toEqual({
135
+ title: 'API Reference',
136
+ href: '/docs/api',
137
+ })
138
+ })
139
+
140
+ it('should respect pagination_prev override', () => {
141
+ const sidebar = createSidebar()
142
+ const docs: DocsData[] = [
143
+ {
144
+ id: 'intro.md',
145
+ title: 'Introduction',
146
+ path: '/docs/intro',
147
+ },
148
+ {
149
+ id: 'guide/getting-started.md',
150
+ title: 'Getting Started',
151
+ path: '/docs/guide/getting-started',
152
+ },
153
+ {
154
+ id: 'guide/advanced.md',
155
+ title: 'Advanced',
156
+ path: '/docs/guide/advanced',
157
+ },
158
+ {
159
+ id: 'api.md',
160
+ title: 'API Reference',
161
+ path: '/docs/api',
162
+ pagination_prev: 'intro.md', // Skip back to intro
163
+ },
164
+ ]
165
+
166
+ const pagination = getPaginationInfo('/docs/api', sidebar, docs)
167
+
168
+ expect(pagination.prev).toEqual({
169
+ title: 'Introduction',
170
+ href: '/docs/intro',
171
+ })
172
+ expect(pagination.next).toBeUndefined()
173
+ })
174
+
175
+ it('should disable next pagination when pagination_next is null', () => {
176
+ const sidebar = createSidebar()
177
+ const docs: DocsData[] = [
178
+ {
179
+ id: 'intro.md',
180
+ title: 'Introduction',
181
+ path: '/docs/intro',
182
+ pagination_next: null, // Explicitly disable
183
+ },
184
+ {
185
+ id: 'guide/getting-started.md',
186
+ title: 'Getting Started',
187
+ path: '/docs/guide/getting-started',
188
+ },
189
+ ]
190
+
191
+ const pagination = getPaginationInfo('/docs/intro', sidebar, docs)
192
+
193
+ expect(pagination.prev).toBeUndefined()
194
+ expect(pagination.next).toBeUndefined()
195
+ })
196
+
197
+ it('should disable prev pagination when pagination_prev is null', () => {
198
+ const sidebar = createSidebar()
199
+ const docs: DocsData[] = [
200
+ {
201
+ id: 'intro.md',
202
+ title: 'Introduction',
203
+ path: '/docs/intro',
204
+ },
205
+ {
206
+ id: 'guide/getting-started.md',
207
+ title: 'Getting Started',
208
+ path: '/docs/guide/getting-started',
209
+ pagination_prev: null, // Explicitly disable
210
+ },
211
+ ]
212
+
213
+ const pagination = getPaginationInfo(
214
+ '/docs/guide/getting-started',
215
+ sidebar,
216
+ docs,
217
+ )
218
+
219
+ expect(pagination.prev).toBeUndefined()
220
+ expect(pagination.next).toEqual({
221
+ title: 'Advanced',
222
+ href: '/docs/guide/advanced',
223
+ })
224
+ })
225
+
226
+ it('should disable all pagination when both are null', () => {
227
+ const sidebar = createSidebar()
228
+ const docs: DocsData[] = [
229
+ {
230
+ id: 'guide/getting-started.md',
231
+ title: 'Getting Started',
232
+ path: '/docs/guide/getting-started',
233
+ pagination_next: null,
234
+ pagination_prev: null,
235
+ },
236
+ ]
237
+
238
+ const pagination = getPaginationInfo(
239
+ '/docs/guide/getting-started',
240
+ sidebar,
241
+ docs,
242
+ )
243
+
244
+ expect(pagination).toEqual({})
245
+ })
246
+
247
+ it('should use sidebarLabel when available', () => {
248
+ const sidebar = createSidebar()
249
+ const docs: DocsData[] = [
250
+ {
251
+ id: 'intro.md',
252
+ title: 'Introduction',
253
+ path: '/docs/intro',
254
+ sidebarLabel: 'Intro', // Custom label
255
+ },
256
+ {
257
+ id: 'guide/getting-started.md',
258
+ title: 'Getting Started',
259
+ path: '/docs/guide/getting-started',
260
+ },
261
+ ]
262
+
263
+ const pagination = getPaginationInfo(
264
+ '/docs/guide/getting-started',
265
+ sidebar,
266
+ docs,
267
+ )
268
+
269
+ expect(pagination.prev).toEqual({
270
+ title: 'Introduction',
271
+ href: '/docs/intro',
272
+ })
273
+ })
274
+
275
+ it('should handle nested sidebar structure correctly', () => {
276
+ const sidebar: Entry = {
277
+ guide: {
278
+ label: 'Guide',
279
+ subEntry: {
280
+ basics: {
281
+ label: 'Basics',
282
+ subEntry: {
283
+ 'page-1': {
284
+ label: 'Page 1',
285
+ href: '/docs/guide/basics/page-1',
286
+ },
287
+ 'page-2': {
288
+ label: 'Page 2',
289
+ href: '/docs/guide/basics/page-2',
290
+ },
291
+ },
292
+ },
293
+ advanced: {
294
+ label: 'Advanced',
295
+ subEntry: {
296
+ 'page-3': {
297
+ label: 'Page 3',
298
+ href: '/docs/guide/advanced/page-3',
299
+ },
300
+ },
301
+ },
302
+ },
303
+ },
304
+ }
305
+
306
+ const docs: DocsData[] = [
307
+ {
308
+ id: 'guide/basics/page-1.md',
309
+ title: 'Page 1',
310
+ path: '/docs/guide/basics/page-1',
311
+ },
312
+ {
313
+ id: 'guide/basics/page-2.md',
314
+ title: 'Page 2',
315
+ path: '/docs/guide/basics/page-2',
316
+ },
317
+ {
318
+ id: 'guide/advanced/page-3.md',
319
+ title: 'Page 3',
320
+ path: '/docs/guide/advanced/page-3',
321
+ },
322
+ ]
323
+
324
+ const pagination = getPaginationInfo(
325
+ '/docs/guide/basics/page-2',
326
+ sidebar,
327
+ docs,
328
+ )
329
+
330
+ expect(pagination.prev).toEqual({
331
+ title: 'Page 1',
332
+ href: '/docs/guide/basics/page-1',
333
+ })
334
+ expect(pagination.next).toEqual({
335
+ title: 'Page 3',
336
+ href: '/docs/guide/advanced/page-3',
337
+ })
338
+ })
339
+
340
+ it('should handle invalid pagination_next reference gracefully', () => {
341
+ const sidebar = createSidebar()
342
+ const docs: DocsData[] = [
343
+ {
344
+ id: 'intro.md',
345
+ title: 'Introduction',
346
+ path: '/docs/intro',
347
+ pagination_next: 'nonexistent.md', // Invalid reference
348
+ },
349
+ {
350
+ id: 'guide/getting-started.md',
351
+ title: 'Getting Started',
352
+ path: '/docs/guide/getting-started',
353
+ },
354
+ ]
355
+
356
+ const pagination = getPaginationInfo('/docs/intro', sidebar, docs)
357
+
358
+ // Should not have a next link because the reference is invalid
359
+ expect(pagination.next).toBeUndefined()
360
+ })
361
+
362
+ it('should handle invalid pagination_prev reference gracefully', () => {
363
+ const sidebar = createSidebar()
364
+ const docs: DocsData[] = [
365
+ {
366
+ id: 'intro.md',
367
+ title: 'Introduction',
368
+ path: '/docs/intro',
369
+ },
370
+ {
371
+ id: 'guide/getting-started.md',
372
+ title: 'Getting Started',
373
+ path: '/docs/guide/getting-started',
374
+ pagination_prev: 'nonexistent.md', // Invalid reference
375
+ },
376
+ ]
377
+
378
+ const pagination = getPaginationInfo(
379
+ '/docs/guide/getting-started',
380
+ sidebar,
381
+ docs,
382
+ )
383
+
384
+ // Should not have a prev link because the reference is invalid
385
+ expect(pagination.prev).toBeUndefined()
386
+ })
387
+ })
@@ -0,0 +1,191 @@
1
+ import type { Entry } from '@levino/shipyard-base'
2
+ import type { DocsData } from './sidebarEntries'
3
+
4
+ export interface PaginationLink {
5
+ title: string
6
+ href: string
7
+ }
8
+
9
+ export interface PaginationInfo {
10
+ prev?: PaginationLink
11
+ next?: PaginationLink
12
+ }
13
+
14
+ interface FlattenedEntry {
15
+ title: string
16
+ href: string
17
+ }
18
+
19
+ /**
20
+ * Normalizes a path by removing trailing slashes (except for root path).
21
+ * This ensures consistent path comparison regardless of trailing slash variations.
22
+ */
23
+ const normalizePath = (path: string): string => {
24
+ if (path === '/') return path
25
+ return path.replace(/\/+$/, '')
26
+ }
27
+
28
+ /**
29
+ * Flattens a hierarchical sidebar entry structure into an ordered array of pages.
30
+ * This preserves the order in which pages appear in the sidebar, including nested items.
31
+ *
32
+ * @param entries - The hierarchical sidebar entries
33
+ * @returns An array of flattened entries with title and href
34
+ */
35
+ const flattenSidebarEntries = (entries: Entry): FlattenedEntry[] => {
36
+ const result: FlattenedEntry[] = []
37
+
38
+ const traverse = (entryRecord: Entry) => {
39
+ for (const [_key, value] of Object.entries(entryRecord)) {
40
+ // Add the current entry if it has an href (i.e., it's a page, not just a category)
41
+ if (value.href && value.label) {
42
+ result.push({
43
+ title: value.label,
44
+ href: value.href,
45
+ })
46
+ }
47
+
48
+ // Recursively traverse children
49
+ if (value.subEntry) {
50
+ traverse(value.subEntry)
51
+ }
52
+ }
53
+ }
54
+
55
+ traverse(entries)
56
+ return result
57
+ }
58
+
59
+ /**
60
+ * Finds the previous and next pages for a given document based on its position
61
+ * in the sidebar navigation.
62
+ *
63
+ * @param currentPath - The path of the current page (e.g., '/docs/getting-started')
64
+ * @param sidebarEntries - The hierarchical sidebar entries
65
+ * @param allDocs - All documentation pages (for looking up frontmatter overrides)
66
+ * @returns An object containing prev and/or next pagination links
67
+ */
68
+ export const getPaginationInfo = (
69
+ currentPath: string,
70
+ sidebarEntries: Entry,
71
+ allDocs: readonly DocsData[],
72
+ ): PaginationInfo => {
73
+ // Normalize the current path to handle trailing slash variations
74
+ const normalizedCurrentPath = normalizePath(currentPath)
75
+
76
+ // Find the current doc to check for pagination overrides
77
+ const currentDoc = allDocs.find(
78
+ (doc) => normalizePath(doc.path) === normalizedCurrentPath,
79
+ )
80
+
81
+ // Check for explicit pagination overrides in frontmatter
82
+ const paginationNext = currentDoc?.['pagination_next' as keyof DocsData]
83
+ const paginationPrev = currentDoc?.['pagination_prev' as keyof DocsData]
84
+
85
+ // If explicitly disabled (null), return empty pagination
86
+ if (paginationNext === null && paginationPrev === null) {
87
+ return {}
88
+ }
89
+
90
+ // Flatten the sidebar to get ordered list of pages
91
+ const flatPages = flattenSidebarEntries(sidebarEntries)
92
+
93
+ // Find current page index (using normalized paths for comparison)
94
+ const currentIndex = flatPages.findIndex(
95
+ (page) => normalizePath(page.href) === normalizedCurrentPath,
96
+ )
97
+
98
+ // Check if this is an index/landing page not in the sidebar
99
+ // Index pages are identified by having a doc but not being in the sidebar.
100
+ // They typically have a path like /en/docs or /docs (the base docs path).
101
+ // We detect this by checking if the current doc exists but isn't in flatPages.
102
+ // Additionally, check if the doc ID matches common index patterns:
103
+ // - Just a locale (e.g., 'en' from 'en/index.md')
104
+ // - Empty or 'index'
105
+ const docIdParts = currentDoc?.id?.split('/') ?? []
106
+ const lastIdPart = docIdParts[docIdParts.length - 1]
107
+ const isIndexPage =
108
+ currentIndex === -1 &&
109
+ currentDoc &&
110
+ // Doc exists but not in sidebar - could be an index page
111
+ // Check if the doc ID suggests it's an index (locale-only ID, 'index', or empty after locale)
112
+ (docIdParts.length === 1 || lastIdPart === 'index' || lastIdPart === '')
113
+
114
+ if (currentIndex === -1 && !isIndexPage) {
115
+ // Current page not found in sidebar and not an index page - no pagination
116
+ return {}
117
+ }
118
+
119
+ const result: PaginationInfo = {}
120
+
121
+ // Special handling for index pages: they come before all sidebar items
122
+ if (isIndexPage) {
123
+ // Index page has no previous, and first sidebar item as next
124
+ if (paginationPrev !== null && typeof paginationPrev === 'string') {
125
+ const targetDoc = allDocs.find((doc) => doc.id === paginationPrev)
126
+ if (targetDoc?.path) {
127
+ result.prev = {
128
+ title: targetDoc.sidebarLabel ?? targetDoc.title,
129
+ href: targetDoc.path,
130
+ }
131
+ }
132
+ }
133
+ // No prev for index page (unless explicitly set above)
134
+
135
+ if (paginationNext === null) {
136
+ // Explicitly disabled
137
+ result.next = undefined
138
+ } else if (typeof paginationNext === 'string') {
139
+ const targetDoc = allDocs.find((doc) => doc.id === paginationNext)
140
+ if (targetDoc?.path) {
141
+ result.next = {
142
+ title: targetDoc.sidebarLabel ?? targetDoc.title,
143
+ href: targetDoc.path,
144
+ }
145
+ }
146
+ } else if (flatPages.length > 0) {
147
+ // Use the first sidebar item as next
148
+ result.next = flatPages[0]
149
+ }
150
+
151
+ return result
152
+ }
153
+
154
+ // Handle previous page
155
+ if (paginationPrev === null) {
156
+ // Explicitly disabled
157
+ result.prev = undefined
158
+ } else if (typeof paginationPrev === 'string') {
159
+ // Explicitly set to a specific page ID
160
+ const targetDoc = allDocs.find((doc) => doc.id === paginationPrev)
161
+ if (targetDoc?.path) {
162
+ result.prev = {
163
+ title: targetDoc.sidebarLabel ?? targetDoc.title,
164
+ href: targetDoc.path,
165
+ }
166
+ }
167
+ } else if (currentIndex > 0) {
168
+ // Use the previous page in sidebar order
169
+ result.prev = flatPages[currentIndex - 1]
170
+ }
171
+
172
+ // Handle next page
173
+ if (paginationNext === null) {
174
+ // Explicitly disabled
175
+ result.next = undefined
176
+ } else if (typeof paginationNext === 'string') {
177
+ // Explicitly set to a specific page ID
178
+ const targetDoc = allDocs.find((doc) => doc.id === paginationNext)
179
+ if (targetDoc?.path) {
180
+ result.next = {
181
+ title: targetDoc.sidebarLabel ?? targetDoc.title,
182
+ href: targetDoc.path,
183
+ }
184
+ }
185
+ } else if (currentIndex < flatPages.length - 1) {
186
+ // Use the next page in sidebar order
187
+ result.next = flatPages[currentIndex + 1]
188
+ }
189
+
190
+ return result
191
+ }
@@ -8,6 +8,8 @@ export interface DocsData {
8
8
  sidebarPosition?: number
9
9
  sidebarLabel?: string
10
10
  sidebarClassName?: string
11
+ pagination_next?: string | null
12
+ pagination_prev?: string | null
11
13
  }
12
14
 
13
15
  interface TreeNode {