diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap index d25aa4d549..9793ebe0da 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap @@ -662,6 +662,7 @@ Array [ exports[`site with custom sidebar items generator sidebarItemsGenerator is called with appropriate data 1`] = ` Object { + "defaultSidebarItemsGenerator": [Function], "docs": Array [ Object { "frontMatter": Object {}, diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts index a06aed5bd1..ad7773b623 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts @@ -24,11 +24,18 @@ import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants'; import * as cliDocs from '../cli'; import {OptionsSchema} from '../options'; import {normalizePluginOptions} from '@docusaurus/utils-validation'; -import {DocMetadata, LoadedVersion, SidebarItemsGenerator} from '../types'; +import { + DocMetadata, + LoadedVersion, + SidebarItem, + SidebarItemsGeneratorOption, + SidebarItemsGeneratorOptionArgs, +} from '../types'; import {toSidebarsProp} from '../props'; // @ts-expect-error: TODO typedefs missing? import {validate} from 'webpack'; +import {DefaultSidebarItemsGenerator} from '../sidebarItemsGenerator'; function findDocById(version: LoadedVersion, unversionedId: string) { return version.docs.find((item) => item.unversionedId === unversionedId); @@ -1576,7 +1583,7 @@ describe('site with partial autogenerated sidebars 2 (fix #4638)', () => { }); describe('site with custom sidebar items generator', () => { - async function loadSite(sidebarItemsGenerator: SidebarItemsGenerator) { + async function loadSite(sidebarItemsGenerator: SidebarItemsGeneratorOption) { const siteDir = path.join( __dirname, '__fixtures__', @@ -1594,42 +1601,19 @@ describe('site with custom sidebar items generator', () => { return {content, siteDir}; } - test('sidebar is autogenerated according to custom sidebarItemsGenerator', async () => { - const customSidebarItemsGenerator: SidebarItemsGenerator = async () => { - return [ - {type: 'doc', id: 'API/api-overview'}, - {type: 'doc', id: 'API/api-end'}, - ]; - }; - - const customSidebarItemsGeneratorMock: SidebarItemsGenerator = jest.fn( - customSidebarItemsGenerator, - ); - - const {content} = await loadSite(customSidebarItemsGeneratorMock); - const version = content.loadedVersions[0]; - - expect(version.sidebars).toEqual({ - defaultSidebar: [ - {type: 'doc', id: 'API/api-overview'}, - {type: 'doc', id: 'API/api-end'}, - ], - }); - }); - test('sidebarItemsGenerator is called with appropriate data', async () => { - type GeneratorArg = Parameters[0]; - const customSidebarItemsGeneratorMock = jest.fn( - async (_arg: GeneratorArg) => [], + async (_arg: SidebarItemsGeneratorOptionArgs) => [], ); const {siteDir} = await loadSite(customSidebarItemsGeneratorMock); - const generatorArg: GeneratorArg = + const generatorArg: SidebarItemsGeneratorOptionArgs = customSidebarItemsGeneratorMock.mock.calls[0][0]; // Make test pass even if docs are in different order and paths are absolutes - function makeDeterministic(arg: GeneratorArg): GeneratorArg { + function makeDeterministic( + arg: SidebarItemsGeneratorOptionArgs, + ): SidebarItemsGeneratorOptionArgs { return { ...arg, docs: orderBy(arg.docs, 'id'), @@ -1641,5 +1625,140 @@ describe('site with custom sidebar items generator', () => { } expect(makeDeterministic(generatorArg)).toMatchSnapshot(); + expect(generatorArg.defaultSidebarItemsGenerator).toEqual( + DefaultSidebarItemsGenerator, + ); + }); + + test('sidebar is autogenerated according to a custom sidebarItemsGenerator', async () => { + const customSidebarItemsGenerator: SidebarItemsGeneratorOption = async () => { + return [ + {type: 'doc', id: 'API/api-overview'}, + {type: 'doc', id: 'API/api-end'}, + ]; + }; + + const {content} = await loadSite(customSidebarItemsGenerator); + const version = content.loadedVersions[0]; + + expect(version.sidebars).toEqual({ + defaultSidebar: [ + {type: 'doc', id: 'API/api-overview'}, + {type: 'doc', id: 'API/api-end'}, + ], + }); + }); + + test('sidebarItemsGenerator can wrap/enhance/sort/reverse the default sidebar generator', async () => { + function reverseSidebarItems(items: SidebarItem[]): SidebarItem[] { + const result: SidebarItem[] = items.map((item) => { + if (item.type === 'category') { + return {...item, items: reverseSidebarItems(item.items)}; + } + return item; + }); + result.reverse(); + return result; + } + + const reversedSidebarItemsGenerator: SidebarItemsGeneratorOption = async ({ + defaultSidebarItemsGenerator, + ...args + }) => { + const sidebarItems = await defaultSidebarItemsGenerator(args); + return reverseSidebarItems(sidebarItems); + }; + + const {content} = await loadSite(reversedSidebarItemsGenerator); + const version = content.loadedVersions[0]; + + expect(version.sidebars).toEqual({ + defaultSidebar: [ + { + type: 'category', + label: 'API (label from _category_.json)', + collapsed: true, + items: [ + { + type: 'doc', + id: 'API/api-end', + }, + { + type: 'category', + label: 'Extension APIs (label from _category_.yml)', + collapsed: true, + items: [ + { + type: 'doc', + id: 'API/Extension APIs/Theme API', + }, + { + type: 'doc', + id: 'API/Extension APIs/Plugin API', + }, + ], + }, + { + type: 'category', + label: 'Core APIs', + collapsed: true, + items: [ + { + type: 'doc', + id: 'API/Core APIs/Server API', + }, + { + type: 'doc', + id: 'API/Core APIs/Client API', + }, + ], + }, + { + type: 'doc', + id: 'API/api-overview', + }, + ], + }, + { + type: 'category', + label: 'Guides', + collapsed: true, + items: [ + { + type: 'doc', + id: 'Guides/guide5', + }, + { + type: 'doc', + id: 'Guides/guide4', + }, + { + type: 'doc', + id: 'Guides/guide3', + }, + { + type: 'doc', + id: 'Guides/guide2.5', + }, + { + type: 'doc', + id: 'Guides/guide2', + }, + { + type: 'doc', + id: 'Guides/guide1', + }, + ], + }, + { + type: 'doc', + id: 'installation', + }, + { + type: 'doc', + id: 'getting-started', + }, + ], + }); }); }); diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/sidebars.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/sidebars.test.ts index 4e717cd10f..1b87d2d9ac 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/sidebars.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/sidebars.test.ts @@ -24,6 +24,7 @@ import { Sidebars, UnprocessedSidebars, } from '../types'; +import {DefaultSidebarItemsGenerator} from '../sidebarItemsGenerator'; /* eslint-disable global-require, import/no-dynamic-require */ @@ -534,16 +535,19 @@ describe('processSidebars', () => { expect(StaticSidebarItemsGenerator).toHaveBeenCalledTimes(3); expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({ + defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator, item: {type: 'autogenerated', dirName: 'dir1'}, docs: [], version: {}, }); expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({ + defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator, item: {type: 'autogenerated', dirName: 'dir2'}, docs: [], version: {}, }); expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({ + defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator, item: {type: 'autogenerated', dirName: 'dir3'}, docs: [], version: {}, diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars.ts b/packages/docusaurus-plugin-content-docs/src/sidebars.ts index 53b202e9da..feae57e475 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars.ts @@ -21,14 +21,15 @@ import { UnprocessedSidebar, DocMetadataBase, VersionMetadata, - SidebarItemsGenerator, SidebarItemsGeneratorDoc, SidebarItemsGeneratorVersion, NumberPrefixParser, + SidebarItemsGeneratorOption, } from './types'; import {mapValues, flatten, flatMap, difference, pick, memoize} from 'lodash'; import {getElementsAround} from '@docusaurus/utils'; import combinePromises from 'combine-promises'; +import {DefaultSidebarItemsGenerator} from './sidebarItemsGenerator'; type SidebarItemCategoryJSON = SidebarItemBase & { type: 'category'; @@ -295,7 +296,7 @@ export async function processSidebar({ docs, version, }: { - sidebarItemsGenerator: SidebarItemsGenerator; + sidebarItemsGenerator: SidebarItemsGeneratorOption; numberPrefixParser: NumberPrefixParser; unprocessedSidebar: UnprocessedSidebar; docs: DocMetadataBase[]; @@ -322,6 +323,7 @@ export async function processSidebar({ return sidebarItemsGenerator({ item, numberPrefixParser, + defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator, ...getSidebarItemsGeneratorDocsAndVersion(), }); } @@ -338,7 +340,7 @@ export async function processSidebars({ docs, version, }: { - sidebarItemsGenerator: SidebarItemsGenerator; + sidebarItemsGenerator: SidebarItemsGeneratorOption; numberPrefixParser: NumberPrefixParser; unprocessedSidebars: UnprocessedSidebars; docs: DocMetadataBase[]; diff --git a/packages/docusaurus-plugin-content-docs/src/types.ts b/packages/docusaurus-plugin-content-docs/src/types.ts index b642586123..585408ba21 100644 --- a/packages/docusaurus-plugin-content-docs/src/types.ts +++ b/packages/docusaurus-plugin-content-docs/src/types.ts @@ -84,7 +84,7 @@ export type PluginOptions = MetadataOptions & disableVersioning: boolean; excludeNextVersionDocs?: boolean; includeCurrentVersion: boolean; - sidebarItemsGenerator: SidebarItemsGenerator; + sidebarItemsGenerator: SidebarItemsGeneratorOption; }; export type SidebarItemBase = { @@ -151,12 +151,25 @@ export type SidebarItemsGeneratorVersion = Pick< VersionMetadata, 'versionName' | 'contentPath' >; -export type SidebarItemsGenerator = (generatorArgs: { + +export type SidebarItemsGeneratorArgs = { item: UnprocessedSidebarItemAutogenerated; version: SidebarItemsGeneratorVersion; docs: SidebarItemsGeneratorDoc[]; numberPrefixParser: NumberPrefixParser; -}) => Promise; +}; +export type SidebarItemsGenerator = ( + generatorArgs: SidebarItemsGeneratorArgs, +) => Promise; + +// Also inject the default generator to conveniently wrap/enhance/sort the default sidebar gen logic +// see https://github.com/facebook/docusaurus/issues/4640#issuecomment-822292320 +export type SidebarItemsGeneratorOptionArgs = { + defaultSidebarItemsGenerator: SidebarItemsGenerator; +} & SidebarItemsGeneratorArgs; +export type SidebarItemsGeneratorOption = ( + generatorArgs: SidebarItemsGeneratorOptionArgs, +) => Promise; export type OrderMetadata = { previous?: string; diff --git a/website/docs/api/plugins/plugin-content-docs.md b/website/docs/api/plugins/plugin-content-docs.md index 639d0ac74b..2861ce4f72 100644 --- a/website/docs/api/plugins/plugin-content-docs.md +++ b/website/docs/api/plugins/plugin-content-docs.md @@ -76,14 +76,25 @@ module.exports = { * Function used to replace the sidebar items of type "autogenerated" * by real sidebar items (docs, categories, links...) */ - sidebarItemsGenerator: function ({ - item, - version, - docs, - numberPrefixParser, + sidebarItemsGenerator: async function ({ + defaultSidebarItemsGenerator, // useful to re-use/enhance default sidebar generation logic from Docusaurus + numberPrefixParser, // numberPrefixParser configured for this plugin + item, // the sidebar item with type "autogenerated" + version, // the current version + docs, // all the docs of that version (unfiltered) }) { - // Use the provided data to create a custom "sidebar slice" - return [{type: 'doc', id: 'doc1'}]; + // Use the provided data to generate a custom sidebar slice + return [ + {type: 'doc', id: 'intro'}, + { + type: 'category', + label: 'Tutorials', + items: [ + {type: 'doc', id: 'tutorial1'}, + {type: 'doc', id: 'tutorial2'}, + ], + }, + ]; }, /** * The Docs plugin supports number prefixes like "01-My Folder/02.My Doc.md". diff --git a/website/docs/guides/docs/sidebar.md b/website/docs/guides/docs/sidebar.md index e6fb08f0af..55c7700455 100644 --- a/website/docs/guides/docs/sidebar.md +++ b/website/docs/guides/docs/sidebar.md @@ -521,14 +521,19 @@ module.exports = { [ '@docusaurus/plugin-content-docs', { - /** - * Function used to replace the sidebar items of type "autogenerated" - * by real sidebar items (docs, categories, links...) - */ // highlight-start - sidebarItemsGenerator: function ({item, version, docs}) { - // Use the provided data to create a custom "sidebar slice" - return [{type: 'doc', id: 'doc1'}]; + sidebarItemsGenerator: async function ({ + defaultSidebarItemsGenerator, + numberPrefixParser, + item, + version, + docs, + }) { + // Example: return an hardcoded list of static sidebar items + return [ + {type: 'doc', id: 'doc1'}, + {type: 'doc', id: 'doc2'}, + ]; }, // highlight-end }, @@ -537,6 +542,51 @@ module.exports = { }; ``` +:::tip + +**Re-use and enhance the default generator** instead of writing a generator from scratch. + +**Add, update, filter, re-order** the sidebar items according to your use-case: + +```js title="docusaurus.config.js" +// highlight-start +// Reverse the sidebar items ordering (including nested category items) +function reverseSidebarItems(items) { + // Reverse items in categories + const result = items.map((item) => { + if (item.type === 'category') { + return {...item, items: reverseSidebarItems(item.items)}; + } + return item; + }); + // Reverse items at current level + result.reverse(); + return result; +} +// highlight-end + +module.exports = { + plugins: [ + [ + '@docusaurus/plugin-content-docs', + { + // highlight-start + sidebarItemsGenerator: async function ({ + defaultSidebarItemsGenerator, + ...args + }) { + const sidebarItems = await defaultSidebarItemsGenerator(args); + return reverseSidebarItems(sidebarItems); + }, + // highlight-end + }, + ], + ], +}; +``` + +::: + ## Hideable sidebar {#hideable-sidebar} Using the enabled `themeConfig.hideableSidebar` option, you can make the entire sidebar hidden, allowing you to better focus your users on the content. This is especially useful when content consumption on medium screens (e.g. on tablets).