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 35d26aa8a5..0d601a5d24 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 @@ -1491,6 +1491,7 @@ Object { "unversionedId": "installation", }, ], + "isCategoryIndex": [Function], "item": Object { "dirName": ".", "type": "autogenerated", diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts index a5cd7c5c31..f00525b88e 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts @@ -12,7 +12,7 @@ import { readVersionDocs, readDocFile, addDocNavigation, - isConventionalDocIndex, + isCategoryIndex, } from '../docs'; import {loadSidebars} from '../sidebars'; import {readVersionsMetadata} from '../versions'; @@ -938,108 +938,124 @@ describe('versioned site', () => { describe('isConventionalDocIndex', () => { test('supports readme', () => { expect( - isConventionalDocIndex({ - sourceDirName: 'doesNotMatter', - source: 'readme.md', + isCategoryIndex({ + fileName: 'readme', + directories: ['doesNotMatter'], + extension: '.md', }), ).toEqual(true); expect( - isConventionalDocIndex({ - sourceDirName: 'doesNotMatter', - source: 'readme.mdx', + isCategoryIndex({ + fileName: 'readme', + directories: ['doesNotMatter'], + extension: '.mdx', }), ).toEqual(true); expect( - isConventionalDocIndex({ - sourceDirName: 'doesNotMatter', - source: 'README.md', + isCategoryIndex({ + fileName: 'README', + directories: ['doesNotMatter'], + extension: '.md', }), ).toEqual(true); expect( - isConventionalDocIndex({ - sourceDirName: 'doesNotMatter', - source: 'parent/ReAdMe', + isCategoryIndex({ + fileName: 'ReAdMe', + directories: ['doesNotMatter'], + extension: '', }), ).toEqual(true); }); test('supports index', () => { expect( - isConventionalDocIndex({ - sourceDirName: 'doesNotMatter', - source: 'index.md', + isCategoryIndex({ + fileName: 'index', + directories: ['doesNotMatter'], + extension: '.md', }), ).toEqual(true); expect( - isConventionalDocIndex({ - sourceDirName: 'doesNotMatter', - source: 'index.mdx', + isCategoryIndex({ + fileName: 'index', + directories: ['doesNotMatter'], + extension: '.mdx', }), ).toEqual(true); expect( - isConventionalDocIndex({ - sourceDirName: 'doesNotMatter', - source: 'INDEX.md', + isCategoryIndex({ + fileName: 'INDEX', + directories: ['doesNotMatter'], + extension: '.md', }), ).toEqual(true); expect( - isConventionalDocIndex({ - sourceDirName: 'doesNotMatter', - source: 'parent/InDeX', + isCategoryIndex({ + fileName: 'InDeX', + directories: ['doesNotMatter'], + extension: '', }), ).toEqual(true); }); test('supports /.md', () => { expect( - isConventionalDocIndex({ - sourceDirName: 'someCategory', - source: 'someCategory', + isCategoryIndex({ + fileName: 'someCategory', + directories: ['someCategory', 'doesNotMatter'], + extension: '', }), ).toEqual(true); expect( - isConventionalDocIndex({ - sourceDirName: 'someCategory', - source: 'someCategory.md', + isCategoryIndex({ + fileName: 'someCategory', + directories: ['someCategory'], + extension: '.md', }), ).toEqual(true); expect( - isConventionalDocIndex({ - sourceDirName: 'someCategory', - source: 'someCategory.mdx', + isCategoryIndex({ + fileName: 'someCategory', + directories: ['someCategory'], + extension: '.mdx', }), ).toEqual(true); expect( - isConventionalDocIndex({ - sourceDirName: 'some_category', - source: 'SOME_CATEGORY.md', + isCategoryIndex({ + fileName: 'SOME_CATEGORY', + directories: ['some_category'], + extension: '.md', }), ).toEqual(true); expect( - isConventionalDocIndex({ - sourceDirName: 'some_category', - source: 'parent/some_category', + isCategoryIndex({ + fileName: 'some_category', + directories: ['some_category'], + extension: '', }), ).toEqual(true); }); test('reject other cases', () => { expect( - isConventionalDocIndex({ - sourceDirName: 'someCategory', - source: 'some_Category', + isCategoryIndex({ + fileName: 'some_Category', + directories: ['someCategory'], + extension: '', }), ).toEqual(false); expect( - isConventionalDocIndex({ - sourceDirName: 'doesNotMatter', - source: 'read_me', + isCategoryIndex({ + fileName: 'read_me', + directories: ['doesNotMatter'], + extension: '', }), ).toEqual(false); expect( - isConventionalDocIndex({ - sourceDirName: 'doesNotMatter', - source: 'the index', + isCategoryIndex({ + fileName: 'the index', + directories: ['doesNotMatter'], + extension: '', }), ).toEqual(false); }); diff --git a/packages/docusaurus-plugin-content-docs/src/docs.ts b/packages/docusaurus-plugin-content-docs/src/docs.ts index 9925156250..9002479b9a 100644 --- a/packages/docusaurus-plugin-content-docs/src/docs.ts +++ b/packages/docusaurus-plugin-content-docs/src/docs.ts @@ -8,7 +8,7 @@ import path from 'path'; import fs from 'fs-extra'; import logger from '@docusaurus/logger'; -import {keyBy, last} from 'lodash'; +import {keyBy} from 'lodash'; import { aliasedSitePath, getEditUrl, @@ -41,6 +41,8 @@ import {toDocNavigationLink, toNavigationLink} from './sidebars/utils'; import type { MetadataOptions, PluginOptions, + CategoryIndexMatcher, + CategoryIndexMatcherParam, } from '@docusaurus/plugin-content-docs'; type LastUpdateOptions = Pick< @@ -367,31 +369,62 @@ export function getMainDocId({ return getMainDoc().unversionedId; } -function getLastPathSegment(str: string): string { - return last(str.split('/'))!; -} - // By convention, Docusaurus considers some docs are "indexes": // - index.md // - readme.md // - /.md // +// This function is the default implementation of this convention +// // Those index docs produce a different behavior // - Slugs do not end with a weird "/index" suffix // - Auto-generated sidebar categories link to them as intro -export function isConventionalDocIndex(doc: { - source: DocMetadataBase['slug']; - sourceDirName: DocMetadataBase['sourceDirName']; -}): boolean { - // "@site/docs/folder/subFolder/subSubFolder/myDoc.md" => "myDoc" - const docName = path.parse(doc.source).name; +export const isCategoryIndex: CategoryIndexMatcher = ({ + fileName, + directories, +}): boolean => { + const eligibleDocIndexNames = [ + 'index', + 'readme', + directories[0]?.toLowerCase(), + ]; + return eligibleDocIndexNames.includes(fileName.toLowerCase()); +}; - // "folder/subFolder/subSubFolder" => "subSubFolder" - const lastDirName = getLastPathSegment(doc.sourceDirName); +export function toCategoryIndexMatcherParam({ + source, + sourceDirName, +}: Pick< + DocMetadataBase, + 'source' | 'sourceDirName' +>): CategoryIndexMatcherParam { + // source + sourceDirName are always posix-style + return { + fileName: path.posix.parse(source).name, + extension: path.posix.parse(source).ext, + directories: sourceDirName.split(path.posix.sep).reverse(), + }; +} - const eligibleDocIndexNames = ['index', 'readme', lastDirName.toLowerCase()]; - - return eligibleDocIndexNames.includes(docName.toLowerCase()); +/** + * guides/sidebar/autogenerated.md -> 'autogenerated', '.md', ['sidebar', 'guides'] + */ +export function splitPath(str: string): { + /** + * The list of directories, from lowest level to highest. + * If there's no dir name, directories is ['.'] + */ + directories: string[]; + /** The file name, without extension */ + fileName: string; + /** The extension, with a leading dot */ + extension: string; +} { + return { + fileName: path.parse(str).name, + extension: path.parse(str).ext, + directories: path.dirname(str).split(path.sep).reverse(), + }; } // Return both doc ids diff --git a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts index 130de3b2c9..59f54a69c7 100644 --- a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts +++ b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts @@ -13,6 +13,15 @@ declare module '@docusaurus/plugin-content-docs' { numberPrefix?: number; }; + export type CategoryIndexMatcherParam = { + fileName: string; + directories: string[]; + extension: string; + }; + export type CategoryIndexMatcher = ( + param: CategoryIndexMatcherParam, + ) => boolean; + export type EditUrlFunction = (editUrlParams: { version: string; versionDocsDirPath: string; diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/generator.test.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/generator.test.ts index defbe97a2c..67e7073294 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/generator.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/generator.test.ts @@ -12,6 +12,7 @@ import { import type {Sidebar, SidebarItemsGenerator} from '../types'; import fs from 'fs-extra'; import {DefaultNumberPrefixParser} from '../../numberPrefix'; +import {isCategoryIndex} from '../../docs'; describe('DefaultSidebarItemsGenerator', () => { function testDefaultSidebarItemsGenerator( @@ -19,6 +20,7 @@ describe('DefaultSidebarItemsGenerator', () => { ) { return DefaultSidebarItemsGenerator({ numberPrefixParser: DefaultNumberPrefixParser, + isCategoryIndex, item: { type: 'autogenerated', dirName: '.', @@ -146,6 +148,7 @@ describe('DefaultSidebarItemsGenerator', () => { const sidebarSlice = await DefaultSidebarItemsGenerator({ numberPrefixParser: DefaultNumberPrefixParser, + isCategoryIndex, item: { type: 'autogenerated', dirName: '.', @@ -157,48 +160,48 @@ describe('DefaultSidebarItemsGenerator', () => { docs: [ { id: 'intro', - source: 'intro.md', + source: '@site/docs/intro.md', sourceDirName: '.', sidebarPosition: 1, frontMatter: {}, }, { id: 'tutorials-index', - source: 'index.md', + source: '@site/docs/01-Tutorials/index.md', sourceDirName: '01-Tutorials', sidebarPosition: 2, frontMatter: {}, }, { id: 'tutorial2', - source: 'tutorial2.md', + source: '@site/docs/01-Tutorials/tutorial2.md', sourceDirName: '01-Tutorials', sidebarPosition: 2, frontMatter: {}, }, { id: 'tutorial1', - source: 'tutorial1.md', + source: '@site/docs/01-Tutorials/tutorial1.md', sourceDirName: '01-Tutorials', sidebarPosition: 1, frontMatter: {}, }, { id: 'guides-index', - source: '02-Guides.md', // TODO should we allow to just use "Guides.md" to have an index? + source: '@site/docs/02-Guides/02-Guides.md', // TODO should we allow to just use "Guides.md" to have an index? sourceDirName: '02-Guides', frontMatter: {}, }, { id: 'guide2', - source: 'guide2.md', + source: '@site/docs/02-Guides/guide2.md', sourceDirName: '02-Guides', sidebarPosition: 2, frontMatter: {}, }, { id: 'guide1', - source: 'guide1.md', + source: '@site/docs/02-Guides/guide1.md', sourceDirName: '02-Guides', sidebarPosition: 1, frontMatter: { @@ -207,14 +210,14 @@ describe('DefaultSidebarItemsGenerator', () => { }, { id: 'nested-guide', - source: 'nested-guide.md', + source: '@site/docs/02-Guides/01-SubGuides/nested-guide.md', sourceDirName: '02-Guides/01-SubGuides', sidebarPosition: undefined, frontMatter: {}, }, { id: 'end', - source: 'end.md', + source: '@site/docs/end.md', sourceDirName: '.', sidebarPosition: 3, frontMatter: {}, @@ -296,6 +299,7 @@ describe('DefaultSidebarItemsGenerator', () => { const sidebarSlice = await DefaultSidebarItemsGenerator({ numberPrefixParser: DefaultNumberPrefixParser, + isCategoryIndex, item: { type: 'autogenerated', dirName: 'subfolder/subsubfolder', @@ -427,19 +431,19 @@ describe('DefaultSidebarItemsGenerator', () => { docs: [ { id: 'parent/doc1', - source: 'index.md', + source: '@site/docs/Category/index.md', sourceDirName: 'Category', frontMatter: {}, }, { id: 'parent/doc2', - source: 'index.md', + source: '@site/docs/Category/index.md', sourceDirName: 'Category', frontMatter: {}, }, { id: 'parent/doc3', - source: 'doc3.md', + source: '@site/docs/Category/doc3.md', sourceDirName: 'Category', frontMatter: {}, }, @@ -473,4 +477,116 @@ describe('DefaultSidebarItemsGenerator', () => { }, ] as Sidebar); }); + + test('respects custom isCategoryIndex', async () => { + const sidebarSlice = await DefaultSidebarItemsGenerator({ + numberPrefixParser: DefaultNumberPrefixParser, + isCategoryIndex({fileName, directories}) { + return ( + fileName.replace( + `${DefaultNumberPrefixParser( + directories[0], + ).filename.toLowerCase()}-`, + '', + ) === 'index' + ); + }, + item: { + type: 'autogenerated', + dirName: '.', + }, + version: { + versionName: 'current', + contentPath: '', + }, + docs: [ + { + id: 'intro', + source: '@site/docs/intro.md', + sourceDirName: '.', + sidebarPosition: 1, + frontMatter: {}, + }, + { + id: 'tutorials-index', + source: '@site/docs/01-Tutorials/tutorials-index.md', + sourceDirName: '01-Tutorials', + sidebarPosition: 2, + frontMatter: {}, + }, + { + id: 'tutorial2', + source: '@site/docs/01-Tutorials/tutorial2.md', + sourceDirName: '01-Tutorials', + sidebarPosition: 2, + frontMatter: {}, + }, + { + id: 'tutorial1', + source: '@site/docs/01-Tutorials/tutorial1.md', + sourceDirName: '01-Tutorials', + sidebarPosition: 1, + frontMatter: {}, + }, + { + id: 'not-guides-index', + source: '@site/docs/02-Guides/README.md', + sourceDirName: '02-Guides', + frontMatter: {}, + }, + { + id: 'guide2', + source: '@site/docs/02-Guides/guide2.md', + sourceDirName: '02-Guides', + sidebarPosition: 2, + frontMatter: {}, + }, + { + id: 'guide1', + source: '@site/docs/02-Guides/guide1.md', + sourceDirName: '02-Guides', + sidebarPosition: 1, + frontMatter: { + sidebar_class_name: 'foo', + }, + }, + ], + options: { + sidebarCollapsed: true, + sidebarCollapsible: true, + }, + }); + + expect(sidebarSlice).toEqual([ + {type: 'doc', id: 'intro'}, + { + type: 'category', + label: 'Tutorials', + collapsed: true, + collapsible: true, + link: { + type: 'doc', + id: 'tutorials-index', + }, + items: [ + {type: 'doc', id: 'tutorial1'}, + {type: 'doc', id: 'tutorial2'}, + ], + }, + { + type: 'category', + label: 'Guides', + collapsed: true, + collapsible: true, + items: [ + {type: 'doc', id: 'guide1', className: 'foo'}, + {type: 'doc', id: 'guide2'}, + { + type: 'doc', + id: 'not-guides-index', + }, + ], + }, + ] as Sidebar); + }); }); diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/processor.test.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/processor.test.ts index b7caeae4de..3341d8c399 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/processor.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/processor.test.ts @@ -16,6 +16,7 @@ import {DefaultSidebarItemsGenerator} from '../generator'; import {createSlugger} from '@docusaurus/utils'; import type {VersionMetadata} from '../../types'; import {DefaultNumberPrefixParser} from '../../numberPrefix'; +import {isCategoryIndex} from '../../docs'; describe('processSidebars', () => { function createStaticSidebarItemGenerator( @@ -137,6 +138,7 @@ describe('processSidebars', () => { versionName: version.versionName, }, numberPrefixParser: DefaultNumberPrefixParser, + isCategoryIndex, options: params.sidebarOptions, }); expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({ @@ -147,6 +149,7 @@ describe('processSidebars', () => { versionName: version.versionName, }, numberPrefixParser: DefaultNumberPrefixParser, + isCategoryIndex, options: params.sidebarOptions, }); expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({ @@ -157,6 +160,7 @@ describe('processSidebars', () => { versionName: version.versionName, }, numberPrefixParser: DefaultNumberPrefixParser, + isCategoryIndex, options: params.sidebarOptions, }); diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts index acd534a993..0807112cba 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts @@ -25,7 +25,7 @@ import path from 'path'; import fs from 'fs-extra'; import Yaml from 'js-yaml'; import {validateCategoryMetadataFile} from './validation'; -import {createDocsByIdIndex, isConventionalDocIndex} from '../docs'; +import {createDocsByIdIndex, toCategoryIndexMatcherParam} from '../docs'; const BreadcrumbSeparator = '/'; // To avoid possible name clashes with a folder of the same name as the ID @@ -94,6 +94,7 @@ async function readCategoryMetadataFile( // Comment for this feature: https://github.com/facebook/docusaurus/issues/3464#issuecomment-818670449 export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({ numberPrefixParser, + isCategoryIndex, docs: allDocs, options, item: {dirName: autogenDir}, @@ -210,10 +211,13 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({ } function findConventionalCategoryDocLink(): SidebarItemDoc | undefined { - return allItems.find( - (item) => - item.type === 'doc' && isConventionalDocIndex(getDoc(item.id)), - ) as SidebarItemDoc | undefined; + return allItems.find((item) => { + if (item.type !== 'doc') { + return false; + } + const doc = getDoc(item.id); + return isCategoryIndex(toCategoryIndexMatcherParam(doc)); + }) as SidebarItemDoc | undefined; } function getCategoryLinkedDocId(): string | undefined { diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/processor.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/processor.ts index e1ec68fbc2..0ef94ba25f 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/processor.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/processor.ts @@ -25,6 +25,7 @@ import {DefaultSidebarItemsGenerator} from './generator'; import {mapValues, memoize, pick} from 'lodash'; import combinePromises from 'combine-promises'; import {normalizeItem} from './normalization'; +import {isCategoryIndex} from '../docs'; import type {Slugger} from '@docusaurus/utils'; import type { NumberPrefixParser, @@ -95,6 +96,7 @@ async function processSidebar( item, numberPrefixParser, defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator, + isCategoryIndex, ...getSidebarItemsGeneratorDocsAndVersion(), options: sidebarOptions, }); diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/types.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/types.ts index 8d6c61ecf3..56a4be084a 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/types.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/types.ts @@ -10,6 +10,7 @@ import type {DocMetadataBase, VersionMetadata} from '../types'; import type { NumberPrefixParser, SidebarOptions, + CategoryIndexMatcher, } from '@docusaurus/plugin-content-docs'; // Makes all properties visible when hovering over the type @@ -195,6 +196,7 @@ export type SidebarItemsGeneratorArgs = { version: SidebarItemsGeneratorVersion; docs: SidebarItemsGeneratorDoc[]; numberPrefixParser: NumberPrefixParser; + isCategoryIndex: CategoryIndexMatcher; options: SidebarOptions; }; export type SidebarItemsGenerator = ( diff --git a/packages/docusaurus-plugin-content-docs/src/slug.ts b/packages/docusaurus-plugin-content-docs/src/slug.ts index 6d84e9b176..e5168563c2 100644 --- a/packages/docusaurus-plugin-content-docs/src/slug.ts +++ b/packages/docusaurus-plugin-content-docs/src/slug.ts @@ -16,7 +16,7 @@ import { stripPathNumberPrefixes, } from './numberPrefix'; import type {DocMetadataBase} from './types'; -import {isConventionalDocIndex} from './docs'; +import {isCategoryIndex, toCategoryIndexMatcherParam} from './docs'; import type {NumberPrefixParser} from '@docusaurus/plugin-content-docs'; export default function getSlug({ @@ -29,7 +29,7 @@ export default function getSlug({ }: { baseID: string; frontMatterSlug?: string; - source: DocMetadataBase['slug']; + source: DocMetadataBase['source']; sourceDirName: DocMetadataBase['sourceDirName']; stripDirNumberPrefixes?: boolean; numberPrefixParser?: NumberPrefixParser; @@ -50,7 +50,10 @@ export default function getSlug({ return frontMatterSlug; } else { const dirNameSlug = getDirNameSlug(); - if (!frontMatterSlug && isConventionalDocIndex({source, sourceDirName})) { + if ( + !frontMatterSlug && + isCategoryIndex(toCategoryIndexMatcherParam({source, sourceDirName})) + ) { return dirNameSlug; } const baseSlug = frontMatterSlug || baseID; diff --git a/packages/docusaurus-plugin-content-docs/src/types.ts b/packages/docusaurus-plugin-content-docs/src/types.ts index 4604ca1d65..b32575772f 100644 --- a/packages/docusaurus-plugin-content-docs/src/types.ts +++ b/packages/docusaurus-plugin-content-docs/src/types.ts @@ -82,8 +82,8 @@ export type DocMetadataBase = LastUpdateData & { version: string; title: string; description: string; - source: string; // @site aliased source => "@site/docs/folder/subFolder/subSubFolder/myDoc.md" - sourceDirName: string; // relative to the versioned docs folder (can be ".") => "folder/subFolder/subSubFolder" + source: string; // @site aliased posix source => "@site/docs/folder/subFolder/subSubFolder/myDoc.md" + sourceDirName: string; // posix path relative to the versioned docs folder (can be ".") => "folder/subFolder/subSubFolder" slug: string; permalink: string; sidebarPosition?: number; diff --git a/website/_dogfooding/_docs tests/tests/category-links/custom-index-convention/intro.md b/website/_dogfooding/_docs tests/tests/category-links/custom-index-convention/intro.md new file mode 100644 index 0000000000..dd647aa825 --- /dev/null +++ b/website/_dogfooding/_docs tests/tests/category-links/custom-index-convention/intro.md @@ -0,0 +1,3 @@ +# Introduction + +This file is called `intro.md`. Typically, it won't be selected by the convention; however, it is in this case, because we have used a custom one. diff --git a/website/_dogfooding/_docs tests/tests/category-links/custom-index-convention/sample-doc.md b/website/_dogfooding/_docs tests/tests/category-links/custom-index-convention/sample-doc.md new file mode 100644 index 0000000000..15fb7937c3 --- /dev/null +++ b/website/_dogfooding/_docs tests/tests/category-links/custom-index-convention/sample-doc.md @@ -0,0 +1,3 @@ +# Sample doc + +Lorem Ipsum diff --git a/website/_dogfooding/dogfooding.config.js b/website/_dogfooding/dogfooding.config.js index 0179dc3599..3d087da411 100644 --- a/website/_dogfooding/dogfooding.config.js +++ b/website/_dogfooding/dogfooding.config.js @@ -21,6 +21,20 @@ const dogfoodingPluginInstances = [ // The target folder uses a _ prefix to test against an edge case regarding MDX partials: https://github.com/facebook/docusaurus/discussions/5181#discussioncomment-1018079 path: fs.realpathSync('_dogfooding/docs-tests-symlink'), showLastUpdateTime: true, + sidebarItemsGenerator(args) { + return args.defaultSidebarItemsGenerator({ + ...args, + isCategoryIndex({fileName, directories}) { + const eligibleDocIndexNames = [ + 'index', + 'readme', + directories[0].toLowerCase(), + 'intro', + ]; + return eligibleDocIndexNames.includes(fileName.toLowerCase()); + }, + }); + }, }), ], diff --git a/website/docs/api/plugins/plugin-content-docs.md b/website/docs/api/plugins/plugin-content-docs.md index ef6b1aa9ac..1c706ef128 100644 --- a/website/docs/api/plugins/plugin-content-docs.md +++ b/website/docs/api/plugins/plugin-content-docs.md @@ -77,6 +77,12 @@ type PrefixParser = (filename: string) => { numberPrefix?: number; }; +type CategoryIndexMatcher = (doc: { + fileName: string; + directories: string[]; + extension: string; +}) => boolean; + type SidebarGenerator = (generatorArgs: { item: {type: 'autogenerated'; dirName: string}; // the sidebar item with type "autogenerated" version: {contentPath: string; versionName: string}; // the current version @@ -88,6 +94,7 @@ type SidebarGenerator = (generatorArgs: { sidebarPosition?: number | undefined; }>; // all the docs of that version (unfiltered) numberPrefixParser: PrefixParser; // numberPrefixParser configured for this plugin + isCategoryIndex: CategoryIndexMatcher; // the default category index matcher, that you can override defaultSidebarItemsGenerator: SidebarGenerator; // useful to re-use/enhance default sidebar generation logic from Docusaurus }) => Promise; @@ -141,6 +148,7 @@ const config = { item, version, docs, + isCategoryIndex, }) { // Use the provided data to generate a custom sidebar slice return [ @@ -274,15 +282,15 @@ website/i18n/[locale]/docusaurus-plugin-content-docs │ │ # translations for website/docs ├── current -│   ├── api -│   │   └── config.md -│   └── getting-started.md +│ ├── api +│ │ └── config.md +│ └── getting-started.md ├── current.json │ │ # translations for website/versioned_docs/version-1.0.0 ├── version-1.0.0 -│   ├── api -│   │   └── config.md -│   └── getting-started.md +│ ├── api +│ │ └── config.md +│ └── getting-started.md └── version-1.0.0.json ``` diff --git a/website/docs/guides/docs/sidebar/autogenerated.md b/website/docs/guides/docs/sidebar/autogenerated.md index f3ffe9b20c..4282e02c11 100644 --- a/website/docs/guides/docs/sidebar/autogenerated.md +++ b/website/docs/guides/docs/sidebar/autogenerated.md @@ -194,6 +194,102 @@ Naming your introductory document `README.md` makes it show up when browsing the ::: +
+ +Customizing category index matching + +It is possible to opt out any of the category index conventions, or define even more conventions. You can inject your own `isCategoryIndex` matcher through the [`sidebarItemsGenerator`](#customize-the-sidebar-items-generator) callback. For example, you can also pick `intro` as another file name eligible for automatically becoming the category index. + +```js title="docusaurus.config.js" +module.exports = { + plugins: [ + [ + '@docusaurus/plugin-content-docs', + { + async sidebarItemsGenerator({ + ...args, + isCategoryIndex: defaultCategoryIndexMatcher, // The default matcher implementation, given below + defaultSidebarItemsGenerator, + }) { + return defaultSidebarItemsGenerator({ + ...args, + // highlight-start + isCategoryIndex(doc) { + return ( + // Also pick intro.md in addition to the default ones + doc.fileName.toLowerCase() === 'intro' || + defaultCategoryIndexMatcher(doc) + ); + }, + // highlight-end + }); + }, + }, + ], + ], +}; +``` + +Or choose to not have any category index convention. + +```js title="docusaurus.config.js" +module.exports = { + plugins: [ + [ + '@docusaurus/plugin-content-docs', + { + async sidebarItemsGenerator({ + ...args, + isCategoryIndex: defaultCategoryIndexMatcher, // The default matcher implementation, given below + defaultSidebarItemsGenerator, + }) { + return defaultSidebarItemsGenerator({ + ...args, + // highlight-start + isCategoryIndex() { + // No doc will be automatically picked as category index + return false; + }, + // highlight-end + }); + }, + }, + ], + ], +}; +``` + +The `isCategoryIndex` matcher will be provided with three fields: + +- `fileName`, the file's name without extension and with casing preserved +- `directories`, the list of directory names _from the lowest level to the highest level_, relative to the docs root directory +- `extension`, the file's extension, with a leading dot. + +For example, for a doc file at `guides/sidebar/autogenerated.md`, the props the matcher receives are + +```js +const props = { + fileName: 'autogenerated', + directories: ['sidebar', 'guides'], + extension: '.md', +}; +``` + +The default implementation is: + +```js +function isCategoryIndex({fileName, directories}) { + const eligibleDocIndexNames = [ + 'index', + 'readme', + directories[0].toLowerCase(), + ]; + return eligibleDocIndexNames.includes(fileName.toLowerCase()); +} +``` + +
+ ## Autogenerated sidebar metadata {#autogenerated-sidebar-metadata} For hand-written sidebar definitions, you would provide metadata to sidebar items through `sidebars.js`; for autogenerated, Docusaurus would read them from the item's respective file. In addition, you may want to adjust the relative position of each item, because, by default, items within a sidebar slice will be generated in **alphabetical order** (using files and folders names). @@ -317,6 +413,7 @@ module.exports = { item, version, docs, + isCategoryIndex, }) { // Example: return an hardcoded list of static sidebar items return [