Skip to content

Commit d87a67d

Browse files
authored
Merge pull request #4344 from kforris/fix/4336-menu-category-mapping
fix(notion): 修复菜单跳转 Category Mapping Page 404
2 parents f402ef3 + 3177661 commit d87a67d

3 files changed

Lines changed: 217 additions & 28 deletions

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/**
2+
* @jest-environment node
3+
*/
4+
5+
import {
6+
getCustomMenu,
7+
getSourcePageSlugs
8+
} from '@/lib/db/notion/getCustomMenu'
9+
10+
describe('getCustomMenu', () => {
11+
it('uses the generated page href when a menu targets the page source slug', () => {
12+
const collectionData = [
13+
{
14+
id: 'page-pending',
15+
type: 'Page',
16+
status: 'Published',
17+
slug: 'pending'
18+
},
19+
{
20+
id: 'page-contact',
21+
type: 'Page',
22+
status: 'Published',
23+
slug: 'contact'
24+
},
25+
{
26+
id: 'menu-pending',
27+
type: 'Menu',
28+
status: 'Published',
29+
title: 'Pending',
30+
slug: 'pending',
31+
href: '/pending'
32+
},
33+
{
34+
id: 'menu-more',
35+
type: 'Menu',
36+
status: 'Published',
37+
title: 'More',
38+
slug: '#',
39+
href: '#'
40+
},
41+
{
42+
id: 'submenu-contact',
43+
type: 'SubMenu',
44+
status: 'Published',
45+
title: 'Contact',
46+
slug: '/contact',
47+
href: '/contact'
48+
}
49+
]
50+
const sourcePageSlugs = getSourcePageSlugs(collectionData)
51+
collectionData[0].slug = 'pending/2026/07/29/pending'
52+
collectionData[0].href = '/pending/2026/07/29/pending'
53+
collectionData[1].slug = 'contact/2026/07/29/contact'
54+
collectionData[1].href = '/contact/2026/07/29/contact.html'
55+
56+
const menus = getCustomMenu({ collectionData, sourcePageSlugs })
57+
58+
expect(menus[0].href).toBe('/pending/2026/07/29/pending')
59+
expect(menus[1].href).toBe('#')
60+
expect(menus[1].subMenus[0].href).toBe('/contact/2026/07/29/contact.html')
61+
})
62+
63+
it('keeps unmatched and external menu links unchanged', () => {
64+
const collectionData = [
65+
{
66+
id: 'menu-archive',
67+
type: 'Menu',
68+
status: 'Published',
69+
title: 'Archive',
70+
slug: '/archive',
71+
href: '/archive'
72+
},
73+
{
74+
id: 'menu-external',
75+
type: 'Menu',
76+
status: 'Published',
77+
title: 'External',
78+
slug: 'https://example.com',
79+
href: 'https://example.com'
80+
}
81+
]
82+
83+
const menus = getCustomMenu({
84+
collectionData,
85+
sourcePageSlugs: new Map()
86+
})
87+
88+
expect(menus.map(menu => menu.href)).toEqual([
89+
'/archive',
90+
'https://example.com'
91+
])
92+
})
93+
94+
it('does not guess when multiple pages shared the same source slug', () => {
95+
const collectionData = [
96+
{
97+
id: 'page-one',
98+
type: 'Page',
99+
status: 'Published',
100+
slug: 'guide'
101+
},
102+
{
103+
id: 'page-two',
104+
type: 'Page',
105+
status: 'Published',
106+
slug: 'guide'
107+
},
108+
{
109+
id: 'menu-guide',
110+
type: 'Menu',
111+
status: 'Published',
112+
title: 'Guide',
113+
slug: 'guide',
114+
href: '/guide'
115+
}
116+
]
117+
const sourcePageSlugs = getSourcePageSlugs(collectionData)
118+
collectionData[0].href = '/manual/guide'
119+
collectionData[1].href = '/docs/guide'
120+
121+
const menus = getCustomMenu({ collectionData, sourcePageSlugs })
122+
123+
expect(menus[0].href).toBe('/guide')
124+
})
125+
})

lib/db/SiteDataApi.js

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import { getConfigMapFromConfigPage } from '@/lib/db/notion/getNotionConfig'
77
import getPageProperties, {
88
adjustPageProperties
99
} from '@/lib/db/notion/getPageProperties'
10+
import {
11+
getCustomMenu,
12+
getSourcePageSlugs
13+
} from '@/lib/db/notion/getCustomMenu'
1014
import {
1115
fetchInBatches,
1216
fetchNotionPageBlocks,
@@ -569,6 +573,7 @@ async function convertNotionToSiteData(
569573
})
570574
)
571575
}
576+
const sourcePageSlugs = getSourcePageSlugs(collectionData)
572577
collectionData.forEach(element => adjustPageProperties(element, NOTION_CONFIG))
573578

