mirror of
https://github.com/facebook/docusaurus.git
synced 2025-07-27 21:48:41 +02:00
feat(docs): sidebar item key
attribute - fix docs translations key conflicts (#11228)
This commit is contained in:
parent
d9d7e855c2
commit
da08536816
23 changed files with 632 additions and 59 deletions
|
@ -5,7 +5,12 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {toSidebarDocItemLinkProp, toTagDocListProp} from '../props';
|
import {fromPartial} from '@total-typescript/shoehorn';
|
||||||
|
import {
|
||||||
|
toSidebarDocItemLinkProp,
|
||||||
|
toSidebarsProp,
|
||||||
|
toTagDocListProp,
|
||||||
|
} from '../props';
|
||||||
|
|
||||||
describe('toTagDocListProp', () => {
|
describe('toTagDocListProp', () => {
|
||||||
type Params = Parameters<typeof toTagDocListProp>[0];
|
type Params = Parameters<typeof toTagDocListProp>[0];
|
||||||
|
@ -132,3 +137,123 @@ describe('toSidebarDocItemLinkProp', () => {
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('toSidebarsProp', () => {
|
||||||
|
type Params = Parameters<typeof toSidebarsProp>[0];
|
||||||
|
|
||||||
|
it('works', () => {
|
||||||
|
const params: Params = {
|
||||||
|
docs: [
|
||||||
|
fromPartial({
|
||||||
|
id: 'doc-id-1',
|
||||||
|
permalink: '/doc-1',
|
||||||
|
title: 'Doc 1 title',
|
||||||
|
frontMatter: {},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
sidebars: {
|
||||||
|
mySidebar: [
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
label: 'Example link',
|
||||||
|
key: 'link-example-key',
|
||||||
|
href: 'https://example.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'ref',
|
||||||
|
label: 'Doc 1 ref',
|
||||||
|
key: 'ref-with-doc-id-1',
|
||||||
|
id: 'doc-id-1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'ref',
|
||||||
|
id: 'doc-id-1',
|
||||||
|
// no label/key on purpose
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'My category',
|
||||||
|
key: 'my-category-key',
|
||||||
|
collapsible: false,
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'doc',
|
||||||
|
label: 'Doc 1',
|
||||||
|
key: 'doc-id-1',
|
||||||
|
id: 'doc-id-1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'doc',
|
||||||
|
id: 'doc-id-1',
|
||||||
|
// no label/key on purpose
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = toSidebarsProp(params);
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"mySidebar": [
|
||||||
|
{
|
||||||
|
"href": "https://example.com",
|
||||||
|
"key": "link-example-key",
|
||||||
|
"label": "Example link",
|
||||||
|
"type": "link",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"className": undefined,
|
||||||
|
"customProps": undefined,
|
||||||
|
"docId": "doc-id-1",
|
||||||
|
"href": "/doc-1",
|
||||||
|
"key": "ref-with-doc-id-1",
|
||||||
|
"label": "Doc 1 ref",
|
||||||
|
"type": "link",
|
||||||
|
"unlisted": undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"className": undefined,
|
||||||
|
"customProps": undefined,
|
||||||
|
"docId": "doc-id-1",
|
||||||
|
"href": "/doc-1",
|
||||||
|
"label": "Doc 1 title",
|
||||||
|
"type": "link",
|
||||||
|
"unlisted": undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsed": true,
|
||||||
|
"collapsible": false,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"className": undefined,
|
||||||
|
"customProps": undefined,
|
||||||
|
"docId": "doc-id-1",
|
||||||
|
"href": "/doc-1",
|
||||||
|
"key": "doc-id-1",
|
||||||
|
"label": "Doc 1",
|
||||||
|
"type": "link",
|
||||||
|
"unlisted": undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"className": undefined,
|
||||||
|
"customProps": undefined,
|
||||||
|
"docId": "doc-id-1",
|
||||||
|
"href": "/doc-1",
|
||||||
|
"label": "Doc 1 title",
|
||||||
|
"type": "link",
|
||||||
|
"unlisted": undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"key": "my-category-key",
|
||||||
|
"label": "My category",
|
||||||
|
"type": "category",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -16,6 +16,7 @@ import type {
|
||||||
LoadedContent,
|
LoadedContent,
|
||||||
LoadedVersion,
|
LoadedVersion,
|
||||||
} from '@docusaurus/plugin-content-docs';
|
} from '@docusaurus/plugin-content-docs';
|
||||||
|
import type {Sidebar} from '../sidebars/types';
|
||||||
|
|
||||||
function createSampleDoc(doc: Pick<DocMetadata, 'id'>): DocMetadata {
|
function createSampleDoc(doc: Pick<DocMetadata, 'id'>): DocMetadata {
|
||||||
return {
|
return {
|
||||||
|
@ -41,7 +42,7 @@ function createSampleDoc(doc: Pick<DocMetadata, 'id'>): DocMetadata {
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSampleVersion(
|
function createSampleVersion(
|
||||||
version: Pick<LoadedVersion, 'versionName'>,
|
version: Pick<LoadedVersion, 'versionName'> & Partial<LoadedVersion>,
|
||||||
): LoadedVersion {
|
): LoadedVersion {
|
||||||
return {
|
return {
|
||||||
label: `${version.versionName} label`,
|
label: `${version.versionName} label`,
|
||||||
|
@ -152,6 +153,150 @@ describe('getLoadedContentTranslationFiles', () => {
|
||||||
it('returns translation files', () => {
|
it('returns translation files', () => {
|
||||||
expect(getSampleTranslationFiles()).toMatchSnapshot();
|
expect(getSampleTranslationFiles()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('translation key conflicts', () => {
|
||||||
|
function runTest({withUniqueKeys}: {withUniqueKeys: boolean}) {
|
||||||
|
const sidebarWithConflicts: Sidebar = [
|
||||||
|
{
|
||||||
|
type: 'doc',
|
||||||
|
id: 'doc4',
|
||||||
|
label: 'COMMON LABEL',
|
||||||
|
translatable: true,
|
||||||
|
...(withUniqueKeys && {key: 'key-doc4'}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'doc',
|
||||||
|
id: 'doc5',
|
||||||
|
label: 'COMMON LABEL',
|
||||||
|
translatable: true,
|
||||||
|
...(withUniqueKeys && {key: 'key-doc5'}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'ref',
|
||||||
|
id: 'doc4',
|
||||||
|
label: 'COMMON LABEL',
|
||||||
|
translatable: true,
|
||||||
|
...(withUniqueKeys && {key: 'key-ref4'}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'ref',
|
||||||
|
id: 'doc5',
|
||||||
|
label: 'COMMON LABEL',
|
||||||
|
translatable: true,
|
||||||
|
...(withUniqueKeys && {key: 'key-ref5'}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'COMMON LABEL',
|
||||||
|
items: [],
|
||||||
|
collapsed: false,
|
||||||
|
collapsible: true,
|
||||||
|
...(withUniqueKeys && {key: 'key-cat1'}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'COMMON LABEL',
|
||||||
|
items: [],
|
||||||
|
collapsed: false,
|
||||||
|
collapsible: true,
|
||||||
|
...(withUniqueKeys && {key: 'key-cat2'}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
href: 'https://example.com',
|
||||||
|
label: 'COMMON LABEL',
|
||||||
|
...(withUniqueKeys && {key: 'key-link1'}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
href: 'https://example.com',
|
||||||
|
label: 'COMMON LABEL',
|
||||||
|
...(withUniqueKeys && {key: 'key-link2'}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const version = createSampleVersion({
|
||||||
|
versionName: CURRENT_VERSION_NAME,
|
||||||
|
sidebars: {
|
||||||
|
sidebarWithConflicts,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return getLoadedContentTranslationFiles({
|
||||||
|
loadedVersions: [version],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('works on sidebar with translation key conflicts resolved by unique sidebar item keys', () => {
|
||||||
|
expect(runTest({withUniqueKeys: true})).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"sidebar.sidebarWithConflicts.category.key-cat1": {
|
||||||
|
"description": "The label for category COMMON LABEL in sidebar sidebarWithConflicts",
|
||||||
|
"message": "COMMON LABEL",
|
||||||
|
},
|
||||||
|
"sidebar.sidebarWithConflicts.category.key-cat2": {
|
||||||
|
"description": "The label for category COMMON LABEL in sidebar sidebarWithConflicts",
|
||||||
|
"message": "COMMON LABEL",
|
||||||
|
},
|
||||||
|
"sidebar.sidebarWithConflicts.doc.key-doc4": {
|
||||||
|
"description": "The label for the doc item COMMON LABEL in sidebar sidebarWithConflicts, linking to the doc doc4",
|
||||||
|
"message": "COMMON LABEL",
|
||||||
|
},
|
||||||
|
"sidebar.sidebarWithConflicts.doc.key-doc5": {
|
||||||
|
"description": "The label for the doc item COMMON LABEL in sidebar sidebarWithConflicts, linking to the doc doc5",
|
||||||
|
"message": "COMMON LABEL",
|
||||||
|
},
|
||||||
|
"sidebar.sidebarWithConflicts.doc.key-ref4": {
|
||||||
|
"description": "The label for the doc item COMMON LABEL in sidebar sidebarWithConflicts, linking to the doc doc4",
|
||||||
|
"message": "COMMON LABEL",
|
||||||
|
},
|
||||||
|
"sidebar.sidebarWithConflicts.doc.key-ref5": {
|
||||||
|
"description": "The label for the doc item COMMON LABEL in sidebar sidebarWithConflicts, linking to the doc doc5",
|
||||||
|
"message": "COMMON LABEL",
|
||||||
|
},
|
||||||
|
"sidebar.sidebarWithConflicts.link.key-link1": {
|
||||||
|
"description": "The label for link COMMON LABEL in sidebar sidebarWithConflicts, linking to https://example.com",
|
||||||
|
"message": "COMMON LABEL",
|
||||||
|
},
|
||||||
|
"sidebar.sidebarWithConflicts.link.key-link2": {
|
||||||
|
"description": "The label for link COMMON LABEL in sidebar sidebarWithConflicts, linking to https://example.com",
|
||||||
|
"message": "COMMON LABEL",
|
||||||
|
},
|
||||||
|
"version.label": {
|
||||||
|
"description": "The label for version current",
|
||||||
|
"message": "current label",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"path": "current",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws on sidebar translation key conflicts', () => {
|
||||||
|
expect(() => runTest({withUniqueKeys: false}))
|
||||||
|
.toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
"Multiple docs sidebar items produce the same translation key.
|
||||||
|
- \`sidebar.sidebarWithConflicts.category.COMMON LABEL\`: 2 duplicates found:
|
||||||
|
- COMMON LABEL (The label for category COMMON LABEL in sidebar sidebarWithConflicts)
|
||||||
|
- COMMON LABEL (The label for category COMMON LABEL in sidebar sidebarWithConflicts)
|
||||||
|
|
||||||
|
- \`sidebar.sidebarWithConflicts.link.COMMON LABEL\`: 2 duplicates found:
|
||||||
|
- COMMON LABEL (The label for link COMMON LABEL in sidebar sidebarWithConflicts, linking to https://example.com)
|
||||||
|
- COMMON LABEL (The label for link COMMON LABEL in sidebar sidebarWithConflicts, linking to https://example.com)
|
||||||
|
|
||||||
|
- \`sidebar.sidebarWithConflicts.doc.COMMON LABEL\`: 4 duplicates found:
|
||||||
|
- COMMON LABEL (The label for the doc item COMMON LABEL in sidebar sidebarWithConflicts, linking to the doc doc4)
|
||||||
|
- COMMON LABEL (The label for the doc item COMMON LABEL in sidebar sidebarWithConflicts, linking to the doc doc5)
|
||||||
|
- COMMON LABEL (The label for the doc item COMMON LABEL in sidebar sidebarWithConflicts, linking to the doc doc4)
|
||||||
|
- COMMON LABEL (The label for the doc item COMMON LABEL in sidebar sidebarWithConflicts, linking to the doc doc5)
|
||||||
|
|
||||||
|
To avoid translation key conflicts, use the \`key\` attribute on the sidebar items above to uniquely identify them.
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('translateLoadedContent', () => {
|
describe('translateLoadedContent', () => {
|
||||||
|
|
|
@ -41,6 +41,7 @@ export function toSidebarDocItemLinkProp({
|
||||||
const {id, title, permalink, frontMatter, unlisted} = doc;
|
const {id, title, permalink, frontMatter, unlisted} = doc;
|
||||||
return {
|
return {
|
||||||
type: 'link',
|
type: 'link',
|
||||||
|
...(item.key && {key: item.key}),
|
||||||
href: permalink,
|
href: permalink,
|
||||||
// Front Matter data takes precedence over sidebars.json
|
// Front Matter data takes precedence over sidebars.json
|
||||||
label: frontMatter.sidebar_label ?? item.label ?? title,
|
label: frontMatter.sidebar_label ?? item.label ?? title,
|
||||||
|
@ -51,7 +52,9 @@ export function toSidebarDocItemLinkProp({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toSidebarsProp(loadedVersion: LoadedVersion): PropSidebars {
|
export function toSidebarsProp(
|
||||||
|
loadedVersion: Pick<LoadedVersion, 'docs' | 'sidebars'>,
|
||||||
|
): PropSidebars {
|
||||||
const docsById = createDocsByIdIndex(loadedVersion.docs);
|
const docsById = createDocsByIdIndex(loadedVersion.docs);
|
||||||
|
|
||||||
function getDocById(docId: string): DocMetadata {
|
function getDocById(docId: string): DocMetadata {
|
||||||
|
|
|
@ -47,6 +47,7 @@ exports[`DefaultSidebarItemsGenerator generates complex nested sidebar 1`] = `
|
||||||
"type": "doc",
|
"type": "doc",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"key": "SubGuides-category-unique-key",
|
||||||
"label": "SubGuides (metadata file label)",
|
"label": "SubGuides (metadata file label)",
|
||||||
"link": {
|
"link": {
|
||||||
"description": "subGuides-description",
|
"description": "subGuides-description",
|
||||||
|
|
|
@ -129,6 +129,7 @@ describe('DefaultSidebarItemsGenerator', () => {
|
||||||
},
|
},
|
||||||
'02-Guides/01-SubGuides': {
|
'02-Guides/01-SubGuides': {
|
||||||
label: 'SubGuides (metadata file label)',
|
label: 'SubGuides (metadata file label)',
|
||||||
|
key: 'SubGuides-category-unique-key',
|
||||||
link: {
|
link: {
|
||||||
type: 'generated-index',
|
type: 'generated-index',
|
||||||
slug: 'subGuides-generated-index-slug',
|
slug: 'subGuides-generated-index-slug',
|
||||||
|
|
|
@ -29,10 +29,12 @@ describe('validateSidebars', () => {
|
||||||
it('accept valid values', () => {
|
it('accept valid values', () => {
|
||||||
const sidebars: SidebarsConfig = {
|
const sidebars: SidebarsConfig = {
|
||||||
sidebar1: [
|
sidebar1: [
|
||||||
{type: 'doc', id: 'doc1'},
|
{type: 'doc', id: 'doc1', key: 'key-doc1'},
|
||||||
{type: 'doc', id: 'doc2'},
|
{type: 'doc', id: 'doc2'},
|
||||||
|
{type: 'ref', id: 'doc2', key: 'ref-doc2'},
|
||||||
{
|
{
|
||||||
type: 'category',
|
type: 'category',
|
||||||
|
key: 'key-cat',
|
||||||
label: 'Category',
|
label: 'Category',
|
||||||
items: [{type: 'doc', id: 'doc3'}],
|
items: [{type: 'doc', id: 'doc3'}],
|
||||||
},
|
},
|
||||||
|
@ -68,6 +70,33 @@ describe('validateSidebars', () => {
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('sidebar category wrong key', () => {
|
||||||
|
expect(() =>
|
||||||
|
validateSidebars({
|
||||||
|
docs: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
key: '',
|
||||||
|
items: [{type: 'doc', id: 'doc1'}],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
"{
|
||||||
|
"type": "category",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "doc",
|
||||||
|
"id": "doc1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"key" [1]: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
[1] "key" is not allowed to be empty"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
it('sidebars link wrong label', () => {
|
it('sidebars link wrong label', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
validateSidebars({
|
validateSidebars({
|
||||||
|
@ -90,6 +119,28 @@ describe('validateSidebars', () => {
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('sidebars link wrong key', () => {
|
||||||
|
expect(() =>
|
||||||
|
validateSidebars({
|
||||||
|
docs: [
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
key: false,
|
||||||
|
href: 'https://github.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
"{
|
||||||
|
"type": "link",
|
||||||
|
"href": "https://github.com",
|
||||||
|
"key" [1]: false
|
||||||
|
}
|
||||||
|
|
||||||
|
[1] "key" must be a string"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
it('sidebars link wrong href', () => {
|
it('sidebars link wrong href', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
validateSidebars({
|
validateSidebars({
|
||||||
|
@ -188,6 +239,35 @@ describe('validateSidebars', () => {
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('sidebars category wrong key', () => {
|
||||||
|
expect(() =>
|
||||||
|
validateSidebars({
|
||||||
|
docs: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'category',
|
||||||
|
key: 42,
|
||||||
|
items: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
type: 'ref',
|
||||||
|
id: 'hello',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
"{
|
||||||
|
"type": "category",
|
||||||
|
"label": "category",
|
||||||
|
"items": [],
|
||||||
|
"key" [1]: 42
|
||||||
|
}
|
||||||
|
|
||||||
|
[1] "key" must be a string"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
it('sidebar category wrong items', () => {
|
it('sidebar category wrong items', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
validateSidebars({
|
validateSidebars({
|
||||||
|
@ -230,8 +310,8 @@ describe('validateSidebars', () => {
|
||||||
const sidebars: SidebarsConfig = {
|
const sidebars: SidebarsConfig = {
|
||||||
sidebar1: [
|
sidebar1: [
|
||||||
{
|
{
|
||||||
// @ts-expect-error - test missing value
|
|
||||||
type: 'html',
|
type: 'html',
|
||||||
|
value: undefined,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@ -251,6 +331,7 @@ describe('validateSidebars', () => {
|
||||||
sidebar1: [
|
sidebar1: [
|
||||||
{
|
{
|
||||||
type: 'html',
|
type: 'html',
|
||||||
|
key: 'html-key',
|
||||||
value: '<p>Hello, World!</p>',
|
value: '<p>Hello, World!</p>',
|
||||||
defaultStyle: true,
|
defaultStyle: true,
|
||||||
className: 'foo',
|
className: 'foo',
|
||||||
|
@ -262,8 +343,6 @@ describe('validateSidebars', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('validateCategoryMetadataFile', () => {
|
describe('validateCategoryMetadataFile', () => {
|
||||||
// TODO add more tests
|
|
||||||
|
|
||||||
it('throw for bad value', () => {
|
it('throw for bad value', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
validateCategoryMetadataFile(42),
|
validateCategoryMetadataFile(42),
|
||||||
|
@ -279,6 +358,7 @@ describe('validateCategoryMetadataFile', () => {
|
||||||
const content: CategoryMetadataFile = {
|
const content: CategoryMetadataFile = {
|
||||||
className: 'className',
|
className: 'className',
|
||||||
label: 'Category Label',
|
label: 'Category Label',
|
||||||
|
key: 'category-key',
|
||||||
description: 'Category Description',
|
description: 'Category Description',
|
||||||
link: {
|
link: {
|
||||||
type: 'generated-index',
|
type: 'generated-index',
|
||||||
|
@ -293,24 +373,70 @@ describe('validateCategoryMetadataFile', () => {
|
||||||
expect(validateCategoryMetadataFile(content)).toEqual(content);
|
expect(validateCategoryMetadataFile(content)).toEqual(content);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects permalink', () => {
|
describe('label', () => {
|
||||||
const content: CategoryMetadataFile = {
|
it('accepts valid label', () => {
|
||||||
className: 'className',
|
const content: CategoryMetadataFile = {label: 'Category label'};
|
||||||
label: 'Category Label',
|
expect(validateCategoryMetadataFile(content)).toEqual(content);
|
||||||
link: {
|
});
|
||||||
type: 'generated-index',
|
|
||||||
slug: 'slug',
|
it('throws for number label', () => {
|
||||||
// @ts-expect-error: rejected on purpose
|
expect(() =>
|
||||||
permalink: 'somePermalink',
|
validateCategoryMetadataFile({label: 42}),
|
||||||
title: 'title',
|
).toThrowErrorMatchingInlineSnapshot(`""label" must be a string"`);
|
||||||
description: 'description',
|
});
|
||||||
},
|
});
|
||||||
collapsible: true,
|
|
||||||
collapsed: true,
|
describe('key', () => {
|
||||||
position: 3,
|
it('accepts valid key', () => {
|
||||||
};
|
const content: CategoryMetadataFile = {key: 'Category key'};
|
||||||
expect(() =>
|
expect(validateCategoryMetadataFile(content)).toEqual(content);
|
||||||
validateCategoryMetadataFile(content),
|
});
|
||||||
).toThrowErrorMatchingInlineSnapshot(`""link.permalink" is not allowed"`);
|
|
||||||
|
it('throws for number key', () => {
|
||||||
|
expect(() =>
|
||||||
|
validateCategoryMetadataFile({key: 42}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(`""key" must be a string"`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('className', () => {
|
||||||
|
it('accepts valid className', () => {
|
||||||
|
const content: CategoryMetadataFile = {className: 'category-className'};
|
||||||
|
expect(validateCategoryMetadataFile(content)).toEqual(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws for number key', () => {
|
||||||
|
expect(() =>
|
||||||
|
validateCategoryMetadataFile({className: 42}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(`""className" must be a string"`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('link', () => {
|
||||||
|
it('accepts valid link', () => {
|
||||||
|
const content: CategoryMetadataFile = {
|
||||||
|
link: {
|
||||||
|
type: 'generated-index',
|
||||||
|
slug: 'slug',
|
||||||
|
title: 'title',
|
||||||
|
description: 'desc',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(validateCategoryMetadataFile(content)).toEqual(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects link permalink', () => {
|
||||||
|
const content: CategoryMetadataFile = {
|
||||||
|
link: {
|
||||||
|
type: 'generated-index',
|
||||||
|
slug: 'slug',
|
||||||
|
// @ts-expect-error: rejected on purpose
|
||||||
|
permalink: 'somePermalink',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(() =>
|
||||||
|
validateCategoryMetadataFile(content),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(`""link.permalink" is not allowed"`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -252,6 +252,7 @@ Available doc IDs:
|
||||||
...(categoryMetadata?.description && {
|
...(categoryMetadata?.description && {
|
||||||
description: categoryMetadata?.description,
|
description: categoryMetadata?.description,
|
||||||
}),
|
}),
|
||||||
|
...(categoryMetadata?.key && {key: categoryMetadata?.key}),
|
||||||
...(link && {link}),
|
...(link && {link}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import type {Slugger} from '@docusaurus/utils';
|
||||||
type Expand<T extends {[x: string]: unknown}> = {[P in keyof T]: T[P]};
|
type Expand<T extends {[x: string]: unknown}> = {[P in keyof T]: T[P]};
|
||||||
|
|
||||||
export type SidebarItemBase = {
|
export type SidebarItemBase = {
|
||||||
|
key?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
customProps?: {[key: string]: unknown};
|
customProps?: {[key: string]: unknown};
|
||||||
};
|
};
|
||||||
|
@ -28,8 +29,9 @@ export type SidebarItemDoc = SidebarItemBase & {
|
||||||
label?: string;
|
label?: string;
|
||||||
id: string;
|
id: string;
|
||||||
/**
|
/**
|
||||||
* This is an internal marker. Items with labels defined in the config needs
|
* This is an internal marker set during the sidebar normalization process.
|
||||||
* to be translated with JSON
|
* Docs with labels defined in the config need to be translated with JSON.
|
||||||
|
* Otherwise, it's preferable to translate the MDX doc title or front matter.
|
||||||
*/
|
*/
|
||||||
translatable?: true;
|
translatable?: true;
|
||||||
};
|
};
|
||||||
|
@ -215,6 +217,7 @@ export type PropSidebarBreadcrumbsItem =
|
||||||
| PropSidebarItemCategory;
|
| PropSidebarItemCategory;
|
||||||
|
|
||||||
export type CategoryMetadataFile = {
|
export type CategoryMetadataFile = {
|
||||||
|
key?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
position?: number;
|
position?: number;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
|
@ -28,6 +28,7 @@ import type {
|
||||||
// in normalization
|
// in normalization
|
||||||
|
|
||||||
const sidebarItemBaseSchema = Joi.object<SidebarItemBase>({
|
const sidebarItemBaseSchema = Joi.object<SidebarItemBase>({
|
||||||
|
key: Joi.string(),
|
||||||
className: Joi.string(),
|
className: Joi.string(),
|
||||||
customProps: Joi.object().unknown(),
|
customProps: Joi.object().unknown(),
|
||||||
});
|
});
|
||||||
|
@ -166,6 +167,7 @@ export function validateSidebars(sidebars: {
|
||||||
}
|
}
|
||||||
|
|
||||||
const categoryMetadataFileSchema = Joi.object<CategoryMetadataFile>({
|
const categoryMetadataFileSchema = Joi.object<CategoryMetadataFile>({
|
||||||
|
key: Joi.string(),
|
||||||
label: Joi.string(),
|
label: Joi.string(),
|
||||||
description: Joi.string(),
|
description: Joi.string(),
|
||||||
position: Joi.number(),
|
position: Joi.number(),
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import {mergeTranslations} from '@docusaurus/utils';
|
import {mergeTranslations} from '@docusaurus/utils';
|
||||||
|
import logger from '@docusaurus/logger';
|
||||||
import {CURRENT_VERSION_NAME} from './constants';
|
import {CURRENT_VERSION_NAME} from './constants';
|
||||||
import {
|
import {
|
||||||
collectSidebarCategories,
|
collectSidebarCategories,
|
||||||
|
@ -40,20 +41,53 @@ function getVersionFileName(versionName: string): string {
|
||||||
return `version-${versionName}`;
|
return `version-${versionName}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TranslationMessageEntry = [string, TranslationMessage];
|
||||||
|
|
||||||
|
function ensureNoSidebarDuplicateEntries(
|
||||||
|
translationEntries: TranslationMessageEntry[],
|
||||||
|
): void {
|
||||||
|
const grouped = _.groupBy(translationEntries, (entry) => entry[0]);
|
||||||
|
const duplicates = Object.entries(grouped).filter(
|
||||||
|
(entry) => entry[1].length > 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (duplicates.length > 0) {
|
||||||
|
throw new Error(`Multiple docs sidebar items produce the same translation key.
|
||||||
|
- ${duplicates
|
||||||
|
.map(([translationKey, entries]) => {
|
||||||
|
return `${logger.code(translationKey)}: ${logger.num(
|
||||||
|
entries.length,
|
||||||
|
)} duplicates found:\n - ${entries
|
||||||
|
.map((duplicate) => {
|
||||||
|
const desc = duplicate[1].description;
|
||||||
|
return `${logger.name(duplicate[1].message)} ${
|
||||||
|
desc ? `(${logger.subdue(desc)})` : ''
|
||||||
|
}`;
|
||||||
|
})
|
||||||
|
.join('\n - ')}`;
|
||||||
|
})
|
||||||
|
.join('\n\n- ')}
|
||||||
|
|
||||||
|
To avoid translation key conflicts, use the ${logger.code(
|
||||||
|
'key',
|
||||||
|
)} attribute on the sidebar items above to uniquely identify them.
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getSidebarTranslationFileContent(
|
function getSidebarTranslationFileContent(
|
||||||
sidebar: Sidebar,
|
sidebar: Sidebar,
|
||||||
sidebarName: string,
|
sidebarName: string,
|
||||||
): TranslationFileContent {
|
): TranslationFileContent {
|
||||||
type TranslationMessageEntry = [string, TranslationMessage];
|
|
||||||
|
|
||||||
const categories = collectSidebarCategories(sidebar);
|
const categories = collectSidebarCategories(sidebar);
|
||||||
|
|
||||||
const categoryContent: TranslationFileContent = Object.fromEntries(
|
const categoryEntries: TranslationMessageEntry[] = categories.flatMap(
|
||||||
categories.flatMap((category) => {
|
(category) => {
|
||||||
const entries: TranslationMessageEntry[] = [];
|
const entries: TranslationMessageEntry[] = [];
|
||||||
|
const categoryKey = category.key ?? category.label;
|
||||||
|
|
||||||
entries.push([
|
entries.push([
|
||||||
`sidebar.${sidebarName}.category.${category.label}`,
|
`sidebar.${sidebarName}.category.${categoryKey}`,
|
||||||
{
|
{
|
||||||
message: category.label,
|
message: category.label,
|
||||||
description: `The label for category ${category.label} in sidebar ${sidebarName}`,
|
description: `The label for category ${category.label} in sidebar ${sidebarName}`,
|
||||||
|
@ -63,7 +97,7 @@ function getSidebarTranslationFileContent(
|
||||||
if (category.link?.type === 'generated-index') {
|
if (category.link?.type === 'generated-index') {
|
||||||
if (category.link.title) {
|
if (category.link.title) {
|
||||||
entries.push([
|
entries.push([
|
||||||
`sidebar.${sidebarName}.category.${category.label}.link.generated-index.title`,
|
`sidebar.${sidebarName}.category.${categoryKey}.link.generated-index.title`,
|
||||||
{
|
{
|
||||||
message: category.link.title,
|
message: category.link.title,
|
||||||
description: `The generated-index page title for category ${category.label} in sidebar ${sidebarName}`,
|
description: `The generated-index page title for category ${category.label} in sidebar ${sidebarName}`,
|
||||||
|
@ -72,7 +106,7 @@ function getSidebarTranslationFileContent(
|
||||||
}
|
}
|
||||||
if (category.link.description) {
|
if (category.link.description) {
|
||||||
entries.push([
|
entries.push([
|
||||||
`sidebar.${sidebarName}.category.${category.label}.link.generated-index.description`,
|
`sidebar.${sidebarName}.category.${categoryKey}.link.generated-index.description`,
|
||||||
{
|
{
|
||||||
message: category.link.description,
|
message: category.link.description,
|
||||||
description: `The generated-index page description for category ${category.label} in sidebar ${sidebarName}`,
|
description: `The generated-index page description for category ${category.label} in sidebar ${sidebarName}`,
|
||||||
|
@ -82,36 +116,40 @@ function getSidebarTranslationFileContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
return entries;
|
return entries;
|
||||||
}),
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const links = collectSidebarLinks(sidebar);
|
const links = collectSidebarLinks(sidebar);
|
||||||
const linksContent: TranslationFileContent = Object.fromEntries(
|
const linksEntries: TranslationMessageEntry[] = links.map((link) => {
|
||||||
links.map((link) => [
|
const linkKey = link.key ?? link.label;
|
||||||
`sidebar.${sidebarName}.link.${link.label}`,
|
return [
|
||||||
|
`sidebar.${sidebarName}.link.${linkKey}`,
|
||||||
{
|
{
|
||||||
message: link.label,
|
message: link.label,
|
||||||
description: `The label for link ${link.label} in sidebar ${sidebarName}, linking to ${link.href}`,
|
description: `The label for link ${link.label} in sidebar ${sidebarName}, linking to ${link.href}`,
|
||||||
},
|
},
|
||||||
]),
|
];
|
||||||
);
|
});
|
||||||
|
|
||||||
const docs = collectSidebarDocItems(sidebar)
|
const docs = collectSidebarDocItems(sidebar)
|
||||||
.concat(collectSidebarRefs(sidebar))
|
.concat(collectSidebarRefs(sidebar))
|
||||||
.filter((item) => item.translatable);
|
.filter((item) => item.translatable);
|
||||||
const docLinksContent: TranslationFileContent = Object.fromEntries(
|
const docLinksEntries: TranslationMessageEntry[] = docs.map((doc) => {
|
||||||
docs.map((doc) => [
|
const docKey = doc.key ?? doc.label!;
|
||||||
`sidebar.${sidebarName}.doc.${doc.label!}`,
|
return [
|
||||||
|
`sidebar.${sidebarName}.doc.${docKey}`,
|
||||||
{
|
{
|
||||||
message: doc.label!,
|
message: doc.label!,
|
||||||
description: `The label for the doc item ${doc.label!} in sidebar ${sidebarName}, linking to the doc ${
|
description: `The label for the doc item ${doc.label!} in sidebar ${sidebarName}, linking to the doc ${
|
||||||
doc.id
|
doc.id
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
]),
|
];
|
||||||
);
|
});
|
||||||
|
|
||||||
return mergeTranslations([categoryContent, linksContent, docLinksContent]);
|
const allEntries = [...categoryEntries, ...linksEntries, ...docLinksEntries];
|
||||||
|
ensureNoSidebarDuplicateEntries(allEntries);
|
||||||
|
return Object.fromEntries(allEntries);
|
||||||
}
|
}
|
||||||
|
|
||||||
function translateSidebar({
|
function translateSidebar({
|
||||||
|
@ -150,27 +188,30 @@ function translateSidebar({
|
||||||
return transformSidebarItems(sidebar, (item) => {
|
return transformSidebarItems(sidebar, (item) => {
|
||||||
if (item.type === 'category') {
|
if (item.type === 'category') {
|
||||||
const link = transformSidebarCategoryLink(item);
|
const link = transformSidebarCategoryLink(item);
|
||||||
|
const categoryKey = item.key ?? item.label;
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
label:
|
label:
|
||||||
sidebarsTranslations[`sidebar.${sidebarName}.category.${item.label}`]
|
sidebarsTranslations[`sidebar.${sidebarName}.category.${categoryKey}`]
|
||||||
?.message ?? item.label,
|
?.message ?? item.label,
|
||||||
...(link && {link}),
|
...(link && {link}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (item.type === 'link') {
|
if (item.type === 'link') {
|
||||||
|
const linkKey = item.key ?? item.label;
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
label:
|
label:
|
||||||
sidebarsTranslations[`sidebar.${sidebarName}.link.${item.label}`]
|
sidebarsTranslations[`sidebar.${sidebarName}.link.${linkKey}`]
|
||||||
?.message ?? item.label,
|
?.message ?? item.label,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if ((item.type === 'doc' || item.type === 'ref') && item.translatable) {
|
if ((item.type === 'doc' || item.type === 'ref') && item.translatable) {
|
||||||
|
const docKey = item.key ?? item.label!;
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
label:
|
label:
|
||||||
sidebarsTranslations[`sidebar.${sidebarName}.doc.${item.label!}`]
|
sidebarsTranslations[`sidebar.${sidebarName}.doc.${docKey}`]
|
||||||
?.message ?? item.label,
|
?.message ?? item.label,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"label": "Conflicts",
|
||||||
|
"link": {
|
||||||
|
"type": "generated-index",
|
||||||
|
"title": "Conflicts",
|
||||||
|
"description": "Testing what happens when docs use similar names"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"label": "Category Index name conflict",
|
||||||
|
"link": {
|
||||||
|
"type": "generated-index",
|
||||||
|
"title": "Category Index name conflict",
|
||||||
|
"description": "Testing what happens when 2 category index have the same name"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"label": "Alpha",
|
||||||
|
"link": {
|
||||||
|
"type": "generated-index"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"key": "CategoryLabelTest alpha",
|
||||||
|
"label": "CategoryLabelTest",
|
||||||
|
"link": {
|
||||||
|
"type": "generated-index",
|
||||||
|
"title": "test category in Alpha",
|
||||||
|
"description": "Test description in Alpha"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"label": "Beta",
|
||||||
|
"link": {
|
||||||
|
"type": "generated-index"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"key": "CategoryLabelTest beta",
|
||||||
|
"label": "CategoryLabelTest",
|
||||||
|
"link": {
|
||||||
|
"type": "generated-index",
|
||||||
|
"title": "test category in Beta",
|
||||||
|
"description": "Test description in Beta"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test file
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
sidebar_label: Doc sidebar label
|
||||||
|
---
|
||||||
|
|
||||||
|
# Doc 1
|
||||||
|
|
||||||
|
Doc 1
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
sidebar_label: Doc sidebar label
|
||||||
|
---
|
||||||
|
|
||||||
|
# Doc 2
|
||||||
|
|
||||||
|
Doc 2
|
|
@ -8,6 +8,7 @@
|
||||||
/**
|
/**
|
||||||
* @typedef {import('@docusaurus/plugin-content-docs').SidebarsConfig} SidebarsConfig
|
* @typedef {import('@docusaurus/plugin-content-docs').SidebarsConfig} SidebarsConfig
|
||||||
* @typedef {import('@docusaurus/plugin-content-docs/lib/sidebars/types').SidebarItemConfig} SidebarItemConfig
|
* @typedef {import('@docusaurus/plugin-content-docs/lib/sidebars/types').SidebarItemConfig} SidebarItemConfig
|
||||||
|
* @typedef {import('@docusaurus/plugin-content-docs/lib/sidebars/types').SidebarItemCategoryConfig} SidebarItemCategoryConfig
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** @type {SidebarsConfig} */
|
/** @type {SidebarsConfig} */
|
||||||
|
@ -63,7 +64,8 @@ const sidebars = {
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
type: 'link',
|
type: 'link',
|
||||||
label: 'Link ',
|
label: 'Link',
|
||||||
|
key: 'link-key-1',
|
||||||
href: 'https://docusaurus.io',
|
href: 'https://docusaurus.io',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -76,6 +78,7 @@ const sidebars = {
|
||||||
{
|
{
|
||||||
type: 'link',
|
type: 'link',
|
||||||
label: 'Link ',
|
label: 'Link ',
|
||||||
|
key: 'link-key-2',
|
||||||
href: 'https://docusaurus.io',
|
href: 'https://docusaurus.io',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -182,14 +185,16 @@ function generateHugeSidebarItems() {
|
||||||
/**
|
/**
|
||||||
* @param {number} maxLevel
|
* @param {number} maxLevel
|
||||||
* @param {number} currentLevel
|
* @param {number} currentLevel
|
||||||
|
* @param {string} parentKey
|
||||||
* @returns {SidebarItemConfig[]}
|
* @returns {SidebarItemConfig[]}
|
||||||
*/
|
*/
|
||||||
function generateRecursive(maxLevel, currentLevel = 0) {
|
function generateRecursive(maxLevel, currentLevel = 0, parentKey = 'ROOT') {
|
||||||
if (currentLevel === maxLevel) {
|
if (currentLevel === maxLevel) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
type: 'link',
|
type: 'link',
|
||||||
href: '/',
|
href: '/',
|
||||||
|
key: `link-${parentKey}-maxLevel`,
|
||||||
label: `Link (level ${currentLevel + 1})`,
|
label: `Link (level ${currentLevel + 1})`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -198,14 +203,23 @@ function generateHugeSidebarItems() {
|
||||||
const linkItems = Array.from(Array(linksCount).keys()).map((index) => ({
|
const linkItems = Array.from(Array(linksCount).keys()).map((index) => ({
|
||||||
type: 'link',
|
type: 'link',
|
||||||
href: '/',
|
href: '/',
|
||||||
|
key: `link-${parentKey}-${index}`,
|
||||||
label: `Link ${index} (level ${currentLevel + 1})`,
|
label: `Link ${index} (level ${currentLevel + 1})`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const categoryItems = Array.from(Array(categoriesCount).keys()).map(
|
const categoryItems = Array.from(Array(categoriesCount).keys()).map(
|
||||||
|
/**
|
||||||
|
* @returns {SidebarItemCategoryConfig}
|
||||||
|
*/
|
||||||
(index) => ({
|
(index) => ({
|
||||||
type: 'category',
|
type: 'category',
|
||||||
label: `Category ${index} (level ${currentLevel + 1})`,
|
label: `Category ${index} (level ${currentLevel + 1})`,
|
||||||
items: generateRecursive(maxLevel, currentLevel + 1),
|
key: `category-${parentKey}-${index}`,
|
||||||
|
items: generateRecursive(
|
||||||
|
maxLevel,
|
||||||
|
currentLevel + 1,
|
||||||
|
`${parentKey}-${index}`,
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,8 @@ slug: /sidebar
|
||||||
|
|
||||||
Creating a sidebar is useful to:
|
Creating a sidebar is useful to:
|
||||||
|
|
||||||
- Group multiple **related documents**
|
- Group multiple **related documents** into an ordered tree
|
||||||
- **Display a sidebar** on each of those documents
|
- **Display a common sidebar** on each of those documents
|
||||||
- Provide **paginated navigation**, with next/previous button
|
- Provide **paginated navigation**, with next/previous button
|
||||||
|
|
||||||
To use sidebars on your Docusaurus site:
|
To use sidebars on your Docusaurus site:
|
||||||
|
@ -160,6 +160,20 @@ export default {
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Passing CSS classes {#passing-css-classes}
|
||||||
|
|
||||||
|
To pass CSS classes to a sidebar item, add the optional `className` attribute to any of the items. This is useful to apply visual customizations to specific sidebar items.
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
type: 'doc',
|
||||||
|
id: 'doc1',
|
||||||
|
// highlight-start
|
||||||
|
className: 'sidebar-item--highlighted',
|
||||||
|
// highlight-end
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
## Passing custom props {#passing-custom-props}
|
## Passing custom props {#passing-custom-props}
|
||||||
|
|
||||||
To pass in custom props to a sidebar item, add the optional `customProps` object to any of the items. This is useful to apply site customizations by swizzling React components rendering sidebar items.
|
To pass in custom props to a sidebar item, add the optional `customProps` object to any of the items. This is useful to apply site customizations by swizzling React components rendering sidebar items.
|
||||||
|
@ -177,6 +191,28 @@ To pass in custom props to a sidebar item, add the optional `customProps` object
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Passing a unique key {#passing-custom-props}
|
||||||
|
|
||||||
|
Passing a unique `key` attribute can help uniquely identify a sidebar item. Sometimes other attributes (such as `label`) are not enough to distinguish two sidebar items from each other.
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
// highlight-start
|
||||||
|
label: 'API', // You may have multiple categories with this widespread label
|
||||||
|
key: 'api-for-feature-1', // and now, they can be uniquely identified
|
||||||
|
// highlight-end
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
:::info How is this useful?
|
||||||
|
|
||||||
|
Docusaurus only uses the `key` attribute to generate unique i18n translation keys. When a translation key conflict happens ([issue](https://github.com/facebook/docusaurus/issues/10913)), Docusaurus will tell you to apply a `key` to distinguish sidebar items.
|
||||||
|
|
||||||
|
Alternatively, you may have your own reasons for using the `key` attribute that will be passed to the respective sidebar item React components.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
## Sidebar Breadcrumbs {#sidebar-breadcrumbs}
|
## Sidebar Breadcrumbs {#sidebar-breadcrumbs}
|
||||||
|
|
||||||
By default, breadcrumbs are rendered at the top, using the "sidebar path" of the current page.
|
By default, breadcrumbs are rendered at the top, using the "sidebar path" of the current page.
|
||||||
|
|
|
@ -11,14 +11,14 @@ import TabItem from '@theme/TabItem';
|
||||||
import BrowserWindow from '@site/src/components/BrowserWindow';
|
import BrowserWindow from '@site/src/components/BrowserWindow';
|
||||||
```
|
```
|
||||||
|
|
||||||
We have introduced three types of item types in the example in the previous section: `doc`, `category`, and `link`, whose usages are fairly intuitive. We will formally introduce their APIs. There's also a fourth type: `autogenerated`, which we will explain in detail later.
|
The sidebar supports various item types:
|
||||||
|
|
||||||
- **[Doc](#sidebar-item-doc)**: link to a doc page, associating it with the sidebar
|
- **[Doc](#sidebar-item-doc)**: link to a doc page, associating it with the sidebar
|
||||||
- **[Link](#sidebar-item-link)**: link to any internal or external page
|
- **[Link](#sidebar-item-link)**: link to any internal or external page
|
||||||
- **[Category](#sidebar-item-category)**: creates a dropdown of sidebar items
|
- **[Category](#sidebar-item-category)**: creates a dropdown of sidebar items
|
||||||
- **[Autogenerated](autogenerated.mdx)**: generate a sidebar slice automatically
|
- **[Autogenerated](autogenerated.mdx)**: generate a sidebar slice automatically
|
||||||
- **[HTML](#sidebar-item-html)**: renders pure HTML in the item's position
|
- **[HTML](#sidebar-item-html)**: renders pure HTML in the item's position
|
||||||
- **[\*Ref](multiple-sidebars.mdx#sidebar-item-ref)**: link to a doc page, without making the item take part in navigation generation
|
- **[Ref](multiple-sidebars.mdx#sidebar-item-ref)**: link to a doc page, without making the item take part in navigation generation
|
||||||
|
|
||||||
## Doc: link to a doc {#sidebar-item-doc}
|
## Doc: link to a doc {#sidebar-item-doc}
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ type SidebarItemDoc =
|
||||||
type: 'doc';
|
type: 'doc';
|
||||||
id: string;
|
id: string;
|
||||||
label: string; // Sidebar label text
|
label: string; // Sidebar label text
|
||||||
|
key?: string; // Sidebar key to uniquely identify the item
|
||||||
className?: string; // Class name for sidebar label
|
className?: string; // Class name for sidebar label
|
||||||
customProps?: Record<string, unknown>; // Custom props
|
customProps?: Record<string, unknown>; // Custom props
|
||||||
}
|
}
|
||||||
|
@ -84,8 +85,10 @@ type SidebarItemLink = {
|
||||||
type: 'link';
|
type: 'link';
|
||||||
label: string;
|
label: string;
|
||||||
href: string;
|
href: string;
|
||||||
className?: string;
|
|
||||||
description?: string;
|
description?: string;
|
||||||
|
key?: string;
|
||||||
|
className?: string;
|
||||||
|
customProps?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -126,7 +129,9 @@ type SidebarItemHtml = {
|
||||||
type: 'html';
|
type: 'html';
|
||||||
value: string;
|
value: string;
|
||||||
defaultStyle?: boolean; // Use default menu item styles
|
defaultStyle?: boolean; // Use default menu item styles
|
||||||
|
key?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
customProps?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -173,8 +178,10 @@ type SidebarItemCategory = {
|
||||||
type: 'category';
|
type: 'category';
|
||||||
label: string; // Sidebar label text.
|
label: string; // Sidebar label text.
|
||||||
items: SidebarItem[]; // Array of sidebar items.
|
items: SidebarItem[]; // Array of sidebar items.
|
||||||
className?: string;
|
|
||||||
description?: string;
|
description?: string;
|
||||||
|
key?: string;
|
||||||
|
className?: string;
|
||||||
|
customProps?: Record<string, unknown>;
|
||||||
|
|
||||||
// Category options:
|
// Category options:
|
||||||
collapsible: boolean; // Set the category to be collapsible
|
collapsible: boolean; // Set the category to be collapsible
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue