Skip to content

Commit 5c22495

Browse files
authoredFeb 13, 2025··
fix hidden nav links when specified with type: 'page', href: '...' in _meta files (#4207)
* - add `Navbar.align` prop to align navigation links to the specified side. (default `'right'`) - fix hidden nav links when specified with `type: 'page', href: '...'` in `_meta` files * a * here * fix nav active link highlighting * upd * upd * upd * prettier
1 parent 4727d96 commit 5c22495

File tree

11 files changed

+116
-79
lines changed

11 files changed

+116
-79
lines changed
 

‎.changeset/ten-seahorses-cheat.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"nextra-theme-docs": patch
3+
"nextra": patch
4+
---
5+
6+
- add `Navbar.align` prop to align navigation links to the specified side. (default `'right'`)
7+
- fix hidden nav links when specified with `type: 'page', href: '...'` in `_meta` files

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

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { ToggleVisibilitySection } from 'components/toggle-visibility-section'
1616
| `chatIcon` | `ReactNode{:ts}` | `<DiscordIcon width="24" />{:ts}` | Icon of the chat link. |
1717
| `children` | `ReactNode{:ts}` | | Extra content after last icon. |
1818
| `className` | `string{:ts}` | | CSS class name. |
19+
| `align` | `'left' \| 'right'` | `'right'` | Aligns navigation links to the specified side. |
1920

2021
## Logo
2122

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

+17-13
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ const isMenu = (page: any): page is MenuItem => page.type === 'menu'
8888

8989
export const ClientNavbar: FC<{
9090
children: ReactNode
91-
}> = ({ children }) => {
91+
className?: string
92+
}> = ({ children, className }) => {
9293
const items = useConfig().normalizePagesResult.topLevelNavbarItems
9394
const themeConfig = useThemeConfig()
9495

@@ -97,8 +98,13 @@ export const ClientNavbar: FC<{
9798

9899
return (
99100
<>
100-
<div className="x:flex x:gap-4 x:overflow-x-auto nextra-scrollbar x:py-1.5 x:max-md:hidden">
101-
{items.map(page => {
101+
<div
102+
className={cn(
103+
'x:flex x:gap-4 x:overflow-x-auto nextra-scrollbar x:py-1.5 x:max-md:hidden',
104+
className
105+
)}
106+
>
107+
{items.map((page, _index, arr) => {
102108
if ('display' in page && page.display === 'hidden') return
103109
if (isMenu(page)) {
104110
return (
@@ -107,18 +113,16 @@ export const ClientNavbar: FC<{
107113
</NavbarMenu>
108114
)
109115
}
110-
let href = page.href || page.route || '#'
111-
112-
// If it's a directory
113-
if (page.children) {
114-
href =
115-
('frontMatter' in page ? page.route : page.firstChildRoute) ||
116-
href
117-
}
116+
const href =
117+
// If it's a directory
118+
('frontMatter' in page ? page.route : page.firstChildRoute) ||
119+
page.href ||
120+
page.route
118121

119122
const isCurrentPage =
120-
page.route === pathname ||
121-
pathname.startsWith(page.route + '/') ||
123+
href === pathname ||
124+
(pathname.startsWith(page.route + '/') &&
125+
arr.every(item => !('href' in item) || item.href !== pathname)) ||
122126
undefined
123127

124128
return (

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

+18-6
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ const propsSchema = z.strictObject({
1717
projectIcon: element.default(<GitHubIcon height="24" />),
1818
chatLink: z.string().optional(),
1919
chatIcon: element.default(<DiscordIcon width="24" />),
20-
className: z.string().optional()
20+
className: z.string().optional(),
21+
align: z.enum(['left', 'right']).default('right')
2122
})
2223

2324
type NavbarProps = z.input<typeof propsSchema>
@@ -35,8 +36,15 @@ export const Navbar: FC<NavbarProps> = props => {
3536
projectIcon,
3637
chatLink,
3738
chatIcon,
38-
className
39+
className,
40+
align
3941
} = data
42+
43+
const logoClass = cn(
44+
'x:flex x:items-center',
45+
align === 'right' && 'x:me-auto'
46+
)
47+
4048
return (
4149
<header
4250
className={cn(
@@ -55,21 +63,25 @@ export const Navbar: FC<NavbarProps> = props => {
5563
<nav
5664
style={{ height: 'var(--nextra-navbar-height)' }}
5765
className={cn(
58-
'x:mx-auto x:flex x:max-w-(--nextra-content-width) x:items-center x:justify-end x:gap-4 x:pl-[max(env(safe-area-inset-left),1.5rem)] x:pr-[max(env(safe-area-inset-right),1.5rem)]',
66+
'x:mx-auto x:flex x:max-w-(--nextra-content-width) x:items-center x:gap-4 x:pl-[max(env(safe-area-inset-left),1.5rem)] x:pr-[max(env(safe-area-inset-right),1.5rem)]',
67+
align === 'right' && 'x:justify-end',
5968
className
6069
)}
6170
>
6271
{logoLink ? (
6372
<NextLink
6473
href={typeof logoLink === 'string' ? logoLink : '/'}
65-
className="x:transition-opacity x:focus-visible:nextra-focus x:flex x:items-center x:hover:opacity-75 x:me-auto"
74+
className={cn(
75+
logoClass,
76+
'x:transition-opacity x:focus-visible:nextra-focus x:hover:opacity-75'
77+
)}
6678
>
6779
{logo}
6880
</NextLink>
6981
) : (
70-
<div className="x:flex x:items-center x:me-auto">{logo}</div>
82+
<div className={logoClass}>{logo}</div>
7183
)}
72-
<ClientNavbar>
84+
<ClientNavbar className={align === 'left' ? 'x:me-auto' : ''}>
7385
{projectLink && <Anchor href={projectLink}>{projectIcon}</Anchor>}
7486
{chatLink && <Anchor href={chatLink}>{chatIcon}</Anchor>}
7587
{children}

‎packages/nextra-theme-docs/src/components/sidebar.tsx

+1-3
Original file line numberDiff line numberDiff line change
@@ -194,9 +194,7 @@ const Separator: FC<{ title: string }> = ({ title }) => {
194194
: 'x:my-4'
195195
)}
196196
>
197-
{title || (
198-
<hr className="x:mx-2 x:border-t x:border-gray-200 x:dark:border-primary-100/10" />
199-
)}
197+
{title || <hr className="x:mx-2 x:border-t nextra-border" />}
200198
</li>
201199
)
202200
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -854,7 +854,7 @@ describe('generatePageMap()', () => {
854854
name: "sponsors",
855855
route: "/sponsors",
856856
frontMatter: app_sponsors_page
857-
}], globalMeta))
857+
}], globalMeta, true))
858858
859859
export const RouteToFilepath = {}"
860860
`)

‎packages/nextra/src/server/page-map/merge-meta-with-page-map.ts

+36-24
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import path from 'node:path'
22
import type {
3+
$NextraMetadata,
34
DynamicFolder,
45
DynamicMeta,
56
DynamicMetaItem,
67
Folder,
7-
NextraMetadata,
8+
MetaJsonFile,
89
PageMapItem
910
} from '../../types.js'
1011
import { pageTitleFromFilename } from '../utils.js'
@@ -19,9 +20,9 @@ function isFolder(value: DynamicMetaItem): value is DynamicFolder {
1920
)
2021
}
2122

22-
function normalizeMetaData(
23+
function normalizeMetaRecord(
2324
obj: DynamicMeta,
24-
map: Record<string, NextraMetadata>
25+
map: Record<string, $NextraMetadata>
2526
): DynamicMeta {
2627
return Object.fromEntries(
2728
Object.entries(obj).map(([key, value]) => {
@@ -41,12 +42,17 @@ function normalizeMetaData(
4142

4243
export function mergeMetaWithPageMap<T extends Folder | PageMapItem[]>(
4344
pageMap: T,
44-
meta: DynamicMeta
45+
meta: DynamicMeta,
46+
shouldCheckIndividualMetaFilesUsage = false
4547
): T {
4648
if ('children' in pageMap) {
4749
return {
4850
...pageMap,
49-
children: mergeMetaWithPageMap(pageMap.children, meta)
51+
children: mergeMetaWithPageMap(
52+
pageMap.children,
53+
meta,
54+
shouldCheckIndividualMetaFilesUsage
55+
)
5056
}
5157
}
5258
// @ts-expect-error -- pagePath exist
@@ -61,26 +67,32 @@ export function mergeMetaWithPageMap<T extends Folder | PageMapItem[]>(
6167
}
6268
return restParent
6369
})
64-
const hasMeta = 'data' in result[0]!
65-
if (hasMeta) {
66-
// @ts-expect-error fixme
67-
const childRoute = result[1].route
68-
const { dir } = path.parse(childRoute)
69-
const metaPath = `${dir.replace(/^\/$/, '')}/_meta`
70-
throw new Error(
71-
[
72-
'Merging an `_meta.global` file with a folder-specific `_meta` is unsupported.',
73-
`Move content of \`${metaPath}\` file into the \`_meta.global\` file`
74-
].join('\n')
75-
)
70+
const normalizedMetaRecord = normalizeMetaRecord(
71+
meta,
72+
// @ts-expect-error -- fixme
73+
Object.fromEntries(result.map(key => [key.name, key.frontMatter]))
74+
)
75+
const metaRecord = result[0] && 'data' in result[0] && result[0].data
76+
if (metaRecord) {
77+
if (shouldCheckIndividualMetaFilesUsage) {
78+
// @ts-expect-error fixme
79+
const childRoute = result[1].route
80+
const { dir } = path.parse(childRoute)
81+
const metaPath = `${dir.replace(/^\/$/, '')}/_meta`
82+
throw new Error(
83+
[
84+
'Merging an `_meta.global` file with a folder-specific `_meta` is unsupported.',
85+
`Move content of \`${metaPath}\` file into the \`_meta.global\` file`
86+
].join('\n')
87+
)
88+
}
89+
;(result[0] as MetaJsonFile).data = {
90+
...metaRecord,
91+
...normalizedMetaRecord
92+
}
93+
} else {
94+
result.unshift({ data: normalizedMetaRecord })
7695
}
77-
result.unshift({
78-
data: normalizeMetaData(
79-
meta,
80-
// @ts-expect-error -- fixme
81-
Object.fromEntries(result.map(key => [key.name, key.frontMatter]))
82-
)
83-
})
8496

8597
// @ts-expect-error -- fixme
8698
return result

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

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

@@ -39,18 +32,15 @@ function titlize(item: Folder | MdxFile, meta: MetaRecord): string {
3932
type MetaRecord = Record<string, Record<string, any>>
4033

4134
function sortFolder(pageMap: PageMapItem[] | Folder) {
42-
const newChildren: (
43-
| ({ title: string } & (Folder | MdxFile))
44-
| ((SeparatorItem | MenuItem) & { name: string })
45-
)[] = []
35+
const newChildren: (Folder | MdxFile)[] = []
4636

4737
const isFolder = !Array.isArray(pageMap)
4838

4939
const folder = (
5040
isFolder ? { ...pageMap } : { children: pageMap }
5141
) as ParsedFolder
5242

53-
const meta: MetaRecord = {}
43+
const meta: Record<string, Record<string, any>> = {}
5444
for (const item of folder.children) {
5545
if (
5646
isFolder &&
@@ -60,10 +50,7 @@ function sortFolder(pageMap: PageMapItem[] | Folder) {
6050
) {
6151
folder.frontMatter = item.frontMatter
6252
} else if ('children' in item) {
63-
newChildren.push({
64-
...normalizePageMap(item),
65-
title: titlize(item, meta)
66-
})
53+
newChildren.push(normalizePageMap(item))
6754
} else if ('data' in item) {
6855
for (const [key, titleOrObject] of Object.entries(item.data)) {
6956
const { data, error } = metaSchema.safeParse(titleOrObject)
@@ -79,16 +66,8 @@ function sortFolder(pageMap: PageMapItem[] | Folder) {
7966
// @ts-expect-error -- fixme
8067
meta[key] = data
8168
}
82-
} else if (
83-
'type' in item &&
84-
(item.type === 'separator' || item.type === 'menu')
85-
) {
86-
newChildren.push(item as any)
8769
} else {
88-
newChildren.push({
89-
...item,
90-
title: titlize(item, meta)
91-
})
70+
newChildren.push(item)
9271
}
9372
}
9473

@@ -121,7 +100,7 @@ function sortFolder(pageMap: PageMapItem[] | Folder) {
121100

122101
// Validate menu items, local page should exist
123102
const { children } = items.find(
124-
(i): i is { title: string } & Folder<MdxFile> => i.name === metaKey
103+
(i): i is Folder<MdxFile> => i.name === metaKey
125104
)!
126105
for (const [key, value] of Object.entries(
127106
// @ts-expect-error fixme
@@ -162,6 +141,22 @@ The field key "${metaKey}" in \`_meta\` file refers to a page that cannot be fou
162141
items.unshift({ data: meta })
163142
}
164143

165-
const result = isFolder ? { ...folder, children: items } : items
144+
const itemsWithTitle = items.map(item => {
145+
const isSeparator = 'type' in item && item.type === 'separator'
146+
if ('name' in item && !isSeparator) {
147+
return {
148+
...item,
149+
title: titlize(item, meta)
150+
}
151+
}
152+
return item
153+
})
154+
155+
const result = isFolder
156+
? {
157+
...folder,
158+
children: itemsWithTitle
159+
}
160+
: itemsWithTitle
166161
return result
167162
}

‎packages/nextra/src/server/page-map/to-js.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export function convertPageMapToJs({
6262

6363
let pageMapRawJs = pageMapResult.value.slice(0, -2 /* replace semicolon */)
6464
if (globalMetaPath) {
65-
pageMapRawJs = `mergeMetaWithPageMap(${pageMapRawJs}, globalMeta)`
65+
pageMapRawJs = `mergeMetaWithPageMap(${pageMapRawJs}, globalMeta, true)`
6666
}
6767

6868
const rawJs = `import { ${[

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,10 @@ export const itemSchema = z.strictObject({
144144
export const metaSchema = z.union([
145145
stringOrElement.transform(transformTitle),
146146
itemSchema,
147-
linkSchema.extend({ type: z.enum(['page', 'doc']).optional() }),
147+
linkSchema.extend({
148+
type: z.enum(['page', 'doc']).optional(),
149+
display: z.enum(['normal', 'hidden']).optional()
150+
}),
148151
separatorItemSchema,
149152
menuSchema
150153
])

‎packages/nextra/src/types.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export type Heading = {
7676
id: string
7777
}
7878

79-
export type NextraMetadata = Omit<Metadata, 'title'> & {
79+
export type $NextraMetadata = Omit<Metadata, 'title'> & {
8080
title: string
8181
filePath: string
8282
timestamp?: number
@@ -101,7 +101,7 @@ export type Nextra = (
101101
export type MDXWrapper = FC<{
102102
toc: Heading[]
103103
children: ReactNode
104-
metadata: NextraMetadata
104+
metadata: $NextraMetadata
105105
bottomContent?: ReactNode
106106
}>
107107

@@ -122,3 +122,8 @@ export type PagefindSearchOptions = {
122122
/** The set of sorts to use for this search, instead of relevancy */
123123
sort?: object
124124
}
125+
126+
export type NextraMetadata = Metadata & {
127+
asIndexPage?: boolean
128+
sidebarTitle?: string
129+
}

0 commit comments

Comments
 (0)
Please sign in to comment.