fix(plugin-docs,theme): refactor docs plugin routes and component tree (#7966)

This commit is contained in:
Sébastien Lorber 2022-08-18 17:55:05 +02:00 committed by GitHub
parent c29218ea1d
commit 3b9b497d13
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 1189 additions and 857 deletions

View file

@ -1242,6 +1242,20 @@ exports[`simple website content: global data 1`] = `
exports[`simple website content: route config 1`] = ` exports[`simple website content: route config 1`] = `
[ [
{
"component": "@theme/DocsRoot",
"exact": false,
"path": "/docs",
"routes": [
{
"component": "@theme/DocVersionRoot",
"exact": false,
"modules": {
"version": "~docs/version-current-metadata-prop-751.json",
},
"path": "/docs",
"priority": -1,
"routes": [
{ {
"component": "@theme/DocTagsListPage", "component": "@theme/DocTagsListPage",
"exact": true, "exact": true,
@ -1275,13 +1289,9 @@ exports[`simple website content: route config 1`] = `
"path": "/docs/tags/tag2-custom-permalink", "path": "/docs/tags/tag2-custom-permalink",
}, },
{ {
"component": "@theme/DocPage", "component": "@theme/DocRoot",
"exact": false, "exact": false,
"modules": {
"versionMetadata": "~docs/version-current-metadata-prop-751.json",
},
"path": "/docs", "path": "/docs",
"priority": -1,
"routes": [ "routes": [
{ {
"component": "@theme/DocItem", "component": "@theme/DocItem",
@ -1454,6 +1464,10 @@ exports[`simple website content: route config 1`] = `
}, },
], ],
}, },
],
},
],
},
] ]
`; `;
@ -2724,13 +2738,23 @@ exports[`versioned website (community) content: global data 1`] = `
exports[`versioned website (community) content: route config 1`] = ` exports[`versioned website (community) content: route config 1`] = `
[ [
{ {
"component": "@theme/DocPage", "component": "@theme/DocsRoot",
"exact": false,
"path": "/community",
"routes": [
{
"component": "@theme/DocVersionRoot",
"exact": false, "exact": false,
"modules": { "modules": {
"versionMetadata": "~docs/version-current-metadata-prop-751.json", "version": "~docs/version-current-metadata-prop-751.json",
}, },
"path": "/community/next", "path": "/community/next",
"priority": undefined, "priority": undefined,
"routes": [
{
"component": "@theme/DocRoot",
"exact": false,
"path": "/community/next",
"routes": [ "routes": [
{ {
"component": "@theme/DocItem", "component": "@theme/DocItem",
@ -2743,14 +2767,21 @@ exports[`versioned website (community) content: route config 1`] = `
}, },
], ],
}, },
],
},
{ {
"component": "@theme/DocPage", "component": "@theme/DocVersionRoot",
"exact": false, "exact": false,
"modules": { "modules": {
"versionMetadata": "~docs/version-1-0-0-metadata-prop-608.json", "version": "~docs/version-1-0-0-metadata-prop-608.json",
}, },
"path": "/community", "path": "/community",
"priority": -1, "priority": -1,
"routes": [
{
"component": "@theme/DocRoot",
"exact": false,
"path": "/community",
"routes": [ "routes": [
{ {
"component": "@theme/DocItem", "component": "@theme/DocItem",
@ -2763,6 +2794,10 @@ exports[`versioned website (community) content: route config 1`] = `
}, },
], ],
}, },
],
},
],
},
] ]
`; `;
@ -3930,6 +3965,65 @@ exports[`versioned website content: global data 1`] = `
exports[`versioned website content: route config 1`] = ` exports[`versioned website content: route config 1`] = `
[ [
{
"component": "@theme/DocsRoot",
"exact": false,
"path": "/docs",
"routes": [
{
"component": "@theme/DocVersionRoot",
"exact": false,
"modules": {
"version": "~docs/version-1-0-0-metadata-prop-608.json",
},
"path": "/docs/1.0.0",
"priority": undefined,
"routes": [
{
"component": "@theme/DocRoot",
"exact": false,
"path": "/docs/1.0.0",
"routes": [
{
"component": "@theme/DocItem",
"exact": true,
"modules": {
"content": "@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md",
},
"path": "/docs/1.0.0/",
"sidebar": "version-1.0.0/docs",
},
{
"component": "@theme/DocItem",
"exact": true,
"modules": {
"content": "@site/versioned_docs/version-1.0.0/foo/bar.md",
},
"path": "/docs/1.0.0/foo/barSlug",
"sidebar": "version-1.0.0/docs",
},
{
"component": "@theme/DocItem",
"exact": true,
"modules": {
"content": "@site/versioned_docs/version-1.0.0/foo/baz.md",
},
"path": "/docs/1.0.0/foo/baz",
"sidebar": "version-1.0.0/docs",
},
],
},
],
},
{
"component": "@theme/DocVersionRoot",
"exact": false,
"modules": {
"version": "~docs/version-current-metadata-prop-751.json",
},
"path": "/docs/next",
"priority": undefined,
"routes": [
{ {
"component": "@theme/DocTagsListPage", "component": "@theme/DocTagsListPage",
"exact": true, "exact": true,
@ -3963,51 +4057,9 @@ exports[`versioned website content: route config 1`] = `
"path": "/docs/next/tags/barTag-3-permalink", "path": "/docs/next/tags/barTag-3-permalink",
}, },
{ {
"component": "@theme/DocPage", "component": "@theme/DocRoot",
"exact": false, "exact": false,
"modules": {
"versionMetadata": "~docs/version-1-0-0-metadata-prop-608.json",
},
"path": "/docs/1.0.0",
"priority": undefined,
"routes": [
{
"component": "@theme/DocItem",
"exact": true,
"modules": {
"content": "@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md",
},
"path": "/docs/1.0.0/",
"sidebar": "version-1.0.0/docs",
},
{
"component": "@theme/DocItem",
"exact": true,
"modules": {
"content": "@site/versioned_docs/version-1.0.0/foo/bar.md",
},
"path": "/docs/1.0.0/foo/barSlug",
"sidebar": "version-1.0.0/docs",
},
{
"component": "@theme/DocItem",
"exact": true,
"modules": {
"content": "@site/versioned_docs/version-1.0.0/foo/baz.md",
},
"path": "/docs/1.0.0/foo/baz",
"sidebar": "version-1.0.0/docs",
},
],
},
{
"component": "@theme/DocPage",
"exact": false,
"modules": {
"versionMetadata": "~docs/version-current-metadata-prop-751.json",
},
"path": "/docs/next", "path": "/docs/next",
"priority": undefined,
"routes": [ "routes": [
{ {
"component": "@theme/DocItem", "component": "@theme/DocItem",
@ -4061,14 +4113,21 @@ exports[`versioned website content: route config 1`] = `
}, },
], ],
}, },
],
},
{ {
"component": "@theme/DocPage", "component": "@theme/DocVersionRoot",
"exact": false, "exact": false,
"modules": { "modules": {
"versionMetadata": "~docs/version-with-slugs-metadata-prop-2bf.json", "version": "~docs/version-with-slugs-metadata-prop-2bf.json",
}, },
"path": "/docs/withSlugs", "path": "/docs/withSlugs",
"priority": undefined, "priority": undefined,
"routes": [
{
"component": "@theme/DocRoot",
"exact": false,
"path": "/docs/withSlugs",
"routes": [ "routes": [
{ {
"component": "@theme/DocItem", "component": "@theme/DocItem",
@ -4137,14 +4196,21 @@ exports[`versioned website content: route config 1`] = `
}, },
], ],
}, },
],
},
{ {
"component": "@theme/DocPage", "component": "@theme/DocVersionRoot",
"exact": false, "exact": false,
"modules": { "modules": {
"versionMetadata": "~docs/version-1-0-1-metadata-prop-e87.json", "version": "~docs/version-1-0-1-metadata-prop-e87.json",
}, },
"path": "/docs", "path": "/docs",
"priority": -1, "priority": -1,
"routes": [
{
"component": "@theme/DocRoot",
"exact": false,
"path": "/docs",
"routes": [ "routes": [
{ {
"component": "@theme/DocItem", "component": "@theme/DocItem",
@ -4166,6 +4232,10 @@ exports[`versioned website content: route config 1`] = `
}, },
], ],
}, },
],
},
],
},
] ]
`; `;

View file

@ -49,7 +49,9 @@ describe('normalizeDocsPluginOptions', () => {
sidebarPath: 'my-sidebar', // Path to sidebar configuration for showing a list of markdown pages. sidebarPath: 'my-sidebar', // Path to sidebar configuration for showing a list of markdown pages.
sidebarItemsGenerator: DefaultSidebarItemsGenerator, sidebarItemsGenerator: DefaultSidebarItemsGenerator,
numberPrefixParser: DefaultNumberPrefixParser, numberPrefixParser: DefaultNumberPrefixParser,
docLayoutComponent: '@theme/DocPage', docsRootComponent: '@theme/DocsRoot',
docVersionRootComponent: '@theme/DocVersionRoot',
docRootComponent: '@theme/DocRoot',
docItemComponent: '@theme/DocItem', docItemComponent: '@theme/DocItem',
docTagDocListComponent: '@theme/DocTagDocListPage', docTagDocListComponent: '@theme/DocTagDocListPage',
docTagsListComponent: '@theme/DocTagsListPage', docTagsListComponent: '@theme/DocTagsListPage',

View file

@ -27,22 +27,18 @@ import {
addDocNavigation, addDocNavigation,
type DocEnv, type DocEnv,
} from './docs'; } from './docs';
import {readVersionsMetadata} from './versions'; import {readVersionsMetadata, toFullVersion} from './versions';
import {cliDocsVersionCommand} from './cli'; import {cliDocsVersionCommand} from './cli';
import {VERSIONS_JSON_FILE} from './constants'; import {VERSIONS_JSON_FILE} from './constants';
import {toGlobalDataVersion} from './globalData'; import {toGlobalDataVersion} from './globalData';
import {toTagDocListProp} from './props';
import {getCategoryGeneratedIndexMetadataList} from './categoryGeneratedIndex';
import { import {
translateLoadedContent, translateLoadedContent,
getLoadedContentTranslationFiles, getLoadedContentTranslationFiles,
} from './translations'; } from './translations';
import {getVersionTags} from './tags'; import {createAllRoutes} from './routes';
import {createVersionRoutes} from './routes';
import {createSidebarsUtils} from './sidebars/utils'; import {createSidebarsUtils} from './sidebars/utils';
import type { import type {
PropTagsListPage,
PluginOptions, PluginOptions,
DocMetadataBase, DocMetadataBase,
VersionMetadata, VersionMetadata,
@ -55,7 +51,6 @@ import type {
SourceToPermalink, SourceToPermalink,
DocFile, DocFile,
DocsMarkdownOption, DocsMarkdownOption,
VersionTag,
FullVersion, FullVersion,
} from './types'; } from './types';
import type {RuleSetRule} from 'webpack'; import type {RuleSetRule} from 'webpack';
@ -209,102 +204,20 @@ export default async function pluginContentDocs(
}, },
async contentLoaded({content, actions}) { async contentLoaded({content, actions}) {
const {loadedVersions} = content; const versions: FullVersion[] = content.loadedVersions.map(toFullVersion);
const {
docLayoutComponent,
docItemComponent,
docCategoryGeneratedIndexComponent,
breadcrumbs,
} = options;
const {addRoute, createData, setGlobalData} = actions;
const versions: FullVersion[] = loadedVersions.map((version) => {
const sidebarsUtils = createSidebarsUtils(version.sidebars);
return {
...version,
sidebarsUtils,
categoryGeneratedIndices: getCategoryGeneratedIndexMetadataList({
docs: version.docs,
sidebarsUtils,
}),
};
});
async function createVersionTagsRoutes(version: FullVersion) { await createAllRoutes({
const versionTags = getVersionTags(version.docs); baseUrl,
versions,
// TODO tags should be a sub route of the version route options,
async function createTagsListPage() {
const tagsProp: PropTagsListPage['tags'] = Object.values(
versionTags,
).map((tagValue) => ({
label: tagValue.label,
permalink: tagValue.permalink,
count: tagValue.docIds.length,
}));
// Only create /tags page if there are tags.
if (tagsProp.length > 0) {
const tagsPropPath = await createData(
`${docuHash(`tags-list-${version.versionName}-prop`)}.json`,
JSON.stringify(tagsProp, null, 2),
);
addRoute({
path: version.tagsPath,
exact: true,
component: options.docTagsListComponent,
modules: {
tags: aliasedSource(tagsPropPath),
},
});
}
}
// TODO tags should be a sub route of the version route
async function createTagDocListPage(tag: VersionTag) {
const tagProps = toTagDocListProp({
allTagsPath: version.tagsPath,
tag,
docs: version.docs,
});
const tagPropPath = await createData(
`${docuHash(`tag-${tag.permalink}`)}.json`,
JSON.stringify(tagProps, null, 2),
);
addRoute({
path: tag.permalink,
component: options.docTagDocListComponent,
exact: true,
modules: {
tag: aliasedSource(tagPropPath),
},
});
}
await createTagsListPage();
await Promise.all(Object.values(versionTags).map(createTagDocListPage));
}
await Promise.all(
versions.map((version) =>
createVersionRoutes({
version,
docItemComponent,
docLayoutComponent,
docCategoryGeneratedIndexComponent,
pluginId,
aliasedSource,
actions, actions,
}), aliasedSource,
), });
);
// TODO tags should be a sub route of the version route actions.setGlobalData({
await Promise.all(versions.map(createVersionTagsRoutes));
setGlobalData({
path: normalizeUrl([baseUrl, options.routeBasePath]), path: normalizeUrl([baseUrl, options.routeBasePath]),
versions: versions.map(toGlobalDataVersion), versions: versions.map(toGlobalDataVersion),
breadcrumbs, breadcrumbs: options.breadcrumbs,
}); });
}, },

View file

@ -30,7 +30,9 @@ export const DEFAULT_OPTIONS: Omit<PluginOptions, 'id' | 'sidebarPath'> = {
exclude: GlobExcludeDefault, exclude: GlobExcludeDefault,
sidebarItemsGenerator: DefaultSidebarItemsGenerator, sidebarItemsGenerator: DefaultSidebarItemsGenerator,
numberPrefixParser: DefaultNumberPrefixParser, numberPrefixParser: DefaultNumberPrefixParser,
docLayoutComponent: '@theme/DocPage', docsRootComponent: '@theme/DocsRoot',
docVersionRootComponent: '@theme/DocVersionRoot',
docRootComponent: '@theme/DocRoot',
docItemComponent: '@theme/DocItem', docItemComponent: '@theme/DocItem',
docTagDocListComponent: '@theme/DocTagDocListPage', docTagDocListComponent: '@theme/DocTagDocListPage',
docTagsListComponent: '@theme/DocTagsListPage', docTagsListComponent: '@theme/DocTagsListPage',
@ -104,7 +106,11 @@ const OptionsSchema = Joi.object<PluginOptions>({
}), }),
) )
.default(() => DEFAULT_OPTIONS.numberPrefixParser), .default(() => DEFAULT_OPTIONS.numberPrefixParser),
docLayoutComponent: Joi.string().default(DEFAULT_OPTIONS.docLayoutComponent), docsRootComponent: Joi.string().default(DEFAULT_OPTIONS.docsRootComponent),
docVersionRootComponent: Joi.string().default(
DEFAULT_OPTIONS.docVersionRootComponent,
),
docRootComponent: Joi.string().default(DEFAULT_OPTIONS.docRootComponent),
docItemComponent: Joi.string().default(DEFAULT_OPTIONS.docItemComponent), docItemComponent: Joi.string().default(DEFAULT_OPTIONS.docItemComponent),
docTagsListComponent: Joi.string().default( docTagsListComponent: Joi.string().default(
DEFAULT_OPTIONS.docTagsListComponent, DEFAULT_OPTIONS.docTagsListComponent,

View file

@ -198,10 +198,24 @@ declare module '@docusaurus/plugin-content-docs' {
*/ */
exclude: string[]; exclude: string[];
/** /**
* Root layout component of each doc page. Provides the version data * Parent component of all the docs plugin pages (including all versions).
* context, and is not unmounted when switching docs. * Stays mounted when navigation between docs pages and versions.
*/ */
docLayoutComponent: string; docsRootComponent: string;
/**
* Parent component of all docs pages of an individual version:
* - docs pages with sidebars
* - tags pages
* Stays mounted when navigation between pages of that specific version.
*/
docVersionRootComponent: string;
/**
* Parent component of all docs pages with sidebars:
* - regular docs pages
* - category generated index pages
* Stays mounted when navigation between such pages.
*/
docRootComponent: string;
/** Main doc container, with TOC, pagination, etc. */ /** Main doc container, with TOC, pagination, etc. */
docItemComponent: string; docItemComponent: string;
/** Root component of the "docs containing tag X" page. */ /** Root component of the "docs containing tag X" page. */
@ -610,14 +624,32 @@ declare module '@theme/DocBreadcrumbs' {
export default function DocBreadcrumbs(): JSX.Element; export default function DocBreadcrumbs(): JSX.Element;
} }
declare module '@theme/DocPage' { declare module '@theme/DocsRoot' {
import type {RouteConfigComponentProps} from 'react-router-config';
import type {Required} from 'utility-types';
export interface Props extends Required<RouteConfigComponentProps, 'route'> {}
export default function DocsRoot(props: Props): JSX.Element;
}
declare module '@theme/DocVersionRoot' {
import type {PropVersionMetadata} from '@docusaurus/plugin-content-docs'; import type {PropVersionMetadata} from '@docusaurus/plugin-content-docs';
import type {RouteConfigComponentProps} from 'react-router-config'; import type {RouteConfigComponentProps} from 'react-router-config';
import type {Required} from 'utility-types'; import type {Required} from 'utility-types';
export interface Props extends Required<RouteConfigComponentProps, 'route'> { export interface Props extends Required<RouteConfigComponentProps, 'route'> {
readonly versionMetadata: PropVersionMetadata; readonly version: PropVersionMetadata;
} }
export default function DocPage(props: Props): JSX.Element; export default function DocVersionRoot(props: Props): JSX.Element;
}
declare module '@theme/DocRoot' {
import type {RouteConfigComponentProps} from 'react-router-config';
import type {Required} from 'utility-types';
export interface Props extends Required<RouteConfigComponentProps, 'route'> {}
export default function DocRoot(props: Props): JSX.Element;
} }

View file

@ -7,7 +7,7 @@
import _ from 'lodash'; import _ from 'lodash';
import {createDocsByIdIndex} from './docs'; import {createDocsByIdIndex} from './docs';
import type {VersionTag} from './types'; import type {VersionTag, VersionTags} from './types';
import type { import type {
SidebarItemDoc, SidebarItemDoc,
SidebarItem, SidebarItem,
@ -21,6 +21,7 @@ import type {
PropSidebarItemCategory, PropSidebarItemCategory,
PropTagDocList, PropTagDocList,
PropTagDocListDoc, PropTagDocListDoc,
PropTagsListPage,
PropSidebarItemLink, PropSidebarItemLink,
PropVersionDocs, PropVersionDocs,
DocMetadata, DocMetadata,
@ -181,3 +182,13 @@ export function toTagDocListProp({
items: toDocListProp(), items: toDocListProp(),
}; };
} }
export function toTagsListTagsProp(
versionTags: VersionTags,
): PropTagsListPage['tags'] {
return Object.values(versionTags).map((tagValue) => ({
label: tagValue.label,
permalink: tagValue.permalink,
count: tagValue.docIds.length,
}));
}

View file

@ -5,30 +5,32 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import _ from 'lodash';
import logger from '@docusaurus/logger'; import logger from '@docusaurus/logger';
import {docuHash, createSlugger} from '@docusaurus/utils'; import {docuHash, createSlugger, normalizeUrl} from '@docusaurus/utils';
import {toVersionMetadataProp} from './props'; import {
toTagDocListProp,
toTagsListTagsProp,
toVersionMetadataProp,
} from './props';
import {getVersionTags} from './tags';
import type {PluginContentLoadedActions, RouteConfig} from '@docusaurus/types'; import type {PluginContentLoadedActions, RouteConfig} from '@docusaurus/types';
import type {FullVersion} from './types'; import type {FullVersion, VersionTag} from './types';
import type { import type {
CategoryGeneratedIndexMetadata, CategoryGeneratedIndexMetadata,
DocMetadata, PluginOptions,
PropTagsListPage,
} from '@docusaurus/plugin-content-docs'; } from '@docusaurus/plugin-content-docs';
export async function createCategoryGeneratedIndexRoutes({ async function buildVersionCategoryGeneratedIndexRoutes({
version, version,
actions, actions,
docCategoryGeneratedIndexComponent, options,
aliasedSource, aliasedSource,
}: { }: BuildVersionRoutesParam): Promise<RouteConfig[]> {
version: FullVersion;
actions: PluginContentLoadedActions;
docCategoryGeneratedIndexComponent: string;
aliasedSource: (str: string) => string;
}): Promise<RouteConfig[]> {
const slugs = createSlugger(); const slugs = createSlugger();
async function createCategoryGeneratedIndexRoute( async function buildCategoryGeneratedIndexRoute(
categoryGeneratedIndex: CategoryGeneratedIndexMetadata, categoryGeneratedIndex: CategoryGeneratedIndexMetadata,
): Promise<RouteConfig> { ): Promise<RouteConfig> {
const {sidebar, ...prop} = categoryGeneratedIndex; const {sidebar, ...prop} = categoryGeneratedIndex;
@ -44,7 +46,7 @@ export async function createCategoryGeneratedIndexRoutes({
return { return {
path: categoryGeneratedIndex.permalink, path: categoryGeneratedIndex.permalink,
component: docCategoryGeneratedIndexComponent, component: options.docCategoryGeneratedIndexComponent,
exact: true, exact: true,
modules: { modules: {
categoryGeneratedIndex: aliasedSource(propData), categoryGeneratedIndex: aliasedSource(propData),
@ -56,21 +58,17 @@ export async function createCategoryGeneratedIndexRoutes({
} }
return Promise.all( return Promise.all(
version.categoryGeneratedIndices.map(createCategoryGeneratedIndexRoute), version.categoryGeneratedIndices.map(buildCategoryGeneratedIndexRoute),
); );
} }
export async function createDocRoutes({ async function buildVersionDocRoutes({
docs, version,
actions, actions,
docItemComponent, options,
}: { }: BuildVersionRoutesParam): Promise<RouteConfig[]> {
docs: DocMetadata[];
actions: PluginContentLoadedActions;
docItemComponent: string;
}): Promise<RouteConfig[]> {
return Promise.all( return Promise.all(
docs.map(async (metadataItem) => { version.docs.map(async (metadataItem) => {
await actions.createData( await actions.createData(
// Note that this created data path must be in sync with // Note that this created data path must be in sync with
// metadataPath provided to mdx-loader. // metadataPath provided to mdx-loader.
@ -80,12 +78,12 @@ export async function createDocRoutes({
const docRoute: RouteConfig = { const docRoute: RouteConfig = {
path: metadataItem.permalink, path: metadataItem.permalink,
component: docItemComponent, component: options.docItemComponent,
exact: true, exact: true,
modules: { modules: {
content: metadataItem.source, content: metadataItem.source,
}, },
// Because the parent (DocPage) comp need to access it easily // Because the parent (DocRoot) comp need to access it easily
// This permits to render the sidebar once without unmount/remount when // This permits to render the sidebar once without unmount/remount when
// navigating (and preserve sidebar state) // navigating (and preserve sidebar state)
...(metadataItem.sidebar && { ...(metadataItem.sidebar && {
@ -98,62 +96,160 @@ export async function createDocRoutes({
); );
} }
export async function createVersionRoutes({ async function buildVersionSidebarRoute(param: BuildVersionRoutesParam) {
version, const [docRoutes, categoryGeneratedIndexRoutes] = await Promise.all([
actions, buildVersionDocRoutes(param),
docItemComponent, buildVersionCategoryGeneratedIndexRoutes(param),
docLayoutComponent,
docCategoryGeneratedIndexComponent,
pluginId,
aliasedSource,
}: {
version: FullVersion;
actions: PluginContentLoadedActions;
docLayoutComponent: string;
docItemComponent: string;
docCategoryGeneratedIndexComponent: string;
pluginId: string;
aliasedSource: (str: string) => string;
}): Promise<void> {
async function doCreateVersionRoutes(): Promise<void> {
const versionMetadata = toVersionMetadataProp(pluginId, version);
const versionMetadataPropPath = await actions.createData(
`${docuHash(`version-${version.versionName}-metadata-prop`)}.json`,
JSON.stringify(versionMetadata, null, 2),
);
async function createVersionSubRoutes() {
const [docRoutes, sidebarsRoutes] = await Promise.all([
createDocRoutes({docs: version.docs, actions, docItemComponent}),
createCategoryGeneratedIndexRoutes({
version,
actions,
docCategoryGeneratedIndexComponent,
aliasedSource,
}),
]); ]);
const subRoutes = [...docRoutes, ...categoryGeneratedIndexRoutes];
const routes = [...docRoutes, ...sidebarsRoutes]; return {
return routes.sort((a, b) => a.path.localeCompare(b.path)); path: param.version.path,
exact: false,
component: param.options.docRootComponent,
routes: subRoutes,
};
} }
actions.addRoute({ async function buildVersionTagsRoutes(
path: version.path, param: BuildVersionRoutesParam,
// Allow matching /docs/* since this is the wrapping route ): Promise<RouteConfig[]> {
exact: false, const {version, options, actions, aliasedSource} = param;
component: docLayoutComponent, const versionTags = getVersionTags(version.docs);
routes: await createVersionSubRoutes(),
async function buildTagsListRoute(): Promise<RouteConfig | null> {
// Don't create a tags list page if there's no tag
if (Object.keys(versionTags).length === 0) {
return null;
}
const tagsProp: PropTagsListPage['tags'] = toTagsListTagsProp(versionTags);
const tagsPropPath = await actions.createData(
`${docuHash(`tags-list-${version.versionName}-prop`)}.json`,
JSON.stringify(tagsProp, null, 2),
);
return {
path: version.tagsPath,
exact: true,
component: options.docTagsListComponent,
modules: { modules: {
versionMetadata: aliasedSource(versionMetadataPropPath), tags: aliasedSource(tagsPropPath),
},
};
}
async function buildTagDocListRoute(tag: VersionTag): Promise<RouteConfig> {
const tagProps = toTagDocListProp({
allTagsPath: version.tagsPath,
tag,
docs: version.docs,
});
const tagPropPath = await actions.createData(
`${docuHash(`tag-${tag.permalink}`)}.json`,
JSON.stringify(tagProps, null, 2),
);
return {
path: tag.permalink,
component: options.docTagDocListComponent,
exact: true,
modules: {
tag: aliasedSource(tagPropPath),
},
};
}
const [tagsListRoute, allTagsDocListRoutes] = await Promise.all([
buildTagsListRoute(),
Promise.all(Object.values(versionTags).map(buildTagDocListRoute)),
]);
return _.compact([tagsListRoute, ...allTagsDocListRoutes]);
}
type BuildVersionRoutesParam = Omit<BuildAllRoutesParam, 'versions'> & {
version: FullVersion;
};
async function buildVersionRoutes(
param: BuildVersionRoutesParam,
): Promise<RouteConfig> {
const {version, actions, options, aliasedSource} = param;
async function buildVersionSubRoutes() {
const [sidebarRoute, tagsRoutes] = await Promise.all([
buildVersionSidebarRoute(param),
buildVersionTagsRoutes(param),
]);
return [sidebarRoute, ...tagsRoutes];
}
async function doBuildVersionRoutes(): Promise<RouteConfig> {
const versionProp = toVersionMetadataProp(options.id, version);
const versionPropPath = await actions.createData(
`${docuHash(`version-${version.versionName}-metadata-prop`)}.json`,
JSON.stringify(versionProp, null, 2),
);
const subRoutes = await buildVersionSubRoutes();
return {
path: version.path,
exact: false,
component: options.docVersionRootComponent,
routes: subRoutes,
modules: {
version: aliasedSource(versionPropPath),
}, },
priority: version.routePriority, priority: version.routePriority,
}); };
} }
try { try {
return await doCreateVersionRoutes(); return await doBuildVersionRoutes();
} catch (err) { } catch (err) {
logger.error`Can't create version routes for version name=${version.versionName}`; logger.error`Can't create version routes for version name=${version.versionName}`;
throw err; throw err;
} }
} }
type BuildAllRoutesParam = Omit<CreateAllRoutesParam, 'actions'> & {
actions: Omit<PluginContentLoadedActions, 'addRoute' | 'setGlobalData'>;
};
// TODO we want this buildAllRoutes function to be easily testable
// Ideally, we should avoid side effects here (ie not injecting actions)
export async function buildAllRoutes(
param: BuildAllRoutesParam,
): Promise<RouteConfig[]> {
const subRoutes = await Promise.all(
param.versions.map((version) =>
buildVersionRoutes({
...param,
version,
}),
),
);
// all docs routes are wrapped under a single parent route, this ensures
// the theme layout never unmounts/remounts when navigating between versions
return [
{
path: normalizeUrl([param.baseUrl, param.options.routeBasePath]),
exact: false,
component: param.options.docsRootComponent,
routes: subRoutes,
},
];
}
type CreateAllRoutesParam = {
baseUrl: string;
versions: FullVersion[];
options: PluginOptions;
actions: PluginContentLoadedActions;
aliasedSource: (str: string) => string;
};
export async function createAllRoutes(
param: CreateAllRoutesParam,
): Promise<void> {
const routes = await buildAllRoutes(param);
routes.forEach(param.actions.addRoute);
}

View file

@ -14,12 +14,16 @@ import {
getVersionMetadataPaths, getVersionMetadataPaths,
readVersionNames, readVersionNames,
} from './files'; } from './files';
import {createSidebarsUtils} from '../sidebars/utils';
import {getCategoryGeneratedIndexMetadataList} from '../categoryGeneratedIndex';
import type {FullVersion} from '../types';
import type {LoadContext} from '@docusaurus/types';
import type { import type {
LoadedVersion,
PluginOptions, PluginOptions,
VersionBanner, VersionBanner,
VersionMetadata, VersionMetadata,
} from '@docusaurus/plugin-content-docs'; } from '@docusaurus/plugin-content-docs';
import type {LoadContext} from '@docusaurus/types';
export type VersionContext = { export type VersionContext = {
/** The version name to get banner of. */ /** The version name to get banner of. */
@ -252,3 +256,15 @@ export async function readVersionsMetadata({
); );
return versionsMetadata; return versionsMetadata;
} }
export function toFullVersion(version: LoadedVersion): FullVersion {
const sidebarsUtils = createSidebarsUtils(version.sidebars);
return {
...version,
sidebarsUtils,
categoryGeneratedIndices: getCategoryGeneratedIndexMetadataList({
docs: version.docs,
sidebarsUtils,
}),
};
}

View file

@ -379,17 +379,17 @@ declare module '@theme/DocItem/Footer' {
export default function DocItemFooter(): JSX.Element; export default function DocItemFooter(): JSX.Element;
} }
declare module '@theme/DocPage/Layout' { declare module '@theme/DocRoot/Layout' {
import type {ReactNode} from 'react'; import type {ReactNode} from 'react';
export interface Props { export interface Props {
readonly children: ReactNode; readonly children: ReactNode;
} }
export default function DocPageLayout(props: Props): JSX.Element; export default function DocRootLayout(props: Props): JSX.Element;
} }
declare module '@theme/DocPage/Layout/Sidebar' { declare module '@theme/DocRoot/Layout/Sidebar' {
import type {Dispatch, SetStateAction} from 'react'; import type {Dispatch, SetStateAction} from 'react';
import type {PropSidebar} from '@docusaurus/plugin-content-docs'; import type {PropSidebar} from '@docusaurus/plugin-content-docs';
@ -399,20 +399,20 @@ declare module '@theme/DocPage/Layout/Sidebar' {
readonly setHiddenSidebarContainer: Dispatch<SetStateAction<boolean>>; readonly setHiddenSidebarContainer: Dispatch<SetStateAction<boolean>>;
} }
export default function DocPageLayoutSidebar(props: Props): JSX.Element; export default function DocRootLayoutSidebar(props: Props): JSX.Element;
} }
declare module '@theme/DocPage/Layout/Sidebar/ExpandButton' { declare module '@theme/DocRoot/Layout/Sidebar/ExpandButton' {
export interface Props { export interface Props {
toggleSidebar: () => void; toggleSidebar: () => void;
} }
export default function DocPageLayoutSidebarExpandButton( export default function DocRootLayoutSidebarExpandButton(
props: Props, props: Props,
): JSX.Element; ): JSX.Element;
} }
declare module '@theme/DocPage/Layout/Main' { declare module '@theme/DocRoot/Layout/Main' {
import type {ReactNode} from 'react'; import type {ReactNode} from 'react';
export interface Props { export interface Props {
@ -420,7 +420,7 @@ declare module '@theme/DocPage/Layout/Main' {
readonly children: ReactNode; readonly children: ReactNode;
} }
export default function DocPageLayoutMain(props: Props): JSX.Element; export default function DocRootLayoutMain(props: Props): JSX.Element;
} }
declare module '@theme/DocPaginator' { declare module '@theme/DocPaginator' {
@ -662,6 +662,14 @@ declare module '@theme/Heading' {
export default function Heading(props: Props): JSX.Element; export default function Heading(props: Props): JSX.Element;
} }
declare module '@theme/NotFound/Content' {
export interface Props {
readonly className?: string;
}
export default function NotFoundContent(props: Props): JSX.Element;
}
declare module '@theme/Layout' { declare module '@theme/Layout' {
import type {ReactNode} from 'react'; import type {ReactNode} from 'react';

View file

@ -1,71 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import clsx from 'clsx';
import {
HtmlClassNameProvider,
ThemeClassNames,
PageMetadata,
} from '@docusaurus/theme-common';
import {
docVersionSearchTag,
DocsSidebarProvider,
DocsVersionProvider,
useDocRouteMetadata,
} from '@docusaurus/theme-common/internal';
import DocPageLayout from '@theme/DocPage/Layout';
import NotFound from '@theme/NotFound';
import SearchMetadata from '@theme/SearchMetadata';
import type {Props} from '@theme/DocPage';
function DocPageMetadata(props: Props): JSX.Element {
const {versionMetadata} = props;
return (
<>
<SearchMetadata
version={versionMetadata.version}
tag={docVersionSearchTag(
versionMetadata.pluginId,
versionMetadata.version,
)}
/>
<PageMetadata>
{versionMetadata.noIndex && (
<meta name="robots" content="noindex, nofollow" />
)}
</PageMetadata>
</>
);
}
export default function DocPage(props: Props): JSX.Element {
const {versionMetadata} = props;
const currentDocRouteMetadata = useDocRouteMetadata(props);
if (!currentDocRouteMetadata) {
return <NotFound />;
}
const {docElement, sidebarName, sidebarItems} = currentDocRouteMetadata;
return (
<>
<DocPageMetadata {...props} />
<HtmlClassNameProvider
className={clsx(
// TODO: it should be removed from here
ThemeClassNames.wrapper.docsPages,
ThemeClassNames.page.docsDocPage,
props.versionMetadata.className,
)}>
<DocsVersionProvider version={versionMetadata}>
<DocsSidebarProvider name={sidebarName} items={sidebarItems}>
<DocPageLayout>{docElement}</DocPageLayout>
</DocsSidebarProvider>
</DocsVersionProvider>
</HtmlClassNameProvider>
</>
);
}

View file

@ -8,11 +8,11 @@
import React from 'react'; import React from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import {useDocsSidebar} from '@docusaurus/theme-common/internal'; import {useDocsSidebar} from '@docusaurus/theme-common/internal';
import type {Props} from '@theme/DocPage/Layout/Main'; import type {Props} from '@theme/DocRoot/Layout/Main';
import styles from './styles.module.css'; import styles from './styles.module.css';
export default function DocPageLayoutMain({ export default function DocRootLayoutMain({
hiddenSidebarContainer, hiddenSidebarContainer,
children, children,
}: Props): JSX.Element { }: Props): JSX.Element {

View file

@ -8,11 +8,11 @@
import React from 'react'; import React from 'react';
import {translate} from '@docusaurus/Translate'; import {translate} from '@docusaurus/Translate';
import IconArrow from '@theme/Icon/Arrow'; import IconArrow from '@theme/Icon/Arrow';
import type {Props} from '@theme/DocPage/Layout/Sidebar/ExpandButton'; import type {Props} from '@theme/DocRoot/Layout/Sidebar/ExpandButton';
import styles from './styles.module.css'; import styles from './styles.module.css';
export default function DocPageLayoutSidebarExpandButton({ export default function DocRootLayoutSidebarExpandButton({
toggleSidebar, toggleSidebar,
}: Props): JSX.Element { }: Props): JSX.Element {
return ( return (

View file

@ -11,8 +11,8 @@ import {ThemeClassNames} from '@docusaurus/theme-common';
import {useDocsSidebar} from '@docusaurus/theme-common/internal'; import {useDocsSidebar} from '@docusaurus/theme-common/internal';
import {useLocation} from '@docusaurus/router'; import {useLocation} from '@docusaurus/router';
import DocSidebar from '@theme/DocSidebar'; import DocSidebar from '@theme/DocSidebar';
import ExpandButton from '@theme/DocPage/Layout/Sidebar/ExpandButton'; import ExpandButton from '@theme/DocRoot/Layout/Sidebar/ExpandButton';
import type {Props} from '@theme/DocPage/Layout/Sidebar'; import type {Props} from '@theme/DocRoot/Layout/Sidebar';
import styles from './styles.module.css'; import styles from './styles.module.css';
@ -28,7 +28,7 @@ function ResetOnSidebarChange({children}: {children: ReactNode}) {
); );
} }
export default function DocPageLayoutSidebar({ export default function DocRootLayoutSidebar({
sidebar, sidebar,
hiddenSidebarContainer, hiddenSidebarContainer,
setHiddenSidebarContainer, setHiddenSidebarContainer,

View file

@ -7,32 +7,31 @@
import React, {useState} from 'react'; import React, {useState} from 'react';
import {useDocsSidebar} from '@docusaurus/theme-common/internal'; import {useDocsSidebar} from '@docusaurus/theme-common/internal';
import Layout from '@theme/Layout';
import BackToTopButton from '@theme/BackToTopButton'; import BackToTopButton from '@theme/BackToTopButton';
import DocPageLayoutSidebar from '@theme/DocPage/Layout/Sidebar'; import DocRootLayoutSidebar from '@theme/DocRoot/Layout/Sidebar';
import DocPageLayoutMain from '@theme/DocPage/Layout/Main'; import DocRootLayoutMain from '@theme/DocRoot/Layout/Main';
import type {Props} from '@theme/DocPage/Layout'; import type {Props} from '@theme/DocRoot/Layout';
import styles from './styles.module.css'; import styles from './styles.module.css';
export default function DocPageLayout({children}: Props): JSX.Element { export default function DocRootLayout({children}: Props): JSX.Element {
const sidebar = useDocsSidebar(); const sidebar = useDocsSidebar();
const [hiddenSidebarContainer, setHiddenSidebarContainer] = useState(false); const [hiddenSidebarContainer, setHiddenSidebarContainer] = useState(false);
return ( return (
<Layout wrapperClassName={styles.docsWrapper}> <div className={styles.docsWrapper}>
<BackToTopButton /> <BackToTopButton />
<div className={styles.docPage}> <div className={styles.docRoot}>
{sidebar && ( {sidebar && (
<DocPageLayoutSidebar <DocRootLayoutSidebar
sidebar={sidebar.items} sidebar={sidebar.items}
hiddenSidebarContainer={hiddenSidebarContainer} hiddenSidebarContainer={hiddenSidebarContainer}
setHiddenSidebarContainer={setHiddenSidebarContainer} setHiddenSidebarContainer={setHiddenSidebarContainer}
/> />
)} )}
<DocPageLayoutMain hiddenSidebarContainer={hiddenSidebarContainer}> <DocRootLayoutMain hiddenSidebarContainer={hiddenSidebarContainer}>
{children} {children}
</DocPageLayoutMain> </DocRootLayoutMain>
</div>
</div> </div>
</Layout>
); );
} }

View file

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
.docPage { .docRoot {
display: flex; display: flex;
width: 100%; width: 100%;
} }

View file

@ -0,0 +1,34 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import clsx from 'clsx';
import {HtmlClassNameProvider, ThemeClassNames} from '@docusaurus/theme-common';
import {
DocsSidebarProvider,
useDocRootMetadata,
} from '@docusaurus/theme-common/internal';
import DocRootLayout from '@theme/DocRoot/Layout';
import NotFoundContent from '@theme/NotFound/Content';
import type {Props} from '@theme/DocRoot';
export default function DocRoot(props: Props): JSX.Element {
const currentDocRouteMetadata = useDocRootMetadata(props);
if (!currentDocRouteMetadata) {
// We only render the not found content to avoid a double layout
// see https://github.com/facebook/docusaurus/pull/7966#pullrequestreview-1077276692
return <NotFoundContent />;
}
const {docElement, sidebarName, sidebarItems} = currentDocRouteMetadata;
return (
<HtmlClassNameProvider className={clsx(ThemeClassNames.page.docsDocPage)}>
<DocsSidebarProvider name={sidebarName} items={sidebarItems}>
<DocRootLayout>{docElement}</DocRootLayout>
</DocsSidebarProvider>
</HtmlClassNameProvider>
);
}

View file

@ -15,7 +15,6 @@ import {
usePluralForm, usePluralForm,
} from '@docusaurus/theme-common'; } from '@docusaurus/theme-common';
import Translate, {translate} from '@docusaurus/Translate'; import Translate, {translate} from '@docusaurus/Translate';
import Layout from '@theme/Layout';
import SearchMetadata from '@theme/SearchMetadata'; import SearchMetadata from '@theme/SearchMetadata';
import type {Props} from '@theme/DocTagDocListPage'; import type {Props} from '@theme/DocTagDocListPage';
@ -37,6 +36,18 @@ function useNDocsTaggedPlural() {
); );
} }
function usePageTitle(props: Props): string {
const nDocsTaggedPlural = useNDocsTaggedPlural();
return translate(
{
id: 'theme.docs.tagDocListPageTitle',
description: 'The title of the page for a docs tag',
message: '{nDocsTagged} with "{tagName}"',
},
{nDocsTagged: nDocsTaggedPlural(props.tag.count), tagName: props.tag.label},
);
}
function DocItem({doc}: {doc: Props['tag']['items'][number]}): JSX.Element { function DocItem({doc}: {doc: Props['tag']['items'][number]}): JSX.Element {
return ( return (
<article className="margin-vert--lg"> <article className="margin-vert--lg">
@ -48,26 +59,24 @@ function DocItem({doc}: {doc: Props['tag']['items'][number]}): JSX.Element {
); );
} }
export default function DocTagDocListPage({tag}: Props): JSX.Element { function DocTagDocListPageMetadata({
const nDocsTaggedPlural = useNDocsTaggedPlural(); title,
const title = translate( }: Props & {title: string}): JSX.Element {
{
id: 'theme.docs.tagDocListPageTitle',
description: 'The title of the page for a docs tag',
message: '{nDocsTagged} with "{tagName}"',
},
{nDocsTagged: nDocsTaggedPlural(tag.count), tagName: tag.label},
);
return ( return (
<HtmlClassNameProvider <>
className={clsx(
ThemeClassNames.wrapper.docsPages,
ThemeClassNames.page.docsTagDocListPage,
)}>
<PageMetadata title={title} /> <PageMetadata title={title} />
<SearchMetadata tag="doc_tag_doc_list" /> <SearchMetadata tag="doc_tag_doc_list" />
<Layout> </>
);
}
function DocTagDocListPageContent({
tag,
title,
}: Props & {title: string}): JSX.Element {
return (
<HtmlClassNameProvider
className={clsx(ThemeClassNames.page.docsTagDocListPage)}>
<div className="container margin-vert--lg"> <div className="container margin-vert--lg">
<div className="row"> <div className="row">
<main className="col col--8 col--offset-2"> <main className="col col--8 col--offset-2">
@ -89,7 +98,16 @@ export default function DocTagDocListPage({tag}: Props): JSX.Element {
</main> </main>
</div> </div>
</div> </div>
</Layout>
</HtmlClassNameProvider> </HtmlClassNameProvider>
); );
} }
export default function DocTagDocListPage(props: Props): JSX.Element {
const title = usePageTitle(props);
return (
<>
<DocTagDocListPageMetadata {...props} title={title} />
<DocTagDocListPageContent {...props} title={title} />
</>
);
}

View file

@ -13,22 +13,28 @@ import {
ThemeClassNames, ThemeClassNames,
translateTagsPageTitle, translateTagsPageTitle,
} from '@docusaurus/theme-common'; } from '@docusaurus/theme-common';
import Layout from '@theme/Layout';
import TagsListByLetter from '@theme/TagsListByLetter'; import TagsListByLetter from '@theme/TagsListByLetter';
import SearchMetadata from '@theme/SearchMetadata'; import SearchMetadata from '@theme/SearchMetadata';
import type {Props} from '@theme/DocTagsListPage'; import type {Props} from '@theme/DocTagsListPage';
export default function DocTagsListPage({tags}: Props): JSX.Element { function DocTagsListPageMetadata({
const title = translateTagsPageTitle(); title,
}: Props & {title: string}): JSX.Element {
return ( return (
<HtmlClassNameProvider <>
className={clsx(
ThemeClassNames.wrapper.docsPages,
ThemeClassNames.page.docsTagsListPage,
)}>
<PageMetadata title={title} /> <PageMetadata title={title} />
<SearchMetadata tag="doc_tags_list" /> <SearchMetadata tag="doc_tags_list" />
<Layout> </>
);
}
function DocTagsListPageContent({
tags,
title,
}: Props & {title: string}): JSX.Element {
return (
<HtmlClassNameProvider
className={clsx(ThemeClassNames.page.docsTagsListPage)}>
<div className="container margin-vert--lg"> <div className="container margin-vert--lg">
<div className="row"> <div className="row">
<main className="col col--8 col--offset-2"> <main className="col col--8 col--offset-2">
@ -37,7 +43,16 @@ export default function DocTagsListPage({tags}: Props): JSX.Element {
</main> </main>
</div> </div>
</div> </div>
</Layout>
</HtmlClassNameProvider> </HtmlClassNameProvider>
); );
} }
export default function DocTagsListPage(props: Props): JSX.Element {
const title = translateTagsPageTitle();
return (
<>
<DocTagsListPageMetadata {...props} title={title} />
<DocTagsListPageContent {...props} title={title} />
</>
);
}

View file

@ -0,0 +1,51 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import {HtmlClassNameProvider, PageMetadata} from '@docusaurus/theme-common';
import {
docVersionSearchTag,
DocsVersionProvider,
} from '@docusaurus/theme-common/internal';
import renderRoutes from '@docusaurus/renderRoutes';
import SearchMetadata from '@theme/SearchMetadata';
import type {Props} from '@theme/DocVersionRoot';
function DocVersionRootMetadata(props: Props): JSX.Element {
const {version} = props;
return (
<>
<SearchMetadata
version={version.version}
tag={docVersionSearchTag(version.pluginId, version.version)}
/>
<PageMetadata>
{version.noIndex && <meta name="robots" content="noindex, nofollow" />}
</PageMetadata>
</>
);
}
function DocVersionRootContent(props: Props): JSX.Element {
const {version, route} = props;
return (
<HtmlClassNameProvider className={version.className}>
<DocsVersionProvider version={version}>
{renderRoutes(route.routes!)}
</DocsVersionProvider>
</HtmlClassNameProvider>
);
}
export default function DocVersionRoot(props: Props): JSX.Element {
return (
<>
<DocVersionRootMetadata {...props} />
<DocVersionRootContent {...props} />
</>
);
}

View file

@ -0,0 +1,22 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import clsx from 'clsx';
import {ThemeClassNames, HtmlClassNameProvider} from '@docusaurus/theme-common';
import renderRoutes from '@docusaurus/renderRoutes';
import Layout from '@theme/Layout';
import type {Props} from '@theme/DocVersionRoot';
export default function DocsRoot(props: Props): JSX.Element {
return (
<HtmlClassNameProvider className={clsx(ThemeClassNames.wrapper.docsPages)}>
<Layout>{renderRoutes(props.route.routes!)}</Layout>
</HtmlClassNameProvider>
);
}

View file

@ -1,54 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import Translate, {translate} from '@docusaurus/Translate';
import {PageMetadata} from '@docusaurus/theme-common';
import Layout from '@theme/Layout';
export default function NotFound(): JSX.Element {
return (
<>
<PageMetadata
title={translate({
id: 'theme.NotFound.title',
message: 'Page Not Found',
})}
/>
<Layout>
<main className="container margin-vert--xl">
<div className="row">
<div className="col col--6 col--offset-3">
<h1 className="hero__title">
<Translate
id="theme.NotFound.title"
description="The title of the 404 page">
Page Not Found
</Translate>
</h1>
<p>
<Translate
id="theme.NotFound.p1"
description="The first paragraph of the 404 page">
We could not find what you were looking for.
</Translate>
</p>
<p>
<Translate
id="theme.NotFound.p2"
description="The 2nd paragraph of the 404 page">
Please contact the owner of the site that linked you to the
original URL and let them know their link is broken.
</Translate>
</p>
</div>
</div>
</main>
</Layout>
</>
);
}

View file

@ -0,0 +1,44 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import clsx from 'clsx';
import Translate from '@docusaurus/Translate';
import type {Props} from '@theme/NotFound/Content';
export default function NotFoundContent({className}: Props): JSX.Element {
return (
<main className={clsx('container margin-vert--xl', className)}>
<div className="row">
<div className="col col--6 col--offset-3">
<h1 className="hero__title">
<Translate
id="theme.NotFound.title"
description="The title of the 404 page">
Page Not Found
</Translate>
</h1>
<p>
<Translate
id="theme.NotFound.p1"
description="The first paragraph of the 404 page">
We could not find what you were looking for.
</Translate>
</p>
<p>
<Translate
id="theme.NotFound.p2"
description="The 2nd paragraph of the 404 page">
Please contact the owner of the site that linked you to the
original URL and let them know their link is broken.
</Translate>
</p>
</div>
</div>
</main>
);
}

View file

@ -0,0 +1,27 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import {translate} from '@docusaurus/Translate';
import {PageMetadata} from '@docusaurus/theme-common';
import Layout from '@theme/Layout';
import NotFoundContent from '@theme/NotFound/Content';
export default function Index(): JSX.Element {
const title = translate({
id: 'theme.NotFound.title',
message: 'Page Not Found',
});
return (
<>
<PageMetadata title={title} />
<Layout>
<NotFoundContent />
</Layout>
</>
);
}

View file

@ -73,7 +73,7 @@ export {
useDocsVersionCandidates, useDocsVersionCandidates,
useLayoutDoc, useLayoutDoc,
useLayoutDocsSidebar, useLayoutDocsSidebar,
useDocRouteMetadata, useDocRootMetadata,
} from './utils/docsUtils'; } from './utils/docsUtils';
export {useTitleFormatter} from './utils/generalUtils'; export {useTitleFormatter} from './utils/generalUtils';

View file

@ -27,6 +27,9 @@ export const ThemeClassNames = {
}, },
wrapper: { wrapper: {
main: 'main-wrapper', main: 'main-wrapper',
// TODO these wrapper class names are now quite useless
// TODO do breaking change later in 3.0
// we already add plugin name/id class on <html>: that's enough
blogPages: 'blog-wrapper', blogPages: 'blog-wrapper',
docsPages: 'docs-wrapper', docsPages: 'docs-wrapper',
mdxPages: 'mdx-wrapper', mdxPages: 'mdx-wrapper',

View file

@ -17,7 +17,7 @@ import {
type GlobalSidebar, type GlobalSidebar,
type GlobalDoc, type GlobalDoc,
} from '@docusaurus/plugin-content-docs/client'; } from '@docusaurus/plugin-content-docs/client';
import type {Props as DocPageProps} from '@theme/DocPage'; import type {Props as DocRootProps} from '@theme/DocRoot';
import {useDocsPreferredVersion} from '../contexts/docsPreferredVersion'; import {useDocsPreferredVersion} from '../contexts/docsPreferredVersion';
import {useDocsVersion} from '../contexts/docsVersion'; import {useDocsVersion} from '../contexts/docsVersion';
import {useDocsSidebar} from '../contexts/docsSidebar'; import {useDocsSidebar} from '../contexts/docsSidebar';
@ -290,14 +290,11 @@ Available doc ids are:
* version metadata, and the subroutes creating individual doc pages. This hook * version metadata, and the subroutes creating individual doc pages. This hook
* will match the current location against all known sub-routes. * will match the current location against all known sub-routes.
* *
* @param props The props received by `@theme/DocPage` * @param props The props received by `@theme/DocRoot`
* @returns The data of the relevant document at the current location, or `null` * @returns The data of the relevant document at the current location, or `null`
* if no document associated with the current location can be found. * if no document associated with the current location can be found.
*/ */
export function useDocRouteMetadata({ export function useDocRootMetadata({route}: DocRootProps): null | {
route,
versionMetadata,
}: DocPageProps): null | {
/** The element that should be rendered at the current location. */ /** The element that should be rendered at the current location. */
docElement: JSX.Element; docElement: JSX.Element;
/** /**
@ -309,6 +306,7 @@ export function useDocRouteMetadata({
sidebarItems: PropSidebar | undefined; sidebarItems: PropSidebar | undefined;
} { } {
const location = useLocation(); const location = useLocation();
const versionMetadata = useDocsVersion();
const docRoutes = route.routes!; const docRoutes = route.routes!;
const currentDocRoute = docRoutes.find((docRoute) => const currentDocRoute = docRoutes.find((docRoute) =>
matchPath(location.pathname, docRoute), matchPath(location.pathname, docRoute),

View file

@ -60,7 +60,7 @@ exports[`loadRoutes loads nested route config 1`] = `
{ {
"registry": { "registry": {
"__comp---theme-doc-item-178-a40": "@theme/DocItem", "__comp---theme-doc-item-178-a40": "@theme/DocItem",
"__comp---theme-doc-page-1-be-9be": "@theme/DocPage", "__comp---theme-doc-roota-94-67a": "@theme/DocRoot",
"content---docs-foo-baz-8-ce-61e": "docs/foo/baz.md", "content---docs-foo-baz-8-ce-61e": "docs/foo/baz.md",
"content---docs-helloaff-811": "docs/hello.md", "content---docs-helloaff-811": "docs/hello.md",
"docsMetadata---docs-routef-34-881": "docs-b5f.json", "docsMetadata---docs-routef-34-881": "docs-b5f.json",
@ -77,8 +77,8 @@ exports[`loadRoutes loads nested route config 1`] = `
"content": "content---docs-helloaff-811", "content": "content---docs-helloaff-811",
"metadata": "metadata---docs-hello-956-741", "metadata": "metadata---docs-hello-956-741",
}, },
"/docs:route-502": { "/docs:route-9d0": {
"__comp": "__comp---theme-doc-page-1-be-9be", "__comp": "__comp---theme-doc-roota-94-67a",
"docsMetadata": "docsMetadata---docs-routef-34-881", "docsMetadata": "docsMetadata---docs-routef-34-881",
}, },
"docs/foo/baz-eb2": { "docs/foo/baz-eb2": {
@ -96,7 +96,7 @@ import ComponentCreator from '@docusaurus/ComponentCreator';
export default [ export default [
{ {
path: '/docs:route', path: '/docs:route',
component: ComponentCreator('/docs:route', '502'), component: ComponentCreator('/docs:route', '9d0'),
routes: [ routes: [
{ {
path: '/docs/hello', path: '/docs/hello',

View file

@ -103,7 +103,7 @@ describe('handleDuplicateRoutes', () => {
describe('loadRoutes', () => { describe('loadRoutes', () => {
it('loads nested route config', () => { it('loads nested route config', () => {
const nestedRouteConfig: RouteConfig = { const nestedRouteConfig: RouteConfig = {
component: '@theme/DocPage', component: '@theme/DocRoot',
path: '/docs:route', path: '/docs:route',
modules: { modules: {
docsMetadata: 'docs-b5f.json', docsMetadata: 'docs-b5f.json',

View file

@ -103,3 +103,45 @@ exports[`sortConfig sorts route config given a baseURL 1`] = `
}, },
] ]
`; `;
exports[`sortConfig sorts route config recursively 1`] = `
[
{
"component": "",
"exact": true,
"path": "/some/page",
},
{
"component": "",
"path": "/docs",
"routes": [
{
"component": "",
"exact": true,
"path": "/docs/tags",
},
{
"component": "",
"exact": true,
"path": "/docs/tags/someTag",
},
{
"component": "",
"path": "/docs",
"routes": [
{
"component": "",
"exact": true,
"path": "/docs/doc1",
},
{
"component": "",
"exact": true,
"path": "/docs/doc2",
},
],
},
],
},
]
`;

View file

@ -207,6 +207,52 @@ describe('sortConfig', () => {
expect(routes).toMatchSnapshot(); expect(routes).toMatchSnapshot();
}); });
it('sorts route config recursively', () => {
const routes: RouteConfig[] = [
{
path: '/docs',
component: '',
routes: [
{
path: '/docs/tags',
component: '',
exact: true,
},
{
path: '/docs',
component: '',
routes: [
{
path: '/docs/doc1',
component: '',
exact: true,
},
{
path: '/docs/doc2',
component: '',
exact: true,
},
],
},
{
path: '/docs/tags/someTag',
component: '',
exact: true,
},
],
},
{
path: '/some/page',
component: '',
exact: true,
},
];
sortConfig(routes);
expect(routes).toMatchSnapshot();
});
it('sorts route config given a baseURL', () => { it('sorts route config given a baseURL', () => {
const baseURL = '/latest/'; const baseURL = '/latest/';
const routes: RouteConfig[] = [ const routes: RouteConfig[] = [

View file

@ -63,6 +63,8 @@ export function sortConfig(
}); });
routeConfigs.forEach((routeConfig) => { routeConfigs.forEach((routeConfig) => {
routeConfig.routes?.sort((a, b) => a.path.localeCompare(b.path)); if (routeConfig.routes) {
sortConfig(routeConfig.routes, baseUrl);
}
}); });
} }

View file

@ -46,7 +46,9 @@ Accepted fields:
| `sidebarCollapsed` | `boolean` | `true` | Whether sidebar categories are collapsed by default. See also [Expanded categories by default](/docs/sidebar#expanded-categories-by-default) | | `sidebarCollapsed` | `boolean` | `true` | Whether sidebar categories are collapsed by default. See also [Expanded categories by default](/docs/sidebar#expanded-categories-by-default) |
| `sidebarItemsGenerator` | <a href="#SidebarGenerator"><code>SidebarGenerator</code></a> | _Omitted_ | Function used to replace the sidebar items of type `'autogenerated'` with real sidebar items (docs, categories, links...). See also [Customize the sidebar items generator](/docs/sidebar#customize-the-sidebar-items-generator) | | `sidebarItemsGenerator` | <a href="#SidebarGenerator"><code>SidebarGenerator</code></a> | _Omitted_ | Function used to replace the sidebar items of type `'autogenerated'` with real sidebar items (docs, categories, links...). See also [Customize the sidebar items generator](/docs/sidebar#customize-the-sidebar-items-generator) |
| `numberPrefixParser` | <code>boolean \|</code> <a href="#PrefixParser"><code>PrefixParser</code></a> | _Omitted_ | Custom parsing logic to extract number prefixes from file names. Use `false` to disable this behavior and leave the docs untouched, and `true` to use the default parser. See also [Using number prefixes](/docs/sidebar#using-number-prefixes) | | `numberPrefixParser` | <code>boolean \|</code> <a href="#PrefixParser"><code>PrefixParser</code></a> | _Omitted_ | Custom parsing logic to extract number prefixes from file names. Use `false` to disable this behavior and leave the docs untouched, and `true` to use the default parser. See also [Using number prefixes](/docs/sidebar#using-number-prefixes) |
| `docLayoutComponent` | `string` | `'@theme/DocPage'` | Root layout component of each doc page. Provides the version data context, and is not unmounted when switching docs. | | `docsRootComponent` | `string` | `'@theme/DocsRoot'` | Parent component of all the docs plugin pages (including all versions). Stays mounted when navigation between docs pages and versions. |
| `docVersionRootComponent` | `string` | `'@theme/DocVersionLayout'` | Parent component of all docs pages of an individual version (doc pages with sidebars, tags pages). Stays mounted when navigation between pages of that specific version. |
| `docRootComponent` | `string` | `'@theme/DocPage'` | Parent component of all doc pages with sidebars (regular docs pages, category generated index pages). Stays mounted when navigation between such pages. |
| `docItemComponent` | `string` | `'@theme/DocItem'` | Main doc container, with TOC, pagination, etc. | | `docItemComponent` | `string` | `'@theme/DocItem'` | Main doc container, with TOC, pagination, etc. |
| `docTagsListComponent` | `string` | `'@theme/DocTagsListPage'` | Root component of the tags list page | | `docTagsListComponent` | `string` | `'@theme/DocTagsListPage'` | Root component of the tags list page |
| `docTagDocListComponent` | `string` | `'@theme/DocTagDocListPage'` | Root component of the "docs containing tag X" page. | | `docTagDocListComponent` | `string` | `'@theme/DocTagDocListPage'` | Root component of the "docs containing tag X" page. |