mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-12 08:37:25 +02:00
feat(v2): allow user to customize/enhance the default sidebar items generator (#4658)
* allow and document how to wrap/enhance the default docusaurus sidebar items generator * improve doc * doc * doc
This commit is contained in:
parent
e11597aba9
commit
792f4ac6fb
7 changed files with 250 additions and 50 deletions
|
@ -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 {},
|
||||
|
|
|
@ -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<SidebarItemsGenerator>[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',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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: {},
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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<SidebarItem[]>;
|
||||
};
|
||||
export type SidebarItemsGenerator = (
|
||||
generatorArgs: SidebarItemsGeneratorArgs,
|
||||
) => Promise<SidebarItem[]>;
|
||||
|
||||
// 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<SidebarItem[]>;
|
||||
|
||||
export type OrderMetadata = {
|
||||
previous?: string;
|
||||
|
|
|
@ -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".
|
||||
|
|
|
@ -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).
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue