Skip to content

Commit 7949e28

Browse files
authoredFeb 6, 2025··
move setting pageMap items' title property from normalizePages to sortFolder (#4166)
* upd * upd * upd * upd * upd * upd * upd * upd * upd * upd * upd * upd * upd * upd * upd * upd * upd * upd * upd * upd * upd snapshots
1 parent b46d831 commit 7949e28

File tree

27 files changed

+371
-95
lines changed

27 files changed

+371
-95
lines changed
 

‎.changeset/three-monkeys-happen.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"nextra": patch
3+
---
4+
5+
move setting `pageMap` items' `title` property from `normalizePages` to `sortFolder`

‎docs/app/docs/built-ins/table/page.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ icon: TableIcon
55

66
import { Table } from 'nextra/components'
77

8-
# Table Component
8+
# `<Table>` Component
99

1010
A collection of built-in components designed to create styled, non-markdown
1111
(i.e., literal) HTML tables.

‎docs/app/docs/custom-theme/page.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { ExampleCode } from 'components/example-code'
22
import { Steps } from 'nextra/components'
33
import OldDocs from './old.mdx'
44

5-
# Custom theme
5+
# Custom Theme
66

77
A theme in Nextra works like a layout, that will be rendered as a wrapper for
88
all pages. This docs will walk you through the process of creating a custom

‎docs/app/docs/docs-theme/built-ins/footer/page.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ sidebarTitle: Footer
44

55
import { ToggleVisibilitySection } from 'components/toggle-visibility-section'
66

7-
# `Footer` Component
7+
# `<Footer>` Component
88

99
The footer area of the website. You can specify content for your default footer.
1010

‎docs/app/docs/docs-theme/built-ins/layout/page.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ sidebarTitle: Layout
44

55
import { ToggleVisibilitySection } from 'components/toggle-visibility-section'
66

7-
# `Layout` Component
7+
# `<Layout>` Component
88

99
The theme is configured with the `<Layout>` component. You should pass your
1010
config options as Layout's `props`, for example:

‎docs/app/docs/docs-theme/built-ins/navbar/page.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ sidebarTitle: Navbar
44

55
import { ToggleVisibilitySection } from 'components/toggle-visibility-section'
66

7-
# `Navbar` Component
7+
# `<Navbar>` Component
88

99
| | | | |
1010
| ------------- | ------------------------ | --------------------------------------- | -------------------------------------------------------------------------------------- |

‎docs/app/docs/docs-theme/built-ins/not-found/page.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
sidebarTitle: NotFoundPage
33
---
44

5-
# `NotFoundPage` Component
5+
# `<NotFoundPage>` Component
66

77
Options to configure report of broken link on not found page.
88

‎docs/app/docs/file-conventions/meta-file/page.mdx

