Skip to content

Commit a22268a

Browse files
committed
Exclude cross links from pagelinks.
1 parent 80da7f5 commit a22268a

7 files changed

Lines changed: 133 additions & 5 deletions

File tree

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
11
{
2-
"pageLinks": true
2+
"pageLinks": true,
3+
"sequence": [
4+
"index.mdx",
5+
"guides"
6+
],
7+
"items": [
8+
{
9+
"id": "guides",
10+
"title": "Guides",
11+
"url": "/guides/",
12+
"icon": "tabler:book-2"
13+
}
14+
]
315
}

‎frontend/purestack.studio/guides/_nav.json‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"pageLinks": true,
33
"sequence": [
44
"index.mdx",
5+
"components",
56
"purestack-cli.mdx",
67
"site-config.mdx",
78
"vscode-extension.mdx",
@@ -11,5 +12,13 @@
1112
"semantic-tones.mdx",
1213
"icons.mdx",
1314
"themes.mdx"
15+
],
16+
"items": [
17+
{
18+
"id": "components",
19+
"title": "Components",
20+
"url": "/components/",
21+
"icon": "tabler:components"
22+
}
1423
]
1524
}

‎frontend/purestack.studio/guides/site-config.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ In `hybrid` mode, a folder's `_nav.json` can order discovered pages, add custom
172172
}
173173
```
174174

175-
Place this in `content/guides/_nav.json`. A nav file can also use `"mode": "override"` to replace that folder's discovered items, or `"root"` to select another built navigation root. The default nav filename is `_nav.json`.
175+
Place this in `content/guides/_nav.json`. A custom item's `url` is relative to the nav file's folder, unless it starts with `/`, which makes it site-absolute: `/components/` links to another section. Give an item an `id` to place it with `sequence`. Previous and next links stay within the current navigation root, so a link to another section appears in the menu but is skipped by previous and next. A nav file can also use `"mode": "override"` to replace that folder's discovered items, or `"root"` to select another built navigation root. The default nav filename is `_nav.json`.
176176

177177
`pageToc.enabled` sets the default for a page's table of contents; individual pages can override it with frontmatter `layout.showToc`. `docLayout.sidebarTop` and `sidebarTopMobile` are CSS lengths measured from the viewport top. Adjust them when your header height changes so the sidebars and mobile drawers start below it.
178178

‎packages/ts-ssg/src/navigation/nav-file.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,10 @@ function resolveUrl(raw: Record<string, unknown>, folder: string) {
129129

130130
function resolveInternalUrl(folder: string, url: string) {
131131
const { base, suffix } = urlNormalizer.splitSuffix(url)
132-
const joined = path.posix.join('/', folder, base)
132+
// A leading slash is site-absolute, so it must not be nested in the folder.
133+
const joined = base.startsWith('/')
134+
? path.posix.normalize(base)
135+
: path.posix.join('/', folder, base)
133136
const hasExt = path.posix.extname(joined).length > 0
134137
const normalized = hasExt || joined.endsWith('/') ? joined : `${joined}/`
135138
return `${normalized}${suffix}`

‎packages/ts-ssg/src/navigation/navigation.test.ts‎

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,38 @@ describe('navigation', () => {
567567
})
568568
})
569569

570+
it('keeps site-absolute custom item urls outside the nav file folder', async () => {
571+
await withTempDir(async (base) => {
572+
const contentDir = path.join(base, 'content')
573+
await fs.mkdir(path.join(contentDir, 'guides'), { recursive: true })
574+
await fs.writeFile(
575+
path.join(contentDir, 'guides', 'index.mdx'),
576+
`---\ntitle: Guides\n---\n# Guides\n`,
577+
'utf8',
578+
)
579+
await fs.writeFile(
580+
path.join(contentDir, 'guides', '_nav.json'),
581+
JSON.stringify({
582+
items: [
583+
{ title: 'Components', url: '/components/' },
584+
{ title: 'Themes', url: 'themes' },
585+
],
586+
}),
587+
'utf8',
588+
)
589+
590+
const files = await discoverContent(contentDir)
591+
const nav = await buildNavigation(contentDir, files, {
592+
mode: 'hybrid',
593+
maxDepth: 20,
594+
})
595+
const urls = (nav?.byFolder.guides ?? []).map((item) => item.url)
596+
597+
expect(urls).toContain('/components/')
598+
expect(urls).toContain('/guides/themes/')
599+
})
600+
})
601+
570602
it('applies nav file icons to mixed auto and custom items', async () => {
571603
await withTempDir(async (base) => {
572604
const contentDir = path.join(base, 'content')
@@ -741,6 +773,67 @@ describe('navigation', () => {
741773
})
742774
})
743775

776+
it('keeps page links inside the current navigation root', async () => {
777+
await withTempDir(async (base) => {
778+
const contentDir = path.join(base, 'content')
779+
await fs.mkdir(path.join(contentDir, 'guides'), { recursive: true })
780+
await fs.mkdir(path.join(contentDir, 'components'), { recursive: true })
781+
await fs.writeFile(
782+
path.join(contentDir, 'guides', 'index.mdx'),
783+
`---\ntitle: Guides\n---\n# Guides\n`,
784+
'utf8',
785+
)
786+
await fs.writeFile(
787+
path.join(contentDir, 'guides', 'setup.mdx'),
788+
`---\ntitle: Setup\n---\n# Setup\n`,
789+
'utf8',
790+
)
791+
await fs.writeFile(
792+
path.join(contentDir, 'components', 'index.mdx'),
793+
`---\ntitle: Components\n---\n# Components\n`,
794+
'utf8',
795+
)
796+
await fs.writeFile(
797+
path.join(contentDir, 'guides', '_nav.json'),
798+
JSON.stringify({
799+
pageLinks: true,
800+
sequence: ['index.mdx', 'components', 'setup.mdx'],
801+
items: [
802+
{ id: 'components', title: 'Components', url: '/components/' },
803+
],
804+
}),
805+
'utf8',
806+
)
807+
808+
const files = await discoverContent(contentDir)
809+
const nav = await buildNavigation(contentDir, files, {
810+
mode: 'hybrid',
811+
maxDepth: 20,
812+
roots: ['guides', 'components'],
813+
})
814+
const guides = resolvePageNavigation(
815+
nav,
816+
findContentFile(files, 'guides/index.mdx'),
817+
)
818+
const setup = resolvePageNavigation(
819+
nav,
820+
findContentFile(files, 'guides/setup.mdx'),
821+
)
822+
823+
expect(guides?.items.map((item) => item.title)).toEqual([
824+
'Guides',
825+
'Components',
826+
'Setup',
827+
])
828+
expect(guides?.pageLinks).toEqual({
829+
next: { title: 'Setup', url: '/guides/setup/' },
830+
})
831+
expect(setup?.pageLinks).toEqual({
832+
previous: { title: 'Guides', url: '/guides/' },
833+
})
834+
})
835+
})
836+
744837
it('falls back to the locale root for localized pages outside configured navigation roots', async () => {
745838
await withTempDir(async (base) => {
746839
const contentDir = path.join(base, 'content')

‎packages/ts-ssg/src/navigation/navigation.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export function resolvePageNavigation(
8787
? (tree.rootByFolder[folder] ?? configuredRoot)
8888
: configuredRoot
8989
const items = tree.byFolder[root] ?? tree.byFolder[folder] ?? []
90-
const pageLinks = resolvePageLinks(tree, folder, items, file)
90+
const pageLinks = resolvePageLinks(tree, folder, root, items, file)
9191
return {
9292
mode: tree.mode,
9393
folder,

‎packages/ts-ssg/src/navigation/page-links.ts‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,19 @@ export function buildPageLinksByFolder(
2020
export function resolvePageLinks(
2121
tree: NavigationTree,
2222
folder: string,
23+
root: string,
2324
items: NavItem[],
2425
file: NavigationContentFile,
2526
): PageNavigationLinks | undefined {
2627
if (!isPageLinksEnabled(tree, folder)) return undefined
2728
const currentUrl = normalizePageLinkUrl(resolveRouteInfo(file).urlPath)
2829
if (!currentUrl) return undefined
2930

30-
const links = flattenPageLinks(items)
31+
// Previous and next walk one section. Items that link to another section,
32+
// such as a cross-link from the guides to the components, are skipped.
33+
const links = flattenPageLinks(items).filter((link) =>
34+
isInsideRoot(link.url, root),
35+
)
3136
const index = links.findIndex((link) => link.url === currentUrl)
3237
if (index < 0) return undefined
3338

@@ -40,6 +45,12 @@ export function resolvePageLinks(
4045
}
4146
}
4247

48+
function isInsideRoot(url: string, root: string) {
49+
if (!root) return true
50+
const rootUrl = `/${root}/`
51+
return url === rootUrl || url.startsWith(rootUrl)
52+
}
53+
4354
function isPageLinksEnabled(tree: NavigationTree, folder: string) {
4455
let current = folder
4556
while (true) {

0 commit comments

Comments
 (0)