574579
const officialMembers = await fetchMembersFromOfficialAPI({
@@ -622,7 +627,7 @@ async function convertNotionToSiteData(
622627
const tagSchemaOptions = getTagOptions(schema)
623628
const tagOptions = getAllTags({ allPages, tagOptions: tagSchemaOptions ?? [], NOTION_CONFIG }) ?? null
624629
const customNav = getCustomNav({ allPages: collectionData.filter(post => post?.type === 'Page' && post.status === 'Published') })
625-
const customMenu = getCustomMenu({ collectionData, NOTION_CONFIG })
630+
const customMenu = getCustomMenu({ collectionData, sourcePageSlugs })
626631
const latestPosts = getLatestPosts({
627632
allPages,
628633
from,
@@ -834,33 +839,6 @@ function getCustomNav({ allPages }) {
834839
return customNav
835840
}
836841

837-
function getCustomMenu({ collectionData, NOTION_CONFIG }) {
838-
const menuPages = collectionData.filter(
839-
post =>
840-
post.status === 'Published' &&
841-
(post?.type === 'Menu' || post?.type === 'SubMenu')
842-
)
843-
const menus = []
844-
if (menuPages && menuPages.length > 0) {
845-
menuPages.forEach(e => {
846-
e.show = true
847-
if (e.type === 'Menu') {
848-
menus.push(e)
849-
} else if (e.type === 'SubMenu') {
850-
const parentMenu = menus[menus.length - 1]
851-
if (parentMenu) {
852-
if (parentMenu.subMenus) {
853-
parentMenu.subMenus.push(e)
854-
} else {
855-
parentMenu.subMenus = [e]
856-
}
857-
}
858-
}
859-
})
860-
}
861-
return menus
862-
}
863-
864842
function getTagOptions(schema) {
865843
if (!schema) return {}
866844
const tagSchema = Object.values(schema).find(

lib/db/notion/getCustomMenu.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
function normalizeSourceSlug(slug) {
2+
if (typeof slug !== 'string') {
3+
return ''
4+
}
5+
6+
const normalized = slug.trim()
7+
if (
8+
!normalized ||
9+
normalized === '#' ||
10+
/^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(normalized)
11+
) {
12+
return ''
13+
}
14+
15+
return normalized.replace(/^\/+|\/+$/g, '')
16+
}
17+
18+
export function getSourcePageSlugs(collectionData) {
19+
return new Map(
20+
collectionData
21+
.filter(page => page?.type === 'Page' && page?.slug)
22+
.map(page => [page.id, page.slug])
23+
)
24+
}
25+
26+
function getPageHrefBySourceSlug(collectionData, sourcePageSlugs) {
27+
const pageHrefBySourceSlug = new Map()
28+
const ambiguousSlugs = new Set()
29+
30+
collectionData.forEach(page => {
31+
if (page?.type !== 'Page' || page?.status !== 'Published' || !page?.href) {
32+
return
33+
}
34+
35+
const sourceSlug = normalizeSourceSlug(sourcePageSlugs?.get(page.id))
36+
if (!sourceSlug || ambiguousSlugs.has(sourceSlug)) {
37+
return
38+
}
39+
40+
const existingHref = pageHrefBySourceSlug.get(sourceSlug)
41+
if (existingHref && existingHref !== page.href) {
42+
pageHrefBySourceSlug.delete(sourceSlug)
43+
ambiguousSlugs.add(sourceSlug)
44+
return
45+
}
46+
47+
pageHrefBySourceSlug.set(sourceSlug, page.href)
48+
})
49+
50+
return pageHrefBySourceSlug
51+
}
52+
53+
export function getCustomMenu({ collectionData, sourcePageSlugs }) {
54+
const pageHrefBySourceSlug = getPageHrefBySourceSlug(
55+
collectionData,
56+
sourcePageSlugs
57+
)
58+
const menuPages = collectionData.filter(
59+
post =>
60+
post.status === 'Published' &&
61+
(post?.type === 'Menu' || post?.type === 'SubMenu')
62+
)
63+
const menus = []
64+
if (menuPages && menuPages.length > 0) {
65+
menuPages.forEach(e => {
66+
e.show = true
67+
const sourceSlug = normalizeSourceSlug(e.slug)
68+
if (sourceSlug && pageHrefBySourceSlug.has(sourceSlug)) {
69+
e.href = pageHrefBySourceSlug.get(sourceSlug)
70+
}
71+
if (e.type === 'Menu') {
72+
menus.push(e)
73+
} else if (e.type === 'SubMenu') {
74+
const parentMenu = menus[menus.length - 1]
75+
if (parentMenu) {
76+
if (parentMenu.subMenus) {
77+
parentMenu.subMenus.push(e)
78+
} else {
79+
parentMenu.subMenus = [e]
80+
}
81+
}
82+
}
83+
})
84+
}
85+
return menus
86+
}

0 commit comments

Comments
 (0)