+30-10
Original file line numberDiff line numberDiff line change
@@ -80,23 +80,43 @@ The generated `pageMap` will be:
8080
[
8181
// content/_meta.js
8282
{ "data": {} },
83-
// content/index.mdx
84-
{ "name": "index", "route": "/", "frontMatter": {} },
85-
// content/contact.md
86-
{ "name": "contact", "route": "/contact", "frontMatter": {} },
83+
{
84+
// content/index.mdx
85+
"name": "index",
86+
"route": "/",
87+
"title": "Index",
88+
"frontMatter": {}
89+
},
90+
{
91+
// content/contact.md
92+
"name": "contact",
93+
"route": "/contact",
94+
"title": "Contact",
95+
"frontMatter": {}
96+
},
8797
{
8898
// content/about
8999
"name": "about",
90100
"route": "/about",
101+
"title": "About",
91102
"children": [
92103
// content/about/_meta.js
93104
{ "data": {} },
94-
// content/about/index.mdx
95-
{ "name": "index", "route": "/about", "frontMatter": {} },
96-
// content/about/legal.md
97-
{ "name": "legal", "route": "/about/legal", "frontMatter": {} }
98-
],
99-
"title": "About"
105+
{
106+
// content/about/index.mdx
107+
"name": "index",
108+
"route": "/about",
109+
"title": "Index",
110+
"frontMatter": {}
111+
},
112+
{
113+
// content/about/legal.md
114+
"name": "legal",
115+
"route": "/about/legal",
116+
"title": "Legal",
117+
"frontMatter": {}
118+
}
119+
]
100120
}
101121
]
102122
```

‎docs/app/layout.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,15 @@ const footer = (
9292
const RootLayout: FC<{
9393
children: ReactNode
9494
}> = async ({ children }) => {
95+
const pageMap = await getPageMap()
9596
return (
9697
<html lang="en" dir="ltr" suppressHydrationWarning>
9798
<Head />
9899
<body>
99100
<Layout
100101
banner={banner}
101102
navbar={navbar}
102-
pageMap={await getPageMap()}
103+
pageMap={pageMap}
103104
docsRepositoryBase="https://github.com/shuding/nextra/tree/main/docs"
104105
editLink="Edit this page on GitHub"
105106
sidebar={{ defaultMenuCollapseLevel: 1 }}

‎packages/nextra-theme-docs/src/components/navbar/index.client.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export const ClientNavbar: FC<{
124124
return (
125125
<Anchor
126126
href={href}
127-
key={href}
127+
key={page.name}
128128
className={cn(
129129
classes.link,
130130
'x:aria-[current]:font-medium x:aria-[current]:subpixel-antialiased x:aria-[current]:text-current'

‎packages/nextra/src/client/normalize-pages.ts

+1-8
Original file line numberDiff line numberDiff line change
@@ -161,17 +161,10 @@ export function normalizePages({
161161
pageThemeContext: extendedPageThemeContext
162162
})
163163

164-
const title =
165-
extendedMeta.title ||
166-
currentItem.frontMatter?.sidebarTitle ||
167-
currentItem.frontMatter?.title ||
168-
// @ts-expect-error -- we use title for capitalize folders without index page
169-
(type === 'separator' ? '' : currentItem.title || currentItem.name)
170-
171164
const getItem = (): Item => ({
172165
...currentItem,
173166
type,
174-
title,
167+
...('title' in currentItem && { title: currentItem.title }),
175168
...(display && { display }),
176169
...(normalizedChildren && { children: [] })
177170
})

‎packages/nextra/src/server/__tests__/fixture/page-maps/page-map.ts

+42-42
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import type { PageMapItem } from '../../../../types.js'
22

33
export const usPageMap: PageMapItem[] = [
4+
{
5+
data: {
6+
index: 'Introduction',
7+
docs: 'Docs',
8+
examples: 'Examples',
9+
blog: 'Blog'
10+
}
11+
},
412
{
513
name: 'blog',
614
children: [
@@ -25,20 +33,41 @@ export const usPageMap: PageMapItem[] = [
2533
{
2634
name: 'docs',
2735
children: [
36+
{
37+
data: {
38+
'getting-started': 'Getting Started',
39+
options: 'Options',
40+
'global-configuration': 'Global Configuration',
41+
'data-fetching': 'Data Fetching',
42+
'error-handling': 'Error Handling',
43+
revalidation: 'Auto Revalidation',
44+
'conditional-fetching': 'Conditional Data Fetching',
45+
arguments: 'Arguments',
46+
mutation: 'Mutation',
47+
pagination: 'Pagination',
48+
prefetching: 'Prefetching',
49+
'with-nextjs': 'Next.js SSG and SSR',
50+
typescript: 'Typescript',
51+
suspense: 'Suspense',
52+
middleware: 'Middleware',
53+
advanced: 'Advanced',
54+
'change-log': 'Change Log'
55+
}
56+
},
2857
{
2958
name: 'advanced',
3059
children: [
31-
{
32-
name: 'cache',
33-
route: '/docs/advanced/cache'
34-
},
3560
{
3661
data: {
3762
cache: 'Cache',
3863
performance: 'Performance',
3964
'react-native': 'React Native'
4065
}
4166
},
67+
{
68+
name: 'cache',
69+
route: '/docs/advanced/cache'
70+
},
4271
{
4372
name: 'performance',
4473
route: '/docs/advanced/performance'
@@ -78,27 +107,6 @@ export const usPageMap: PageMapItem[] = [
78107
name: 'global-configuration',
79108
route: '/docs/global-configuration'
80109
},
81-
{
82-
data: {
83-
'getting-started': 'Getting Started',
84-
options: 'Options',
85-
'global-configuration': 'Global Configuration',
86-
'data-fetching': 'Data Fetching',
87-
'error-handling': 'Error Handling',
88-
revalidation: 'Auto Revalidation',
89-
'conditional-fetching': 'Conditional Data Fetching',
90-
arguments: 'Arguments',
91-
mutation: 'Mutation',
92-
pagination: 'Pagination',
93-
prefetching: 'Prefetching',
94-
'with-nextjs': 'Next.js SSG and SSR',
95-
typescript: 'Typescript',
96-
suspense: 'Suspense',
97-
middleware: 'Middleware',
98-
advanced: 'Advanced',
99-
'change-log': 'Change Log'
100-
}
101-
},
102110
{
103111
name: 'middleware',
104112
route: '/docs/middleware'
@@ -141,6 +149,15 @@ export const usPageMap: PageMapItem[] = [
141149
{
142150
name: 'examples',
143151
children: [
152+
{
153+
data: {
154+
basic: 'Basic Usage',
155+
auth: 'Authentication',
156+
'infinite-loading': 'Infinite Loading',
157+
'error-handling': 'Error Handling',
158+
ssr: 'Next.js SSR'
159+
}
160+
},
144161
{
145162
name: 'auth',
146163
route: '/examples/auth',
@@ -169,15 +186,6 @@ export const usPageMap: PageMapItem[] = [
169186
title: 'Infinite Loading'
170187
}
171188
},
172-
{
173-
data: {
174-
basic: 'Basic Usage',
175-
auth: 'Authentication',
176-
'infinite-loading': 'Infinite Loading',
177-
'error-handling': 'Error Handling',
178-
ssr: 'Next.js SSR'
179-
}
180-
},
181189
{
182190
name: 'ssr',
183191
route: '/examples/ssr',
@@ -194,13 +202,5 @@ export const usPageMap: PageMapItem[] = [
194202
frontMatter: {
195203
title: 'React Hooks for Data Fetching'
196204
}
197-
},
198-
{
199-
data: {
200-
index: 'Introduction',
201-
docs: 'Docs',
202-
examples: 'Examples',
203-
blog: 'Blog'
204-
}
205205
}
206206
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const metadata = {
2+
sidebarTitle: 'from sidebarTitle',
3+
title: 'from title'
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const metadata = {
2+
sidebarTitle: 'from sidebarTitle',
3+
title: 'from title'
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const metadata = {
2+
title: 'from title'
3+
}

‎packages/nextra/src/server/__tests__/fixture/page-maps/title/4-from-filename.mdx

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export default {
2+
_: {
3+
type: 'separator'
4+
},
5+
folder: 'from meta',
6+
'folder-with-index': '',
7+
'1-meta': 'from meta',
8+
'2-sidebar-title': '',
9+
'3-title': '',
10+
'4-from-filename': '',
11+
_2: {
12+
type: 'separator',
13+
title: 'separator with title'
14+
}
15+
}

‎packages/nextra/src/server/__tests__/fixture/page-maps/title/folder-with-index/foo.mdx

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const metadata = {
2+
asIndexPage: true,
3+
sidebarTitle: 'from sidebarTitle',
4+
title: 'from title'
5+
}

‎packages/nextra/src/server/__tests__/fixture/page-maps/title/folder/bar.mdx

Whitespace-only changes.

‎packages/nextra/src/server/__tests__/fixture/page-maps/title/folder/index.mdx

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// @ts-nocheck
2+
import { normalizePageMap } from 'nextra/page-map'
3+
4+
import meta from "./_meta.ts";
5+
import {metadata as _1_meta} from "./1-meta.mdx?metadata";
6+
import {metadata as _2_sidebar_title} from "./2-sidebar-title.mdx?metadata";
7+
import {metadata as _3_title} from "./3-title.mdx?metadata";
8+
import {metadata as _4_from_filename} from "./4-from-filename.mdx?metadata";
9+
import {metadata as folder_with_index_foo} from "./folder-with-index/foo.mdx?metadata";
10+
import {metadata as folder_with_index_index} from "./folder-with-index/index.mdx?metadata";
11+
import {metadata as folder_bar} from "./folder/bar.mdx?metadata";
12+
import {metadata as folder_index} from "./folder/index.mdx?metadata";
13+
14+
export const pageMap = normalizePageMap([{
15+
data: meta
16+
}, {
17+
name: "1-meta",
18+
route: "/1-meta",
19+
frontMatter: _1_meta
20+
}, {
21+
name: "2-sidebar-title",
22+
route: "/2-sidebar-title",
23+
frontMatter: _2_sidebar_title
24+
}, {
25+
name: "3-title",
26+
route: "/3-title",
27+
frontMatter: _3_title
28+
}, {
29+
name: "4-from-filename",
30+
route: "/4-from-filename",
31+
frontMatter: _4_from_filename
32+
}, {
33+
name: "folder-with-index",
34+
route: "/folder-with-index",
35+
children: [{
36+
name: "foo",
37+
route: "/folder-with-index/foo",
38+
frontMatter: folder_with_index_foo
39+
}, {
40+
name: "index",
41+
route: "/folder-with-index",
42+
frontMatter: folder_with_index_index
43+
}]
44+
}, {
45+
name: "folder",
46+
route: "/folder",
47+
children: [{
48+
name: "bar",
49+
route: "/folder/bar",
50+
frontMatter: folder_bar
51+
}, {
52+
name: "index",
53+
route: "/folder",
54+
frontMatter: folder_index
55+
}]
56+
}])
57+
58+
export const RouteToFilepath = {
59+
"1-meta": "1-meta.mdx",
60+
"2-sidebar-title": "2-sidebar-title.mdx",
61+
"3-title": "3-title.mdx",
62+
"4-from-filename": "4-from-filename.mdx",
63+
"folder-with-index/foo": "folder-with-index/foo.mdx",
64+
"folder-with-index": "folder-with-index/index.mdx",
65+
"folder/bar": "folder/bar.mdx",
66+
"folder": "folder/index.mdx"
67+
}

‎packages/nextra/src/server/__tests__/normalize.test.ts

+133-15
Original file line numberDiff line numberDiff line change
@@ -61,21 +61,24 @@ describe('normalize-page', () => {
6161
{
6262
name: 'foo',
6363
route: '/1-level/2-level/foo',
64-
frontMatter: undefined
64+
frontMatter: undefined,
65+
title: 'Foo'
6566
}
6667
]
6768
},
6869
{
6970
name: 'qux',
7071
route: '/1-level/qux',
71-
frontMatter: undefined
72+
frontMatter: undefined,
73+
title: 'Qux'
7274
}
7375
]
7476
},
7577
{
7678
name: 'bar',
7779
route: '/bar',
78-
frontMatter: undefined
80+
frontMatter: undefined,
81+
title: 'Bar'
7982
}
8083
])
8184

@@ -120,7 +123,8 @@ describe('normalize-page', () => {
120123
{
121124
name: 'foo',
122125
route: '/1-level/foo',
123-
frontMatter: undefined
126+
frontMatter: undefined,
127+
title: 'Foo'
124128
}
125129
]
126130
}
@@ -166,15 +170,14 @@ describe('normalize-page', () => {
166170
{
167171
"isUnderCurrentDocsTree": true,
168172
"name": "---",
169-
"title": "",
170173
"type": "separator",
171174
},
172175
{
173176
"frontMatter": undefined,
174177
"isUnderCurrentDocsTree": true,
175178
"name": "qux",
176179
"route": "/one/two/qux",
177-
"title": "qux",
180+
"title": "Qux",
178181
"type": "doc",
179182
},
180183
{
@@ -189,7 +192,7 @@ describe('normalize-page', () => {
189192
"isUnderCurrentDocsTree": true,
190193
"name": "1-one",
191194
"route": "/one/two/1-one",
192-
"title": "1-one",
195+
"title": "1 One",
193196
"type": "doc",
194197
},
195198
{
@@ -205,15 +208,15 @@ describe('normalize-page', () => {
205208
"isUnderCurrentDocsTree": true,
206209
"name": "foo",
207210
"route": "/one/two/foo",
208-
"title": "foo",
211+
"title": "Foo",
209212
"type": "doc",
210213
},
211214
{
212215
"frontMatter": undefined,
213216
"isUnderCurrentDocsTree": true,
214217
"name": "one",
215218
"route": "/one/two/one",
216-
"title": "one",
219+
"title": "One",
217220
"type": "doc",
218221
},
219222
],
@@ -255,14 +258,14 @@ describe('normalize-page', () => {
255258
"frontMatter": undefined,
256259
"name": "not-specified",
257260
"route": "/mix/not-specified",
258-
"title": "not-specified",
261+
"title": "Not Specified",
259262
"type": "doc",
260263
},
261264
{
262265
"frontMatter": undefined,
263266
"name": "qux",
264267
"route": "/mix/qux",
265-
"title": "qux",
268+
"title": "Qux",
266269
"type": "doc",
267270
},
268271
],
@@ -298,14 +301,14 @@ describe('normalize-page', () => {
298301
"frontMatter": undefined,
299302
"name": "one",
300303
"route": "/pagesOnly/one",
301-
"title": "one",
304+
"title": "One",
302305
"type": "doc",
303306
},
304307
{
305308
"frontMatter": undefined,
306309
"name": "two",
307310
"route": "/pagesOnly/two",
308-
"title": "two",
311+
"title": "Two",
309312
"type": "doc",
310313
},
311314
],
@@ -359,7 +362,7 @@ describe('normalize-page', () => {
359362
route: '/themes/bar',
360363
frontMatter: undefined,
361364
type: 'doc',
362-
title: 'bar',
365+
title: 'Bar',
363366
isUnderCurrentDocsTree: true
364367
}
365368
],
@@ -379,7 +382,7 @@ describe('normalize-page', () => {
379382
route: '/themes-test/foo',
380383
frontMatter: undefined,
381384
type: 'doc',
382-
title: 'foo',
385+
title: 'Foo',
383386
isUnderCurrentDocsTree: true
384387
}
385388
],
@@ -436,4 +439,119 @@ describe('normalize-page', () => {
436439
)
437440
expect(pageMap).toBeInstanceOf(Array)
438441
})
442+
443+
it('title priority', async () => {
444+
const pageMap = await getPageMapForFixture('title')
445+
expect(pageMap).toMatchInlineSnapshot(`
446+
[
447+
{
448+
"data": {
449+
"1-meta": {
450+
"title": "from meta",
451+
},
452+
"2-sidebar-title": {
453+
"title": "",
454+
},
455+
"3-title": {
456+
"title": "",
457+
},
458+
"4-from-filename": {
459+
"title": "",
460+
},
461+
"_": {
462+
"type": "separator",
463+
},
464+
"_2": {
465+
"title": "separator with title",
466+
"type": "separator",
467+
},
468+
"folder": {
469+
"title": "from meta",
470+
},
471+
"folder-with-index": {
472+
"title": "",
473+
},
474+
},
475+
},
476+
{
477+
"name": "_",
478+
"type": "separator",
479+
},
480+
{
481+
"children": [
482+
{
483+
"frontMatter": undefined,
484+
"name": "index",
485+
"route": "/folder",
486+
"title": "Index",
487+
},
488+
{
489+
"frontMatter": undefined,
490+
"name": "bar",
491+
"route": "/folder/bar",
492+
"title": "Bar",
493+
},
494+
],
495+
"name": "folder",
496+
"route": "/folder",
497+
"title": "from meta",
498+
},
499+
{
500+
"children": [
501+
{
502+
"frontMatter": undefined,
503+
"name": "foo",
504+
"route": "/folder-with-index/foo",
505+
"title": "Foo",
506+
},
507+
],
508+
"frontMatter": {
509+
"asIndexPage": true,
510+
"sidebarTitle": "from sidebarTitle",
511+
"title": "from title",
512+
},
513+
"name": "folder-with-index",
514+
"route": "/folder-with-index",
515+
"title": "from sidebarTitle",
516+
},
517+
{
518+
"frontMatter": {
519+
"sidebarTitle": "from sidebarTitle",
520+
"title": "from title",
521+
},
522+
"name": "1-meta",
523+
"route": "/1-meta",
524+
"title": "from meta",
525+
},
526+
{
527+
"frontMatter": {
528+
"sidebarTitle": "from sidebarTitle",
529+
"title": "from title",
530+
},
531+
"name": "2-sidebar-title",
532+
"route": "/2-sidebar-title",
533+
"title": "from sidebarTitle",
534+
},
535+
{
536+
"frontMatter": {
537+
"title": "from title",
538+
},
539+
"name": "3-title",
540+
"route": "/3-title",
541+
"title": "from title",
542+
},
543+
{
544+
"frontMatter": undefined,
545+
"name": "4-from-filename",
546+
"route": "/4-from-filename",
547+
"title": "4 from Filename",
548+
},
549+
{
550+
"name": "_2",
551+
"title": "separator with title",
552+
"type": "separator",
553+
},
554+
]
555+
`)
556+
})
439557
})

‎packages/nextra/src/server/__tests__/page-map.test.ts

+4
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ describe('Page Process', () => {
1414
"frontMatter": undefined,
1515
"name": "callout",
1616
"route": "/callout",
17+
"title": "Callout",
1718
},
1819
{
1920
"frontMatter": undefined,
2021
"name": "tabs",
2122
"route": "/tabs",
23+
"title": "Tabs",
2224
},
2325
]
2426
`)
@@ -34,6 +36,7 @@ describe('Page Process', () => {
3436
"frontMatter": undefined,
3537
"name": "test2",
3638
"route": "/docs/test2",
39+
"title": "Test2",
3740
},
3841
],
3942
"name": "docs",
@@ -44,6 +47,7 @@ describe('Page Process', () => {
4447
"frontMatter": undefined,
4548
"name": "test1",
4649
"route": "/test1",
50+
"title": "Test1",
4751
},
4852
]
4953
`)

‎packages/nextra/src/server/page-map/normalize.ts

+42-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { fromZodError } from 'zod-validation-error'
2-
import type { Folder, FrontMatter, MdxFile, PageMapItem } from '../../types.js'
2+
import type {
3+
Folder,
4+
FrontMatter,
5+
MdxFile,
6+
MenuItem,
7+
PageMapItem,
8+
SeparatorItem
9+
} from '../../types.js'
310
import { metaSchema } from '../schemas.js'
411
import { pageTitleFromFilename } from '../utils.js'
512

@@ -16,16 +23,34 @@ type ParsedFolder = Folder & {
1623
frontMatter?: FrontMatter
1724
}
1825

26+
function titlize(item: Folder | MdxFile, meta: MetaRecord): string {
27+
const titleFromMeta = meta[item.name]?.title
28+
if (titleFromMeta) return titleFromMeta
29+
if ('frontMatter' in item && item.frontMatter) {
30+
const titleFromFrontMatter =
31+
item.frontMatter.sidebarTitle || item.frontMatter.title
32+
33+
if (titleFromFrontMatter) return titleFromFrontMatter
34+
}
35+
// We use `title` package for capitalize folders without index page
36+
return pageTitleFromFilename(item.name)
37+
}
38+
39+
type MetaRecord = Record<string, Record<string, any>>
40+
1941
function sortFolder(pageMap: PageMapItem[] | Folder) {
20-
const newChildren: (Folder | MdxFile)[] = []
42+
const newChildren: (
43+
| ({ title: string } & (Folder | MdxFile))
44+
| ((SeparatorItem | MenuItem) & { name: string })
45+
)[] = []
2146

2247
const isFolder = !Array.isArray(pageMap)
2348

2449
const folder = (
2550
isFolder ? { ...pageMap } : { children: pageMap }
2651
) as ParsedFolder
2752

28-
const meta: Record<string, Record<string, any>> = {}
53+
const meta: MetaRecord = {}
2954
for (const item of folder.children) {
3055
if (
3156
isFolder &&
@@ -35,7 +60,10 @@ function sortFolder(pageMap: PageMapItem[] | Folder) {
3560
) {
3661
folder.frontMatter = item.frontMatter
3762
} else if ('children' in item) {
38-
newChildren.push(normalizePageMap(item))
63+
newChildren.push({
64+
...normalizePageMap(item),
65+
title: titlize(item, meta)
66+
})
3967
} else if ('data' in item) {
4068
for (const [key, titleOrObject] of Object.entries(item.data)) {
4169
const { data, error } = metaSchema.safeParse(titleOrObject)
@@ -51,14 +79,18 @@ function sortFolder(pageMap: PageMapItem[] | Folder) {
5179
// @ts-expect-error -- fixme
5280
meta[key] = data
5381
}
82+
} else if (
83+
'type' in item &&
84+
(item.type === 'separator' || item.type === 'menu')
85+
) {
86+
newChildren.push(item as any)
5487
} else {
55-
newChildren.push(item)
88+
newChildren.push({
89+
...item,
90+
title: titlize(item, meta)
91+
})
5692
}
5793
}
58-
if (folder.name && !folder.frontMatter?.title) {
59-
// @ts-expect-error -- we use title for capitalize folders without index page
60-
folder.title = pageTitleFromFilename(folder.name)
61-
}
6294

6395
const metaKeys = Object.keys(meta)
6496
const hasIndexKey = metaKeys.includes('index')
@@ -89,7 +121,7 @@ function sortFolder(pageMap: PageMapItem[] | Folder) {
89121

90122
// Validate menu items, local page should exist
91123
const { children } = items.find(
92-
(i): i is Folder<MdxFile> => i.name === metaKey
124+
(i): i is { title: string } & Folder<MdxFile> => i.name === metaKey
93125
)!
94126
for (const [key, value] of Object.entries(
95127
// @ts-expect-error fixme

‎packages/nextra/src/server/schemas.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ const linkSchema = z.strictObject({
9595
href: z.string()
9696
})
9797

98-
const separatorItemSchema = z.strictObject({
98+
export const separatorItemSchema = z.strictObject({
9999
type: z.literal('separator'),
100100
title
101101
})

‎packages/nextra/src/types.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import type { FC, ReactElement, ReactNode } from 'react'
44
import type { z } from 'zod'
55
import type {
66
mathJaxOptionsSchema,
7+
menuSchema,
78
metaSchema,
8-
nextraConfigSchema
9+
nextraConfigSchema,
10+
separatorItemSchema
911
} from './server/schemas.js'
1012

1113
export interface LoaderOptions extends z.infer<typeof nextraConfigSchema> {
@@ -105,6 +107,9 @@ export type MDXWrapper = FC<{
105107

106108
export type MetaRecord = Record<string, z.infer<typeof metaSchema>>
107109

110+
export type SeparatorItem = z.infer<typeof separatorItemSchema>
111+
export type MenuItem = z.infer<typeof menuSchema>
112+
108113
// Copied from https://github.com/CloudCannon/pagefind/blob/2a0aa90cfb78bb8551645ac9127a1cd49cf54add/pagefind_web_js/types/index.d.ts#L72-L82
109114
/** Options that can be passed to pagefind.search() */
110115
export type PagefindSearchOptions = {

0 commit comments

Comments
 (0)
Please sign in to comment.