diff --git a/.changeset/little-students-care.md b/.changeset/little-students-care.md new file mode 100644 index 00000000000..de0a0f0e6fe --- /dev/null +++ b/.changeset/little-students-care.md @@ -0,0 +1,6 @@ +--- +'nextra-theme-docs': minor +'nextra': minor +--- + +add `frontmatter.sidebar_label` support for setting page label in sidebar via frontmatter diff --git a/packages/nextra/__test__/__snapshots__/page-map.test.ts.snap b/packages/nextra/__test__/__snapshots__/page-map.test.ts.snap index 416a76fbd69..3724d1a0bc6 100644 --- a/packages/nextra/__test__/__snapshots__/page-map.test.ts.snap +++ b/packages/nextra/__test__/__snapshots__/page-map.test.ts.snap @@ -83,18 +83,30 @@ exports[`Page Process > should match i18n site page maps 1`] = ` }, }, { + "frontMatter": { + "sidebar_label": "A Page", + }, "name": "a-page", "route": "/en/about/a-page", }, { + "frontMatter": { + "sidebar_label": "Acknowledgement", + }, "name": "acknowledgement", "route": "/en/about/acknowledgement", }, { + "frontMatter": { + "sidebar_label": "Changelog", + }, "name": "changelog", "route": "/en/about/changelog", }, { + "frontMatter": { + "sidebar_label": "Team", + }, "name": "team", "route": "/en/about/team", }, @@ -118,6 +130,7 @@ exports[`Page Process > should match i18n site page maps 1`] = ` "frontMatter": { "description": "Almost 2 years ago we open sourced SWR, the tiny data-fetching React library that people love. Today we are reaching another milestone: the 1.0 version of SWR.", "image": "https://assets.vercel.com/image/upload/v1630059453/swr/v1.png", + "sidebar_label": "SWR V1", }, "name": "swr-v1", "route": "/en/blog/swr-v1", @@ -129,6 +142,7 @@ exports[`Page Process > should match i18n site page maps 1`] = ` { "frontMatter": { "searchable": false, + "sidebar_label": "Blog", }, "name": "blog", "route": "/en/blog", @@ -184,6 +198,9 @@ exports[`Page Process > should match i18n site page maps 1`] = ` }, }, { + "frontMatter": { + "sidebar_label": "404 500", + }, "name": "404-500", "route": "/en/docs/404-500", }, @@ -205,26 +222,44 @@ exports[`Page Process > should match i18n site page maps 1`] = ` }, }, { + "frontMatter": { + "sidebar_label": "Cache", + }, "name": "cache", "route": "/en/docs/advanced/cache", }, { + "frontMatter": { + "sidebar_label": "Code Highlighting", + }, "name": "code-highlighting", "route": "/en/docs/advanced/code-highlighting", }, { + "frontMatter": { + "sidebar_label": "Dynamic Markdown Import", + }, "name": "dynamic-markdown-import", "route": "/en/docs/advanced/dynamic-markdown-import", }, { + "frontMatter": { + "sidebar_label": "File Name.with.dots", + }, "name": "file-name.with.DOTS", "route": "/en/docs/advanced/file-name.with.DOTS", }, { + "frontMatter": { + "sidebar_label": "Images", + }, "name": "images", "route": "/en/docs/advanced/images", }, { + "frontMatter": { + "sidebar_label": "Markdown Import", + }, "name": "markdown-import", "route": "/en/docs/advanced/markdown-import", }, @@ -237,6 +272,9 @@ exports[`Page Process > should match i18n site page maps 1`] = ` }, }, { + "frontMatter": { + "sidebar_label": "Loooooooooooooooooooong Title", + }, "name": "loooooooooooooooooooong-title", "route": "/en/docs/advanced/more/loooooooooooooooooooong-title", }, @@ -250,14 +288,23 @@ exports[`Page Process > should match i18n site page maps 1`] = ` }, }, { + "frontMatter": { + "sidebar_label": "One", + }, "name": "one", "route": "/en/docs/advanced/more/tree/one", }, { + "frontMatter": { + "sidebar_label": "Three", + }, "name": "three", "route": "/en/docs/advanced/more/tree/three", }, { + "frontMatter": { + "sidebar_label": "Two", + }, "name": "two", "route": "/en/docs/advanced/more/tree/two", }, @@ -270,14 +317,23 @@ exports[`Page Process > should match i18n site page maps 1`] = ` "route": "/en/docs/advanced/more", }, { + "frontMatter": { + "sidebar_label": "Performance", + }, "name": "performance", "route": "/en/docs/advanced/performance", }, { + "frontMatter": { + "sidebar_label": "React Native", + }, "name": "react-native", "route": "/en/docs/advanced/react-native", }, { + "frontMatter": { + "sidebar_label": "Scrollbar X", + }, "name": "scrollbar-x", "route": "/en/docs/advanced/scrollbar-x", }, @@ -286,94 +342,163 @@ exports[`Page Process > should match i18n site page maps 1`] = ` "route": "/en/docs/advanced", }, { + "frontMatter": { + "sidebar_label": "Advanced", + }, "name": "advanced", "route": "/en/docs/advanced", }, { + "frontMatter": { + "sidebar_label": "Arguments", + }, "name": "arguments", "route": "/en/docs/arguments", }, { + "frontMatter": { + "sidebar_label": "Callout", + }, "name": "callout", "route": "/en/docs/callout", }, { + "frontMatter": { + "sidebar_label": "Change Log", + }, "name": "change-log", "route": "/en/docs/change-log", }, { + "frontMatter": { + "sidebar_label": "Code Block without Language", + }, "name": "code-block-without-language", "route": "/en/docs/code-block-without-language", }, { + "frontMatter": { + "sidebar_label": "Conditional Fetching", + }, "name": "conditional-fetching", "route": "/en/docs/conditional-fetching", }, { + "frontMatter": { + "sidebar_label": "Custom Header Ids", + }, "name": "custom-header-ids", "route": "/en/docs/custom-header-ids", }, { + "frontMatter": { + "sidebar_label": "Data Fetching", + }, "name": "data-fetching", "route": "/en/docs/data-fetching", }, { + "frontMatter": { + "sidebar_label": "Error Handling", + }, "name": "error-handling", "route": "/en/docs/error-handling", }, { + "frontMatter": { + "sidebar_label": "Getting Started", + }, "name": "getting-started", "route": "/en/docs/getting-started", }, { + "frontMatter": { + "sidebar_label": "Global Configuration", + }, "name": "global-configuration", "route": "/en/docs/global-configuration", }, { + "frontMatter": { + "sidebar_label": "Middleware", + }, "name": "middleware", "route": "/en/docs/middleware", }, { + "frontMatter": { + "sidebar_label": "Mutation", + }, "name": "mutation", "route": "/en/docs/mutation", }, { + "frontMatter": { + "sidebar_label": "Options", + }, "name": "options", "route": "/en/docs/options", }, { + "frontMatter": { + "sidebar_label": "Pagination", + }, "name": "pagination", "route": "/en/docs/pagination", }, { + "frontMatter": { + "sidebar_label": "Prefetching", + }, "name": "prefetching", "route": "/en/docs/prefetching", }, { + "frontMatter": { + "sidebar_label": "Raw Layout", + }, "name": "raw-layout", "route": "/en/docs/raw-layout", }, { + "frontMatter": { + "sidebar_label": "Revalidation", + }, "name": "revalidation", "route": "/en/docs/revalidation", }, { + "frontMatter": { + "sidebar_label": "Suspense", + }, "name": "suspense", "route": "/en/docs/suspense", }, { + "frontMatter": { + "sidebar_label": "TypeScript", + }, "name": "typescript", "route": "/en/docs/typescript", }, { + "frontMatter": { + "sidebar_label": "Understanding", + }, "name": "understanding", "route": "/en/docs/understanding", }, { + "frontMatter": { + "sidebar_label": "With Nextjs", + }, "name": "with-nextjs", "route": "/en/docs/with-nextjs", }, { + "frontMatter": { + "sidebar_label": "Wrap Toc Items", + }, "name": "wrap-toc-items", "route": "/en/docs/wrap-toc-items", }, @@ -400,6 +525,7 @@ exports[`Page Process > should match i18n site page maps 1`] = ` { "frontMatter": { "full": true, + "sidebar_label": "Auth", "title": "Authentication", }, "name": "auth", @@ -408,6 +534,7 @@ exports[`Page Process > should match i18n site page maps 1`] = ` { "frontMatter": { "full": true, + "sidebar_label": "Basic", "title": "Basic Usage", }, "name": "basic", @@ -416,18 +543,23 @@ exports[`Page Process > should match i18n site page maps 1`] = ` { "frontMatter": { "full": true, + "sidebar_label": "Error Handling", "title": "Error Handling", }, "name": "error-handling", "route": "/en/examples/error-handling", }, { + "frontMatter": { + "sidebar_label": "Full", + }, "name": "full", "route": "/en/examples/full", }, { "frontMatter": { "full": true, + "sidebar_label": "Infinite Loading", "title": "Infinite Loading", }, "name": "infinite-loading", @@ -436,6 +568,7 @@ exports[`Page Process > should match i18n site page maps 1`] = ` { "frontMatter": { "full": true, + "sidebar_label": "SSR", "title": "Next.js SSR", }, "name": "ssr", @@ -446,12 +579,16 @@ exports[`Page Process > should match i18n site page maps 1`] = ` "route": "/en/examples", }, { + "frontMatter": { + "sidebar_label": "Foo", + }, "name": "foo", "route": "/en/foo", }, { "frontMatter": { "searchable": false, + "sidebar_label": "Index", "title": "React Hooks for Data Fetching", }, "name": "index", @@ -488,6 +625,9 @@ exports[`Page Process > should match i18n site page maps 1`] = ` "route": "/en/remote", }, { + "frontMatter": { + "sidebar_label": "Test", + }, "name": "test", "route": "/en/test", }, @@ -549,10 +689,16 @@ exports[`Page Process > should match i18n site page maps 1`] = ` }, }, { + "frontMatter": { + "sidebar_label": "File Name.with.dots", + }, "name": "file-name.with.DOTS", "route": "/es/docs/advanced/file-name.with.DOTS", }, { + "frontMatter": { + "sidebar_label": "Performance", + }, "name": "performance", "route": "/es/docs/advanced/performance", }, @@ -561,66 +707,114 @@ exports[`Page Process > should match i18n site page maps 1`] = ` "route": "/es/docs/advanced", }, { + "frontMatter": { + "sidebar_label": "Arguments", + }, "name": "arguments", "route": "/es/docs/arguments", }, { + "frontMatter": { + "sidebar_label": "Change Log", + }, "name": "change-log", "route": "/es/docs/change-log", }, { + "frontMatter": { + "sidebar_label": "Conditional Fetching", + }, "name": "conditional-fetching", "route": "/es/docs/conditional-fetching", }, { + "frontMatter": { + "sidebar_label": "Data Fetching", + }, "name": "data-fetching", "route": "/es/docs/data-fetching", }, { + "frontMatter": { + "sidebar_label": "Error Handling", + }, "name": "error-handling", "route": "/es/docs/error-handling", }, { + "frontMatter": { + "sidebar_label": "Getting Started", + }, "name": "getting-started", "route": "/es/docs/getting-started", }, { + "frontMatter": { + "sidebar_label": "Global Configuration", + }, "name": "global-configuration", "route": "/es/docs/global-configuration", }, { + "frontMatter": { + "sidebar_label": "Mutation", + }, "name": "mutation", "route": "/es/docs/mutation", }, { + "frontMatter": { + "sidebar_label": "Options", + }, "name": "options", "route": "/es/docs/options", }, { + "frontMatter": { + "sidebar_label": "Pagination", + }, "name": "pagination", "route": "/es/docs/pagination", }, { + "frontMatter": { + "sidebar_label": "Prefetching", + }, "name": "prefetching", "route": "/es/docs/prefetching", }, { + "frontMatter": { + "sidebar_label": "Revalidation", + }, "name": "revalidation", "route": "/es/docs/revalidation", }, { + "frontMatter": { + "sidebar_label": "Suspense", + }, "name": "suspense", "route": "/es/docs/suspense", }, { + "frontMatter": { + "sidebar_label": "Understanding", + }, "name": "understanding", "route": "/es/docs/understanding", }, { + "frontMatter": { + "sidebar_label": "With Nextjs", + }, "name": "with-nextjs", "route": "/es/docs/with-nextjs", }, { + "frontMatter": { + "sidebar_label": "Wrap Toc Items", + }, "name": "wrap-toc-items", "route": "/es/docs/wrap-toc-items", }, @@ -642,6 +836,7 @@ exports[`Page Process > should match i18n site page maps 1`] = ` { "frontMatter": { "full": true, + "sidebar_label": "Auth", "title": "Autenticación", }, "name": "auth", @@ -650,6 +845,7 @@ exports[`Page Process > should match i18n site page maps 1`] = ` { "frontMatter": { "full": true, + "sidebar_label": "Basic", "title": "Uso Básico", }, "name": "basic", @@ -658,6 +854,7 @@ exports[`Page Process > should match i18n site page maps 1`] = ` { "frontMatter": { "full": true, + "sidebar_label": "Error Handling", "title": "Manejo De Errores", }, "name": "error-handling", @@ -666,6 +863,7 @@ exports[`Page Process > should match i18n site page maps 1`] = ` { "frontMatter": { "full": true, + "sidebar_label": "Infinite Loading", "title": "Carga Infinita", }, "name": "infinite-loading", @@ -677,6 +875,7 @@ exports[`Page Process > should match i18n site page maps 1`] = ` }, { "frontMatter": { + "sidebar_label": "Index", "title": "Biblioteca React Hooks para la obtención de datos", }, "name": "index", @@ -728,6 +927,7 @@ exports[`Page Process > should match i18n site page maps 1`] = ` "frontMatter": { "description": "Почти 2 года назад мы сделали SWR — крошечную React библиотеку с открытым исходным кодом для выборки данных, которую люди полюбили. Сегодня мы приближаемся к еще одной вехе: версии 1.0 SWR!", "image": "https://assets.vercel.com/image/upload/v1630059453/swr/v1.png", + "sidebar_label": "SWR V1", }, "name": "swr-v1", "route": "/ru/blog/swr-v1", @@ -737,6 +937,9 @@ exports[`Page Process > should match i18n site page maps 1`] = ` "route": "/ru/blog", }, { + "frontMatter": { + "sidebar_label": "Blog", + }, "name": "blog", "route": "/ru/blog", }, @@ -775,18 +978,30 @@ exports[`Page Process > should match i18n site page maps 1`] = ` }, }, { + "frontMatter": { + "sidebar_label": "Cache", + }, "name": "cache", "route": "/ru/docs/advanced/cache", }, { + "frontMatter": { + "sidebar_label": "File Name.with.dots", + }, "name": "file-name.with.DOTS", "route": "/ru/docs/advanced/file-name.with.DOTS", }, { + "frontMatter": { + "sidebar_label": "Performance", + }, "name": "performance", "route": "/ru/docs/advanced/performance", }, { + "frontMatter": { + "sidebar_label": "React Native", + }, "name": "react-native", "route": "/ru/docs/advanced/react-native", }, @@ -795,70 +1010,121 @@ exports[`Page Process > should match i18n site page maps 1`] = ` "route": "/ru/docs/advanced", }, { + "frontMatter": { + "sidebar_label": "Arguments", + }, "name": "arguments", "route": "/ru/docs/arguments", }, { + "frontMatter": { + "sidebar_label": "Change Log", + }, "name": "change-log", "route": "/ru/docs/change-log", }, { + "frontMatter": { + "sidebar_label": "Conditional Fetching", + }, "name": "conditional-fetching", "route": "/ru/docs/conditional-fetching", }, { + "frontMatter": { + "sidebar_label": "Data Fetching", + }, "name": "data-fetching", "route": "/ru/docs/data-fetching", }, { + "frontMatter": { + "sidebar_label": "Error Handling", + }, "name": "error-handling", "route": "/ru/docs/error-handling", }, { + "frontMatter": { + "sidebar_label": "Getting Started", + }, "name": "getting-started", "route": "/ru/docs/getting-started", }, { + "frontMatter": { + "sidebar_label": "Global Configuration", + }, "name": "global-configuration", "route": "/ru/docs/global-configuration", }, { + "frontMatter": { + "sidebar_label": "Middleware", + }, "name": "middleware", "route": "/ru/docs/middleware", }, { + "frontMatter": { + "sidebar_label": "Mutation", + }, "name": "mutation", "route": "/ru/docs/mutation", }, { + "frontMatter": { + "sidebar_label": "Options", + }, "name": "options", "route": "/ru/docs/options", }, { + "frontMatter": { + "sidebar_label": "Pagination", + }, "name": "pagination", "route": "/ru/docs/pagination", }, { + "frontMatter": { + "sidebar_label": "Prefetching", + }, "name": "prefetching", "route": "/ru/docs/prefetching", }, { + "frontMatter": { + "sidebar_label": "Revalidation", + }, "name": "revalidation", "route": "/ru/docs/revalidation", }, { + "frontMatter": { + "sidebar_label": "Suspense", + }, "name": "suspense", "route": "/ru/docs/suspense", }, { + "frontMatter": { + "sidebar_label": "Understanding", + }, "name": "understanding", "route": "/ru/docs/understanding", }, { + "frontMatter": { + "sidebar_label": "With Nextjs", + }, "name": "with-nextjs", "route": "/ru/docs/with-nextjs", }, { + "frontMatter": { + "sidebar_label": "Wrap Toc Items", + }, "name": "wrap-toc-items", "route": "/ru/docs/wrap-toc-items", }, @@ -880,6 +1146,7 @@ exports[`Page Process > should match i18n site page maps 1`] = ` { "frontMatter": { "full": true, + "sidebar_label": "Auth", "title": "Аутентификация", }, "name": "auth", @@ -888,6 +1155,7 @@ exports[`Page Process > should match i18n site page maps 1`] = ` { "frontMatter": { "full": true, + "sidebar_label": "Basic", "title": "Основное использование", }, "name": "basic", @@ -896,6 +1164,7 @@ exports[`Page Process > should match i18n site page maps 1`] = ` { "frontMatter": { "full": true, + "sidebar_label": "Error Handling", "title": "Обработка ошибок", }, "name": "error-handling", @@ -904,6 +1173,7 @@ exports[`Page Process > should match i18n site page maps 1`] = ` { "frontMatter": { "full": true, + "sidebar_label": "Infinite Loading", "title": "Бесконечная загрузка", }, "name": "infinite-loading", @@ -912,6 +1182,7 @@ exports[`Page Process > should match i18n site page maps 1`] = ` { "frontMatter": { "full": true, + "sidebar_label": "SSR", "title": "Next.js SSR", }, "name": "ssr", @@ -923,6 +1194,7 @@ exports[`Page Process > should match i18n site page maps 1`] = ` }, { "frontMatter": { + "sidebar_label": "Index", "title": "React хуки для получения данных", }, "name": "index", diff --git a/packages/nextra/__test__/collect-catch-all.test.ts b/packages/nextra/__test__/collect-catch-all.test.ts index e4e602b2a1b..a9a64c40537 100644 --- a/packages/nextra/__test__/collect-catch-all.test.ts +++ b/packages/nextra/__test__/collect-catch-all.test.ts @@ -4,8 +4,6 @@ import { collectCatchAllRoutes } from '../src/client/setup-page.js' describe('collectCatchAllRoutes', () => { it('should collect', () => { const meta = { - kind: 'Meta' as const, - locale: 'en-US', data: createCatchAllMeta([ 'configs.md', 'custom-rules.md', @@ -17,14 +15,9 @@ describe('collectCatchAllRoutes', () => { ]) } const parent = { - kind: 'Folder' as const, name: 'nested', route: '/remote/nested', - children: [ - meta, - { kind: 'Meta', locale: 'es-ES', data: {} }, - { kind: 'Meta', locale: 'ru', data: {} } - ] + children: [meta] } collectCatchAllRoutes(parent, meta) expect(parent).toMatchInlineSnapshot(` @@ -37,18 +30,6 @@ describe('collectCatchAllRoutes', () => { "getting-started": "Getting Started", "index": "Index", }, - "kind": "Meta", - "locale": "en-US", - }, - { - "data": {}, - "kind": "Meta", - "locale": "es-ES", - }, - { - "data": {}, - "kind": "Meta", - "locale": "ru", }, { "name": "configs", @@ -103,7 +84,6 @@ describe('collectCatchAllRoutes', () => { "route": "/remote/nested", }, ], - "kind": "Folder", "name": "nested", "route": "/remote/nested", } @@ -112,7 +92,6 @@ describe('collectCatchAllRoutes', () => { it('should not create MdxPage for "*" key', () => { const meta = { - kind: 'Meta' as const, data: createCatchAllMeta([], { '*': { type: 'page', @@ -125,7 +104,6 @@ describe('collectCatchAllRoutes', () => { }) } const parent = { - kind: 'Folder' as const, name: 'nested', route: '/remote/nested', children: [meta] @@ -145,10 +123,8 @@ describe('collectCatchAllRoutes', () => { "type": "page", }, }, - "kind": "Meta", }, ], - "kind": "Folder", "name": "nested", "route": "/remote/nested", } diff --git a/packages/nextra/__test__/normalize-page.spec.ts b/packages/nextra/__test__/normalize-page.spec.ts index 57eeed0052e..caddedb4203 100644 --- a/packages/nextra/__test__/normalize-page.spec.ts +++ b/packages/nextra/__test__/normalize-page.spec.ts @@ -76,12 +76,8 @@ describe('normalize-page', () => { layout: 'raw' } }, - index: { - title: 'Introduction' - }, - 'get-started': { - title: 'Get Started' - } + index: 'Introduction', + 'get-started': 'Get Started' } } ], diff --git a/packages/nextra/__test__/page-map.test.ts b/packages/nextra/__test__/page-map.test.ts index a76ef78c019..d24b370cb8a 100644 --- a/packages/nextra/__test__/page-map.test.ts +++ b/packages/nextra/__test__/page-map.test.ts @@ -40,16 +40,28 @@ describe('collectPageMap', () => { data: examples_swr_site_pages_en_about_meta }, { name: \\"a-page\\", - route: \\"/en/about/a-page\\" + route: \\"/en/about/a-page\\", + frontMatter: { + \\"sidebar_label\\": \\"A Page\\" + } }, { name: \\"acknowledgement\\", - route: \\"/en/about/acknowledgement\\" + route: \\"/en/about/acknowledgement\\", + frontMatter: { + \\"sidebar_label\\": \\"Acknowledgement\\" + } }, { name: \\"changelog\\", - route: \\"/en/about/changelog\\" + route: \\"/en/about/changelog\\", + frontMatter: { + \\"sidebar_label\\": \\"Changelog\\" + } }, { name: \\"team\\", - route: \\"/en/about/team\\" + route: \\"/en/about/team\\", + frontMatter: { + \\"sidebar_label\\": \\"Team\\" + } }] }, { name: \\"blog\\", @@ -60,6 +72,7 @@ describe('collectPageMap', () => { name: \\"swr-v1\\", route: \\"/en/blog/swr-v1\\", frontMatter: { + \\"sidebar_label\\": \\"SWR V1\\", \\"image\\": \\"https://assets.vercel.com/image/upload/v1630059453/swr/v1.png\\", \\"description\\": \\"Almost 2 years ago we open sourced SWR, the tiny data-fetching React library that people love. Today we are reaching another milestone: the 1.0 version of SWR.\\" } @@ -68,6 +81,7 @@ describe('collectPageMap', () => { name: \\"blog\\", route: \\"/en/blog\\", frontMatter: { + \\"sidebar_label\\": \\"Blog\\", \\"searchable\\": false } }, { @@ -77,7 +91,10 @@ describe('collectPageMap', () => { data: examples_swr_site_pages_en_docs_meta }, { name: \\"404-500\\", - route: \\"/en/docs/404-500\\" + route: \\"/en/docs/404-500\\", + frontMatter: { + \\"sidebar_label\\": \\"404 500\\" + } }, { name: \\"advanced\\", route: \\"/en/docs/advanced\\", @@ -85,22 +102,40 @@ describe('collectPageMap', () => { data: examples_swr_site_pages_en_docs_advanced_meta }, { name: \\"cache\\", - route: \\"/en/docs/advanced/cache\\" + route: \\"/en/docs/advanced/cache\\", + frontMatter: { + \\"sidebar_label\\": \\"Cache\\" + } }, { name: \\"code-highlighting\\", - route: \\"/en/docs/advanced/code-highlighting\\" + route: \\"/en/docs/advanced/code-highlighting\\", + frontMatter: { + \\"sidebar_label\\": \\"Code Highlighting\\" + } }, { name: \\"dynamic-markdown-import\\", - route: \\"/en/docs/advanced/dynamic-markdown-import\\" + route: \\"/en/docs/advanced/dynamic-markdown-import\\", + frontMatter: { + \\"sidebar_label\\": \\"Dynamic Markdown Import\\" + } }, { name: \\"file-name.with.DOTS\\", - route: \\"/en/docs/advanced/file-name.with.DOTS\\" + route: \\"/en/docs/advanced/file-name.with.DOTS\\", + frontMatter: { + \\"sidebar_label\\": \\"File Name.with.dots\\" + } }, { name: \\"images\\", - route: \\"/en/docs/advanced/images\\" + route: \\"/en/docs/advanced/images\\", + frontMatter: { + \\"sidebar_label\\": \\"Images\\" + } }, { name: \\"markdown-import\\", - route: \\"/en/docs/advanced/markdown-import\\" + route: \\"/en/docs/advanced/markdown-import\\", + frontMatter: { + \\"sidebar_label\\": \\"Markdown Import\\" + } }, { name: \\"more\\", route: \\"/en/docs/advanced/more\\", @@ -111,7 +146,10 @@ describe('collectPageMap', () => { } }, { name: \\"loooooooooooooooooooong-title\\", - route: \\"/en/docs/advanced/more/loooooooooooooooooooong-title\\" + route: \\"/en/docs/advanced/more/loooooooooooooooooooong-title\\", + frontMatter: { + \\"sidebar_label\\": \\"Loooooooooooooooooooong Title\\" + } }, { name: \\"tree\\", route: \\"/en/docs/advanced/more/tree\\", @@ -123,94 +161,181 @@ describe('collectPageMap', () => { } }, { name: \\"one\\", - route: \\"/en/docs/advanced/more/tree/one\\" + route: \\"/en/docs/advanced/more/tree/one\\", + frontMatter: { + \\"sidebar_label\\": \\"One\\" + } }, { name: \\"three\\", - route: \\"/en/docs/advanced/more/tree/three\\" + route: \\"/en/docs/advanced/more/tree/three\\", + frontMatter: { + \\"sidebar_label\\": \\"Three\\" + } }, { name: \\"two\\", - route: \\"/en/docs/advanced/more/tree/two\\" + route: \\"/en/docs/advanced/more/tree/two\\", + frontMatter: { + \\"sidebar_label\\": \\"Two\\" + } }] }] }, { name: \\"performance\\", - route: \\"/en/docs/advanced/performance\\" + route: \\"/en/docs/advanced/performance\\", + frontMatter: { + \\"sidebar_label\\": \\"Performance\\" + } }, { name: \\"react-native\\", - route: \\"/en/docs/advanced/react-native\\" + route: \\"/en/docs/advanced/react-native\\", + frontMatter: { + \\"sidebar_label\\": \\"React Native\\" + } }, { name: \\"scrollbar-x\\", - route: \\"/en/docs/advanced/scrollbar-x\\" + route: \\"/en/docs/advanced/scrollbar-x\\", + frontMatter: { + \\"sidebar_label\\": \\"Scrollbar X\\" + } }] }, { name: \\"advanced\\", - route: \\"/en/docs/advanced\\" + route: \\"/en/docs/advanced\\", + frontMatter: { + \\"sidebar_label\\": \\"Advanced\\" + } }, { name: \\"arguments\\", - route: \\"/en/docs/arguments\\" + route: \\"/en/docs/arguments\\", + frontMatter: { + \\"sidebar_label\\": \\"Arguments\\" + } }, { name: \\"callout\\", - route: \\"/en/docs/callout\\" + route: \\"/en/docs/callout\\", + frontMatter: { + \\"sidebar_label\\": \\"Callout\\" + } }, { name: \\"change-log\\", - route: \\"/en/docs/change-log\\" + route: \\"/en/docs/change-log\\", + frontMatter: { + \\"sidebar_label\\": \\"Change Log\\" + } }, { name: \\"code-block-without-language\\", - route: \\"/en/docs/code-block-without-language\\" + route: \\"/en/docs/code-block-without-language\\", + frontMatter: { + \\"sidebar_label\\": \\"Code Block without Language\\" + } }, { name: \\"conditional-fetching\\", - route: \\"/en/docs/conditional-fetching\\" + route: \\"/en/docs/conditional-fetching\\", + frontMatter: { + \\"sidebar_label\\": \\"Conditional Fetching\\" + } }, { name: \\"custom-header-ids\\", - route: \\"/en/docs/custom-header-ids\\" + route: \\"/en/docs/custom-header-ids\\", + frontMatter: { + \\"sidebar_label\\": \\"Custom Header Ids\\" + } }, { name: \\"data-fetching\\", - route: \\"/en/docs/data-fetching\\" + route: \\"/en/docs/data-fetching\\", + frontMatter: { + \\"sidebar_label\\": \\"Data Fetching\\" + } }, { name: \\"error-handling\\", - route: \\"/en/docs/error-handling\\" + route: \\"/en/docs/error-handling\\", + frontMatter: { + \\"sidebar_label\\": \\"Error Handling\\" + } }, { name: \\"getting-started\\", - route: \\"/en/docs/getting-started\\" + route: \\"/en/docs/getting-started\\", + frontMatter: { + \\"sidebar_label\\": \\"Getting Started\\" + } }, { name: \\"global-configuration\\", - route: \\"/en/docs/global-configuration\\" + route: \\"/en/docs/global-configuration\\", + frontMatter: { + \\"sidebar_label\\": \\"Global Configuration\\" + } }, { name: \\"middleware\\", - route: \\"/en/docs/middleware\\" + route: \\"/en/docs/middleware\\", + frontMatter: { + \\"sidebar_label\\": \\"Middleware\\" + } }, { name: \\"mutation\\", - route: \\"/en/docs/mutation\\" + route: \\"/en/docs/mutation\\", + frontMatter: { + \\"sidebar_label\\": \\"Mutation\\" + } }, { name: \\"options\\", - route: \\"/en/docs/options\\" + route: \\"/en/docs/options\\", + frontMatter: { + \\"sidebar_label\\": \\"Options\\" + } }, { name: \\"pagination\\", - route: \\"/en/docs/pagination\\" + route: \\"/en/docs/pagination\\", + frontMatter: { + \\"sidebar_label\\": \\"Pagination\\" + } }, { name: \\"prefetching\\", - route: \\"/en/docs/prefetching\\" + route: \\"/en/docs/prefetching\\", + frontMatter: { + \\"sidebar_label\\": \\"Prefetching\\" + } }, { name: \\"raw-layout\\", - route: \\"/en/docs/raw-layout\\" + route: \\"/en/docs/raw-layout\\", + frontMatter: { + \\"sidebar_label\\": \\"Raw Layout\\" + } }, { name: \\"revalidation\\", - route: \\"/en/docs/revalidation\\" + route: \\"/en/docs/revalidation\\", + frontMatter: { + \\"sidebar_label\\": \\"Revalidation\\" + } }, { name: \\"suspense\\", - route: \\"/en/docs/suspense\\" + route: \\"/en/docs/suspense\\", + frontMatter: { + \\"sidebar_label\\": \\"Suspense\\" + } }, { name: \\"typescript\\", - route: \\"/en/docs/typescript\\" + route: \\"/en/docs/typescript\\", + frontMatter: { + \\"sidebar_label\\": \\"TypeScript\\" + } }, { name: \\"understanding\\", - route: \\"/en/docs/understanding\\" + route: \\"/en/docs/understanding\\", + frontMatter: { + \\"sidebar_label\\": \\"Understanding\\" + } }, { name: \\"with-nextjs\\", - route: \\"/en/docs/with-nextjs\\" + route: \\"/en/docs/with-nextjs\\", + frontMatter: { + \\"sidebar_label\\": \\"With Nextjs\\" + } }, { name: \\"wrap-toc-items\\", - route: \\"/en/docs/wrap-toc-items\\" + route: \\"/en/docs/wrap-toc-items\\", + frontMatter: { + \\"sidebar_label\\": \\"Wrap Toc Items\\" + } }] }, { name: \\"examples\\", @@ -221,6 +346,7 @@ describe('collectPageMap', () => { name: \\"auth\\", route: \\"/en/examples/auth\\", frontMatter: { + \\"sidebar_label\\": \\"Auth\\", \\"title\\": \\"Authentication\\", \\"full\\": true } @@ -228,6 +354,7 @@ describe('collectPageMap', () => { name: \\"basic\\", route: \\"/en/examples/basic\\", frontMatter: { + \\"sidebar_label\\": \\"Basic\\", \\"title\\": \\"Basic Usage\\", \\"full\\": true } @@ -235,16 +362,21 @@ describe('collectPageMap', () => { name: \\"error-handling\\", route: \\"/en/examples/error-handling\\", frontMatter: { + \\"sidebar_label\\": \\"Error Handling\\", \\"title\\": \\"Error Handling\\", \\"full\\": true } }, { name: \\"full\\", - route: \\"/en/examples/full\\" + route: \\"/en/examples/full\\", + frontMatter: { + \\"sidebar_label\\": \\"Full\\" + } }, { name: \\"infinite-loading\\", route: \\"/en/examples/infinite-loading\\", frontMatter: { + \\"sidebar_label\\": \\"Infinite Loading\\", \\"title\\": \\"Infinite Loading\\", \\"full\\": true } @@ -252,17 +384,22 @@ describe('collectPageMap', () => { name: \\"ssr\\", route: \\"/en/examples/ssr\\", frontMatter: { + \\"sidebar_label\\": \\"SSR\\", \\"title\\": \\"Next.js SSR\\", \\"full\\": true } }] }, { name: \\"foo\\", - route: \\"/en/foo\\" + route: \\"/en/foo\\", + frontMatter: { + \\"sidebar_label\\": \\"Foo\\" + } }, { name: \\"index\\", route: \\"/en\\", frontMatter: { + \\"sidebar_label\\": \\"Index\\", \\"title\\": \\"React Hooks for Data Fetching\\", \\"searchable\\": false } @@ -289,7 +426,10 @@ describe('collectPageMap', () => { }] }, { name: \\"test\\", - route: \\"/en/test\\" + route: \\"/en/test\\", + frontMatter: { + \\"sidebar_label\\": \\"Test\\" + } }]; export const dynamicMetaModules = { \\"/en/remote/graphql-eslint\\": examples_swr_site_pages_en_remote_graphql_eslint_meta, @@ -334,10 +474,16 @@ describe('Page Process', () => { } }, { name: \\"callout\\", - route: \\"/callout\\" + route: \\"/callout\\", + frontMatter: { + \\"sidebar_label\\": \\"Callout\\" + } }, { name: \\"tabs\\", - route: \\"/tabs\\" + route: \\"/tabs\\", + frontMatter: { + \\"sidebar_label\\": \\"Tabs\\" + } }]; export const dynamicMetaModules = {};" `) @@ -370,11 +516,17 @@ describe('Page Process', () => { } }, { name: \\"test2\\", - route: \\"/docs/test2\\" + route: \\"/docs/test2\\", + frontMatter: { + \\"sidebar_label\\": \\"Test2\\" + } }] }, { name: \\"test1\\", - route: \\"/test1\\" + route: \\"/test1\\", + frontMatter: { + \\"sidebar_label\\": \\"Test1\\" + } }]; export const dynamicMetaModules = {};" `) diff --git a/packages/nextra/package.json b/packages/nextra/package.json index 4bb0c1f915f..9f317014b21 100644 --- a/packages/nextra/package.json +++ b/packages/nextra/package.json @@ -153,7 +153,6 @@ "@types/react-dom": "^18.2.7", "@types/webpack": "^5.28.2", "@vitejs/plugin-react": "^3.0.1", - "fast-glob": "^3.2.12", "next": "^13.4.19", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/packages/nextra/src/client/normalize-pages.ts b/packages/nextra/src/client/normalize-pages.ts index 59e08141ab8..d5a454d7cc2 100644 --- a/packages/nextra/src/client/normalize-pages.ts +++ b/packages/nextra/src/client/normalize-pages.ts @@ -225,7 +225,9 @@ export function normalizePages({ pageThemeContext: extendedPageThemeContext }) - const title = extendedMeta.title || (type !== 'separator' && a.name) + const title = + extendedMeta.title || + (type !== 'separator' && (a.frontMatter?.sidebar_label || a.name)) const getItem = (): Item => ({ ...a, type, diff --git a/packages/nextra/src/server/compile.ts b/packages/nextra/src/server/compile.ts index 635ebffa18e..053468b6fc8 100644 --- a/packages/nextra/src/server/compile.ts +++ b/packages/nextra/src/server/compile.ts @@ -93,6 +93,7 @@ type CompileMdxOptions = Pick< filePath?: string useCachedCompiler?: boolean isPageImport?: boolean + isPageMapImport?: boolean } export async function compileMdx( @@ -109,9 +110,41 @@ export async function compileMdx( mdxOptions = {}, filePath = '', useCachedCompiler, - isPageImport = true + isPageImport = true, + isPageMapImport }: CompileMdxOptions = {} ) { + const { + jsx = false, + format: _format = 'mdx', + outputFormat = 'function-body', + remarkPlugins, + rehypePlugins, + rehypePrettyCodeOptions + }: MdxOptions = mdxOptions + + const format = + _format === 'detect' ? (filePath.endsWith('.mdx') ? 'mdx' : 'md') : _format + + if (isPageMapImport) { + const compiler = createProcessor({ + format, + remarkPlugins: [ + remarkFrontmatter, // parse and attach yaml node + remarkMdxFrontMatter + ] + }) + const vFile = await compiler.process( + filePath ? { value: source, path: filePath } : source + ) + const content = vFile.toString() + + const index = content.lastIndexOf('function _createMdxContent(props) {') + const result = content.slice(0, index) + + return { result } as any + } + let searchIndexKey: string | null = null if (ERROR_ROUTES.has(route)) { /* skip */ @@ -128,18 +161,6 @@ export async function compileMdx( searchIndexKey = locale || DEFAULT_LOCALE } - const { - jsx = false, - format: _format = 'mdx', - outputFormat = 'function-body', - remarkPlugins, - rehypePlugins, - rehypePrettyCodeOptions - }: MdxOptions = mdxOptions - - const format = - _format === 'detect' ? (filePath.endsWith('.mdx') ? 'mdx' : 'md') : _format - // https://github.com/shuding/nextra/issues/1303 const isFileOutsideCWD = !isPageImport && path.relative(CWD, filePath).startsWith('..') diff --git a/packages/nextra/src/server/constants.ts b/packages/nextra/src/server/constants.ts index 9977396f32b..8db8589fd5a 100644 --- a/packages/nextra/src/server/constants.ts +++ b/packages/nextra/src/server/constants.ts @@ -2,7 +2,8 @@ * Benefit of server/constants - do not include unneeded `path` polyfill in client bundle, * while importing constants in client file */ -import path from 'node:path' +import path from 'path' +import type { Property } from 'estree' import type { NextraConfig } from '../types' export { @@ -42,3 +43,15 @@ export const EXTERNAL_URL_REGEX = /^https?:\/\// export const CODE_BLOCK_FILENAME_REGEX = /filename="([^"]+)"/ export const DEFAULT_LOCALES = [''] + +// experimental, need to deep dive why bundle becomes bigger and there is full +// reload while navigating between pages every time +export const IMPORT_FRONTMATTER = false + +export const DEFAULT_PROPERTY_PROPS: Omit = { + type: 'Property', + kind: 'init', + method: false, + shorthand: false, + computed: false +} diff --git a/packages/nextra/src/server/index.ts b/packages/nextra/src/server/index.ts index f244854404b..8ebc9c4cbc6 100644 --- a/packages/nextra/src/server/index.ts +++ b/packages/nextra/src/server/index.ts @@ -8,6 +8,7 @@ import { DEFAULT_CONFIG, DEFAULT_LOCALE, DEFAULT_LOCALES, + IMPORT_FRONTMATTER, MARKDOWN_EXTENSION_REGEX, MARKDOWN_EXTENSIONS, META_REGEX @@ -104,14 +105,34 @@ const nextra: Nextra = nextraConfig => { // Resolves ESM _app file instead cjs, so we could import theme.config via `import` statement [defaultCJSAppPath]: defaultESMAppPath } - ;(config.module.rules as RuleSetRule[]).push( + const rules = config.module.rules as RuleSetRule[] + + if (IMPORT_FRONTMATTER) { + rules.push({ + test: MARKDOWN_EXTENSION_REGEX, + issuer: request => + request.includes('.next/static/chunks/nextra-page-map'), + use: [ + options.defaultLoaders.babel, + { + loader: 'nextra/loader', + options: { ...loaderOptions, isPageMapImport: true } + } + ] + }) + } + + rules.push( { // Match Markdown imports from non-pages. These imports have an // issuer, which can be anything as long as it's not empty. // When the issuer is null, it means that it can be imported via a // runtime import call such as `import('...')`. test: MARKDOWN_EXTENSION_REGEX, - issuer: request => !!request || request === null, + issuer: request => + (!!request && + !request.includes('.next/static/chunks/nextra-page-map')) || + request === null, use: [ options.defaultLoaders.babel, { @@ -128,10 +149,7 @@ const nextra: Nextra = nextraConfig => { options.defaultLoaders.babel, { loader: 'nextra/loader', - options: { - ...loaderOptions, - isPageImport: true - } + options: { ...loaderOptions, isPageImport: true } } ] }, diff --git a/packages/nextra/src/server/loader.ts b/packages/nextra/src/server/loader.ts index 4fea42a01d2..47ebd4c160c 100644 --- a/packages/nextra/src/server/loader.ts +++ b/packages/nextra/src/server/loader.ts @@ -54,6 +54,7 @@ export async function loader( ): Promise { const { isPageImport = false, + isPageMapImport, isMetaFile, theme, themeConfig, @@ -162,9 +163,9 @@ ${themeConfigImport && '__nextra_internal__.themeConfig = __themeConfig'}` locale, filePath: mdxPath, useCachedCompiler: true, - isPageImport + isPageImport, + isPageMapImport }) - // Imported as a normal component, no need to add the layout. if (!isPageImport) { return result diff --git a/packages/nextra/src/server/mdx-plugins/rehype.ts b/packages/nextra/src/server/mdx-plugins/rehype.ts index 5f4fba61cd8..dfb3fee8d3a 100644 --- a/packages/nextra/src/server/mdx-plugins/rehype.ts +++ b/packages/nextra/src/server/mdx-plugins/rehype.ts @@ -15,8 +15,8 @@ function visit(node, tagNames, handler) { export const parseMeta = ({ defaultShowCopyCode }) => - tree => { - visit(tree, ['pre'], preEl => { + ast => { + visit(ast, ['pre'], preEl => { const [codeEl] = preEl.children // Add default language `text` for code-blocks without languages codeEl.properties.className ||= ['language-text'] @@ -30,8 +30,8 @@ export const parseMeta = }) } -export const attachMeta = () => tree => { - visit(tree, ['div', 'pre'], node => { +export const attachMeta = () => ast => { + visit(ast, ['div', 'pre'], node => { if ('data-rehype-pretty-code-fragment' in node.properties) { // remove
element that wraps
 element
       // because we'll wrap with our own 
diff --git a/packages/nextra/src/server/mdx-plugins/remark-custom-heading-id.ts b/packages/nextra/src/server/mdx-plugins/remark-custom-heading-id.ts index 862d4f4f200..83e6a23e275 100644 --- a/packages/nextra/src/server/mdx-plugins/remark-custom-heading-id.ts +++ b/packages/nextra/src/server/mdx-plugins/remark-custom-heading-id.ts @@ -6,21 +6,19 @@ export type HProperties = { id?: string } -export const remarkCustomHeadingId: Plugin<[], Root> = - () => (tree, _file, done) => { - visit(tree, 'heading', node => { - const lastChild = node.children.at(-1) - if (!lastChild || lastChild.type !== 'text') return +export const remarkCustomHeadingId: Plugin<[], Root> = () => ast => { + visit(ast, 'heading', node => { + const lastChild = node.children.at(-1) + if (!lastChild || lastChild.type !== 'text') return - const heading = lastChild.value - const matched = heading.match(/\s*\[#([^]+?)]\s*$/) + const heading = lastChild.value + const matched = heading.match(/\s*\[#([^]+?)]\s*$/) - if (!matched) return - node.data ||= {} - const headingProps: HProperties = (node.data.hProperties ||= {}) - headingProps.id = matched[1] + if (!matched) return + node.data ||= {} + const headingProps: HProperties = (node.data.hProperties ||= {}) + headingProps.id = matched[1] - lastChild.value = heading.slice(0, matched.index) - }) - done() - } + lastChild.value = heading.slice(0, matched.index) + }) +} diff --git a/packages/nextra/src/server/mdx-plugins/remark-headings.ts b/packages/nextra/src/server/mdx-plugins/remark-headings.ts index 8c626696fc2..7883cc40b67 100644 --- a/packages/nextra/src/server/mdx-plugins/remark-headings.ts +++ b/packages/nextra/src/server/mdx-plugins/remark-headings.ts @@ -1,9 +1,11 @@ +import type { SpreadElement } from 'estree' import Slugger from 'github-slugger' import type { Parent, Root } from 'mdast' import type { Plugin } from 'unified' import { visit } from 'unist-util-visit' import type { Heading } from '../../types' import { MARKDOWN_EXTENSION_REGEX } from '../constants.js' +import { createAstExportConst, createAstObject } from '../utils.js' import type { HProperties } from './remark-custom-heading-id' const getFlattenedValue = (node: Parent): string => @@ -28,12 +30,12 @@ export const remarkHeadings: Plugin< let title: string const slugger = new Slugger() - return (tree, file, done) => { + return (ast, file) => { const PartialComponentToHeadingsName: Record = Object.create(null) visit( - tree, + ast, [ 'heading', // push partial component's __toc export name to headings list @@ -109,52 +111,30 @@ export const remarkHeadings: Plugin< if (isRemoteContent) { // Attach headings for remote content, because we can't access to __toc variable file.data.headings = headings - done() return } const headingElements = headings.map(heading => typeof heading === 'string' - ? { + ? ({ type: 'SpreadElement', argument: { type: 'Identifier', name: heading } - } - : { - type: 'ObjectExpression', - properties: Object.entries(heading).map(([key, value]) => ({ - type: 'Property', - kind: 'init', - key: { type: 'Identifier', name: key }, - value: { type: 'Literal', value } - })) - } + } satisfies SpreadElement) + : createAstObject(heading) ) - tree.children.push({ + ast.children.push({ type: 'mdxjsEsm', data: { estree: { body: [ - { - type: 'ExportNamedDeclaration', - specifiers: [], - declaration: { - type: 'VariableDeclaration', - kind: 'const', - declarations: [ - { - type: 'VariableDeclarator', - id: { type: 'Identifier', name: exportName }, - init: { type: 'ArrayExpression', elements: headingElements } - } - ] - } - } + createAstExportConst(exportName, { + type: 'ArrayExpression', + elements: headingElements + }) ] } } } as any) - - done() } } diff --git a/packages/nextra/src/server/mdx-plugins/remark-link-rewrite.ts b/packages/nextra/src/server/mdx-plugins/remark-link-rewrite.ts index 7583568d61d..a38e73dbf3e 100644 --- a/packages/nextra/src/server/mdx-plugins/remark-link-rewrite.ts +++ b/packages/nextra/src/server/mdx-plugins/remark-link-rewrite.ts @@ -9,17 +9,12 @@ export type RemarkLinkRewriteOptions = { excludeExternalLinks?: boolean } -export const remarkLinkRewrite: Plugin<[RemarkLinkRewriteOptions], Root> = ({ - pattern, - replace, - excludeExternalLinks -}) => { - return (tree, _file, done) => { - visit(tree, 'link', node => { +export const remarkLinkRewrite: Plugin<[RemarkLinkRewriteOptions], Root> = + ({ pattern, replace, excludeExternalLinks }) => + ast => { + visit(ast, 'link', node => { if (!(excludeExternalLinks && EXTERNAL_URL_REGEX.test(node.url))) { node.url = node.url.replace(pattern, replace) } }) - done() } -} diff --git a/packages/nextra/src/server/mdx-plugins/remark-mdx-disable-explicit-jsx.ts b/packages/nextra/src/server/mdx-plugins/remark-mdx-disable-explicit-jsx.ts index 5c2633359eb..c4c36428b7e 100644 --- a/packages/nextra/src/server/mdx-plugins/remark-mdx-disable-explicit-jsx.ts +++ b/packages/nextra/src/server/mdx-plugins/remark-mdx-disable-explicit-jsx.ts @@ -7,9 +7,9 @@ export const remarkMdxDisableExplicitJsx: Plugin< Root > = ({ whiteList }) => - tree => { + ast => { const test = whiteList.map(name => ({ name })) - visit(tree, test, node => { + visit(ast, test, node => { delete node.data!._mdxExplicitJsx }) } diff --git a/packages/nextra/src/server/mdx-plugins/remark-mdx-frontmatter.ts b/packages/nextra/src/server/mdx-plugins/remark-mdx-frontmatter.ts index 62e89435e6d..16d46ea9bf4 100644 --- a/packages/nextra/src/server/mdx-plugins/remark-mdx-frontmatter.ts +++ b/packages/nextra/src/server/mdx-plugins/remark-mdx-frontmatter.ts @@ -2,29 +2,14 @@ import { valueToEstree } from 'estree-util-value-to-estree' import type { Parent, Root } from 'mdast' import type { Plugin } from 'unified' import { parse as parseYaml } from 'yaml' +import { createAstExportConst } from '../utils.js' function createNode(data: Record): any { return { type: 'mdxjsEsm', data: { estree: { - body: [ - { - type: 'ExportNamedDeclaration', - specifiers: [], - declaration: { - type: 'VariableDeclaration', - kind: 'const', - declarations: [ - { - type: 'VariableDeclarator', - id: { type: 'Identifier', name: 'frontMatter' }, - init: valueToEstree(data) - } - ] - } - } - ] + body: [createAstExportConst('frontMatter', valueToEstree(data))] } } } diff --git a/packages/nextra/src/server/mdx-plugins/remark-remove-imports.ts b/packages/nextra/src/server/mdx-plugins/remark-remove-imports.ts index 54f1667bcd8..26d5f909d77 100644 --- a/packages/nextra/src/server/mdx-plugins/remark-remove-imports.ts +++ b/packages/nextra/src/server/mdx-plugins/remark-remove-imports.ts @@ -2,9 +2,6 @@ import type { Root } from 'mdast' import type { Plugin } from 'unified' import { remove } from 'unist-util-remove' -export const remarkRemoveImports: Plugin<[], Root> = () => { - return (tree, _file, done) => { - remove(tree, 'mdxjsEsm') - done() - } +export const remarkRemoveImports: Plugin<[], Root> = () => ast => { + remove(ast, 'mdxjsEsm') } diff --git a/packages/nextra/src/server/mdx-plugins/remark-static-image.ts b/packages/nextra/src/server/mdx-plugins/remark-static-image.ts index 88addd49008..3d8b8c32e91 100644 --- a/packages/nextra/src/server/mdx-plugins/remark-static-image.ts +++ b/packages/nextra/src/server/mdx-plugins/remark-static-image.ts @@ -15,97 +15,94 @@ const VALID_BLUR_EXT = ['.jpeg', '.png', '.webp', '.avif', '.jpg'] // Based on the remark-embed-images project // https://github.com/remarkjs/remark-embed-images -export const remarkStaticImage: Plugin<[], Root> = - () => (tree, _file, done) => { - const importsToInject: { variableName: string; importPath: string }[] = [] +export const remarkStaticImage: Plugin<[], Root> = () => ast => { + const importsToInject: { variableName: string; importPath: string }[] = [] - visit(tree, 'image', node => { - // https://github.com/shuding/nextra/issues/1344 - let url = decodeURI(node.url) - if (!url) { - return - } + visit(ast, 'image', node => { + // https://github.com/shuding/nextra/issues/1344 + let url = decodeURI(node.url) + if (!url) { + return + } - if (EXTERNAL_URL_REGEX.test(url)) { - // do nothing with images with external url - return - } + if (EXTERNAL_URL_REGEX.test(url)) { + // do nothing with images with external url + return + } - if (url.startsWith('/')) { - const urlPath = path.join(PUBLIC_DIR, url) - if (!existsSync(urlPath)) { - return - } - url = slash(urlPath) + if (url.startsWith('/')) { + const urlPath = path.join(PUBLIC_DIR, url) + if (!existsSync(urlPath)) { + return } - // Unique variable name for the given static image URL - const variableName = `__img${importsToInject.length}` - const hasBlur = VALID_BLUR_EXT.some(ext => url.endsWith(ext)) - importsToInject.push({ variableName, importPath: url }) - // Replace the image node with an MDX component node (Next.js Image) - Object.assign(node, { - type: 'mdxJsxFlowElement', - name: 'img', - attributes: [ - // do not render empty alt in html markup - node.alt && { - type: 'mdxJsxAttribute', - name: 'alt', - value: node.alt - }, - hasBlur && { - type: 'mdxJsxAttribute', - name: 'placeholder', - value: 'blur' - }, - { - type: 'mdxJsxAttribute', - name: 'src', - value: { - type: 'mdxJsxAttributeValueExpression', - value: variableName, - data: { - estree: { - body: [ - { - type: 'ExpressionStatement', - expression: { type: 'Identifier', name: variableName } - } - ] - } + url = slash(urlPath) + } + // Unique variable name for the given static image URL + const variableName = `__img${importsToInject.length}` + const hasBlur = VALID_BLUR_EXT.some(ext => url.endsWith(ext)) + importsToInject.push({ variableName, importPath: url }) + // Replace the image node with an MDX component node (Next.js Image) + Object.assign(node, { + type: 'mdxJsxFlowElement', + name: 'img', + attributes: [ + // do not render empty alt in html markup + node.alt && { + type: 'mdxJsxAttribute', + name: 'alt', + value: node.alt + }, + hasBlur && { + type: 'mdxJsxAttribute', + name: 'placeholder', + value: 'blur' + }, + { + type: 'mdxJsxAttribute', + name: 'src', + value: { + type: 'mdxJsxAttributeValueExpression', + value: variableName, + data: { + estree: { + body: [ + { + type: 'ExpressionStatement', + expression: { type: 'Identifier', name: variableName } + } + ] } } } - ].filter(truthy) - }) + } + ].filter(truthy) }) + }) - if (importsToInject.length) { - tree.children.unshift( - ...importsToInject.map( - ({ variableName, importPath }) => - ({ - type: 'mdxjsEsm', - data: { - estree: { - body: [ - { - type: 'ImportDeclaration', - source: { type: 'Literal', value: importPath }, - specifiers: [ - { - type: 'ImportDefaultSpecifier', - local: { type: 'Identifier', name: variableName } - } - ] - } - ] - } + if (importsToInject.length) { + ast.children.unshift( + ...importsToInject.map( + ({ variableName, importPath }) => + ({ + type: 'mdxjsEsm', + data: { + estree: { + body: [ + { + type: 'ImportDeclaration', + source: { type: 'Literal', value: importPath }, + specifiers: [ + { + type: 'ImportDefaultSpecifier', + local: { type: 'Identifier', name: variableName } + } + ] + } + ] } - }) as any - ) + } + }) as any ) - } - - done() + ) } +} diff --git a/packages/nextra/src/server/mdx-plugins/remark-structurize.ts b/packages/nextra/src/server/mdx-plugins/remark-structurize.ts index 25c00173bf4..6806e00cdf6 100644 --- a/packages/nextra/src/server/mdx-plugins/remark-structurize.ts +++ b/packages/nextra/src/server/mdx-plugins/remark-structurize.ts @@ -17,11 +17,10 @@ export const remarkStructurize: Plugin<[Search], Root> = options => { let skip = false let content = '' - return (tree, file, done) => { + return (tree, file) => { walk(tree) save() file.data.structurizedData = structurizedData - done() } function save() { diff --git a/packages/nextra/src/server/page-map.ts b/packages/nextra/src/server/page-map.ts index c2e4828e066..d4053973e39 100644 --- a/packages/nextra/src/server/page-map.ts +++ b/packages/nextra/src/server/page-map.ts @@ -1,24 +1,25 @@ import path from 'node:path' -import type { - ArrayExpression, - ExportNamedDeclaration, - Expression, - ImportDeclaration, - ObjectExpression, - Property -} from 'estree' +import type { ArrayExpression, Expression, ImportDeclaration } from 'estree' import { toJs } from 'estree-util-to-js' import { valueToEstree } from 'estree-util-value-to-estree' import gracefulFs from 'graceful-fs' import grayMatter from 'gray-matter' import pLimit from 'p-limit' import { + DEFAULT_PROPERTY_PROPS, + IMPORT_FRONTMATTER, MARKDOWN_EXTENSION_REGEX, META_FILENAME, META_REGEX } from './constants.js' import { PAGES_DIR } from './file-system.js' -import { normalizePageRoute, pageTitleFromFilename, truthy } from './utils.js' +import { + createAstExportConst, + createAstObject, + normalizePageRoute, + pageTitleFromFilename, + truthy +} from './utils.js' const fs = gracefulFs.promises @@ -27,66 +28,35 @@ const limit = pLimit(20) type Import = { importName: string; filePath: string } type DynamicImport = { importName: string; route: string } -const DEFAULT_OBJECT_PROPS: Omit = { - type: 'Property', - kind: 'init', - method: false, - shorthand: false, - computed: false -} - -function createAstObject( - obj: Record -): ObjectExpression { - return { - type: 'ObjectExpression', - properties: Object.entries(obj).map(([key, value]) => ({ - ...DEFAULT_OBJECT_PROPS, - key: { type: 'Identifier', name: key }, - value: - value && typeof value === 'object' ? value : { type: 'Literal', value } - })) - } -} - -function createAstExportConst( - name: string, - value: ArrayExpression | ObjectExpression -): ExportNamedDeclaration { - return { - type: 'ExportNamedDeclaration', - specifiers: [], - declaration: { - type: 'VariableDeclaration', - kind: 'const', - declarations: [ - { - type: 'VariableDeclarator', - id: { type: 'Identifier', name }, - init: value - } - ] - } - } -} - type CollectFilesOptions = { dir: string route: string - metaImports?: Import[] + imports?: Import[] dynamicMetaImports?: DynamicImport[] isFollowingSymlink: boolean } +function cleanFileName(name: string): string { + return ( + path + .relative(PAGES_DIR, name) + .replace(/\.([jt]sx?|json|mdx?)$/, '') + .replaceAll(/[\W_]+/g, '_') + .replace(/^_/, '') + // Variable can't start with number + .replace(/^\d/, (match: string) => `_${match}`) + ) +} + async function collectFiles({ dir, route, - metaImports = [], + imports = [], dynamicMetaImports = [], isFollowingSymlink }: CollectFilesOptions): Promise<{ pageMapAst: ArrayExpression - metaImports: Import[] + imports: Import[] dynamicMetaImports: DynamicImport[] }> { const files = await fs.readdir(dir, { withFileTypes: true }) @@ -120,7 +90,7 @@ async function collectFiles({ const { pageMapAst } = await collectFiles({ dir: filePath, route: fileRoute, - metaImports, + imports, dynamicMetaImports, isFollowingSymlink }) @@ -138,15 +108,25 @@ async function collectFiles({ // add concurrency because folder can contain a lot of files return limit(async () => { if (MARKDOWN_EXTENSION_REGEX.test(ext)) { - const content = await fs.readFile(filePath, 'utf8') - const { data } = grayMatter(content) + let frontMatter: Expression + + if (IMPORT_FRONTMATTER) { + const importName = cleanFileName(filePath) + imports.push({ importName, filePath }) + frontMatter = { type: 'Identifier', name: importName } + } else { + const content = await fs.readFile(filePath, 'utf8') + const { data } = grayMatter(content) + frontMatter = valueToEstree({ + sidebar_label: pageTitleFromFilename(name), + ...data + }) + } return createAstObject({ name: path.parse(filePath).name, route: fileRoute, - ...(Object.keys(data).length && { - frontMatter: valueToEstree(data) - }) + frontMatter }) } @@ -154,12 +134,9 @@ async function collectFiles({ const isMetaJs = META_REGEX.test(fileName) if (fileName === META_FILENAME || isMetaJs) { - const importName = path - .relative(PAGES_DIR, filePath) - .replace(/\.([jt]sx?|json)?$/, '') - .replaceAll(/[\W_]+/g, '_') - .replace(/^_/, '') - metaImports.push({ importName, filePath }) + const importName = cleanFileName(filePath) + imports.push({ importName, filePath }) + if (isMetaJs) { const dynamicPage = array.find(f => f.name.startsWith('[')) if (dynamicPage) { @@ -198,7 +175,7 @@ async function collectFiles({ return { pageMapAst: { type: 'ArrayExpression', elements: items }, - metaImports, + imports, dynamicMetaImports } } @@ -210,13 +187,13 @@ export async function collectPageMap({ dir: string route?: string }): Promise { - const { pageMapAst, metaImports, dynamicMetaImports } = await collectFiles({ + const { pageMapAst, imports, dynamicMetaImports } = await collectFiles({ dir, route, isFollowingSymlink: false }) - const metaImportsAST: ImportDeclaration[] = metaImports + const metaImportsAST: ImportDeclaration[] = imports // localeCompare to avoid race condition .sort((a, b) => a.filePath.localeCompare(b.filePath)) .map(({ filePath, importName }) => ({ @@ -224,8 +201,13 @@ export async function collectPageMap({ source: { type: 'Literal', value: filePath }, specifiers: [ { - type: 'ImportDefaultSpecifier', - local: { type: 'Identifier', name: importName } + local: { type: 'Identifier', name: importName }, + ...(IMPORT_FRONTMATTER && MARKDOWN_EXTENSION_REGEX.test(filePath) + ? { + type: 'ImportSpecifier', + imported: { type: 'Identifier', name: 'frontMatter' } + } + : { type: 'ImportDefaultSpecifier' }) } ] })) @@ -242,7 +224,7 @@ export async function collectPageMap({ // localeCompare to avoid race condition .sort((a, b) => a.route.localeCompare(b.route)) .map(({ importName, route }) => ({ - ...DEFAULT_OBJECT_PROPS, + ...DEFAULT_PROPERTY_PROPS, key: { type: 'Literal', value: route }, value: { type: 'Identifier', name: importName } })) diff --git a/packages/nextra/src/server/utils.ts b/packages/nextra/src/server/utils.ts index b4759241567..64c8b252cee 100644 --- a/packages/nextra/src/server/utils.ts +++ b/packages/nextra/src/server/utils.ts @@ -1,7 +1,14 @@ import path from 'path' +import type { + ArrayExpression, + ExportNamedDeclaration, + Expression, + ObjectExpression +} from 'estree' import slash from 'slash' import title from 'title' import type { Folder, MdxFile } from '../types' +import { DEFAULT_PROPERTY_PROPS } from './constants.js' type Truthy = T extends false | '' | 0 | null | undefined ? never : T // from lodash @@ -16,7 +23,7 @@ export const logger = { } export function pageTitleFromFilename(fileName: string) { - return title(fileName.replaceAll(/[-_]/g, ' ')) + return title(fileName.replaceAll(/[-_]/g, ' '), { special: ['SSR'] }) } export function sortPages( @@ -51,3 +58,38 @@ export function sortPages( export function normalizePageRoute(parentRoute: string, route: string): string { return slash(path.join(parentRoute, route.replace(/^index$/, ''))) } + +export function createAstExportConst( + name: string, + value: ArrayExpression | ObjectExpression | Expression +): ExportNamedDeclaration { + return { + type: 'ExportNamedDeclaration', + specifiers: [], + declaration: { + type: 'VariableDeclaration', + kind: 'const', + declarations: [ + { + type: 'VariableDeclarator', + id: { type: 'Identifier', name }, + init: value + } + ] + } + } +} + +export function createAstObject( + obj: Record +): ObjectExpression { + return { + type: 'ObjectExpression', + properties: Object.entries(obj).map(([key, value]) => ({ + ...DEFAULT_PROPERTY_PROPS, + key: { type: 'Identifier', name: key }, + value: + value && typeof value === 'object' ? value : { type: 'Literal', value } + })) + } +} diff --git a/packages/nextra/src/types.ts b/packages/nextra/src/types.ts index b7c11b5b30a..a77e674cdc5 100644 --- a/packages/nextra/src/types.ts +++ b/packages/nextra/src/types.ts @@ -7,6 +7,7 @@ import type { nextraConfigSchema, searchSchema } from './server/schemas' export interface LoaderOptions extends NextraConfig { isPageImport?: boolean + isPageMapImport?: boolean isMetaFile?: boolean locales: string[] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fad8c7d3bf5..daa6931b7fe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -343,9 +343,6 @@ importers: '@vitejs/plugin-react': specifier: ^3.0.1 version: 3.0.1(vite@4.0.4) - fast-glob: - specifier: ^3.2.12 - version: 3.2.12 next: specifier: ^13.4.19 version: 13.4.19(@babel/core@7.21.8)(react-dom@18.2.0)(react@18.2.0)