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 11d22b0c99..321cb87ce3 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 @@ -52,6 +52,15 @@ Object { exports[`simple website content 2`] = ` Array [ + Object { + "component": "@theme/DocPage", + "exact": true, + "modules": Object { + "content": "@site/docs/hello.md", + "docsMetadata": "~docs/site-docs-hello-md-9df-base.json", + }, + "path": "/docs", + }, Object { "component": "@theme/DocPage", "modules": Object { @@ -107,6 +116,33 @@ Array [ exports[`versioned website content 1`] = ` Array [ + Object { + "component": "@theme/DocPage", + "exact": true, + "modules": Object { + "content": "@site/versioned_docs/version-1.0.1/hello.md", + "docsMetadata": "~docs/site-versioned-docs-version-1-0-1-hello-md-0c7-base.json", + }, + "path": "/docs", + }, + Object { + "component": "@theme/DocPage", + "exact": true, + "modules": Object { + "content": "@site/versioned_docs/version-1.0.0/hello.md", + "docsMetadata": "~docs/site-versioned-docs-version-1-0-0-hello-md-3ef-base.json", + }, + "path": "/docs/1.0.0", + }, + Object { + "component": "@theme/DocPage", + "exact": true, + "modules": Object { + "content": "@site/docs/hello.md", + "docsMetadata": "~docs/site-docs-hello-md-9df-base.json", + }, + "path": "/docs/next", + }, Object { "component": "@theme/DocPage", "modules": Object { diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts index d727bf6a29..e3ff1f1dda 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts @@ -93,6 +93,7 @@ describe('simple website', () => { const plugin = pluginContentDocs(context, { path: pluginPath, sidebarPath, + homePageId: 'hello', }); const pluginContentDir = path.join(context.generatedFilesDir, plugin.name); @@ -203,6 +204,9 @@ describe('simple website', () => { expect(baseMetadata.docsSidebars).toEqual(docsSidebars); expect(baseMetadata.permalinkToSidebar).toEqual(permalinkToSidebar); + // Sort the route config like in src/server/plugins/index.ts for consistent snapshot ordering + sortConfig(routeConfigs); + expect(routeConfigs).not.toEqual([]); expect(routeConfigs).toMatchSnapshot(); }); @@ -216,6 +220,7 @@ describe('versioned website', () => { const plugin = pluginContentDocs(context, { routeBasePath, sidebarPath, + homePageId: 'hello', }); const env = loadEnv(siteDir); const {docsDir: versionedDir} = env.versioning; diff --git a/packages/docusaurus-plugin-content-docs/src/index.ts b/packages/docusaurus-plugin-content-docs/src/index.ts index 30f01730fc..6ab24b3084 100644 --- a/packages/docusaurus-plugin-content-docs/src/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/index.ts @@ -48,9 +48,12 @@ import {Configuration} from 'webpack'; import {docsVersion} from './version'; import {VERSIONS_JSON_FILE} from './constants'; +const REVERSED_DOCS_HOME_PAGE_ID = '_index'; + const DEFAULT_OPTIONS: PluginOptions = { path: 'docs', // Path to data on filesystem, relative to site dir. routeBasePath: 'docs', // URL Route. + homePageId: REVERSED_DOCS_HOME_PAGE_ID, // Document id for docs home page. include: ['**/*.{md,mdx}'], // Extensions to include. sidebarPath: '', // Path to sidebar configuration for showing a list of markdown pages. docLayoutComponent: '@theme/DocPage', @@ -313,28 +316,96 @@ export default function pluginContentDocs( const aliasedSource = (source: string) => `~docs/${path.relative(dataDir, source)}`; + const createDocsBaseMetadata = (version?: string): DocsBaseMetadata => { + const {docsSidebars, permalinkToSidebar, versionToSidebars} = content; + const neededSidebars: Set = + versionToSidebars[version!] || new Set(); + + return { + docsSidebars: version + ? pick(docsSidebars, Array.from(neededSidebars)) + : docsSidebars, + permalinkToSidebar: version + ? pickBy(permalinkToSidebar, (sidebar) => + neededSidebars.has(sidebar), + ) + : permalinkToSidebar, + version, + }; + }; + const genRoutes = async ( metadataItems: Metadata[], ): Promise => { - const routes = await Promise.all( - metadataItems.map(async (metadataItem) => { - await createData( - // Note that this created data path must be in sync with - // metadataPath provided to mdx-loader. - `${docuHash(metadataItem.source)}.json`, - JSON.stringify(metadataItem, null, 2), + const routes: RouteConfig[] = []; + + await metadataItems.forEach(async (metadataItem, i) => { + const isDocsHomePage = + metadataItem.id.substr(metadataItem.id.indexOf('/') + 1) === + options.homePageId; + + if (isDocsHomePage) { + const homeDocsRoutePath = + routeBasePath === '' ? '/' : routeBasePath; + const versionDocsPathPrefix = + (metadataItem?.version === versioning.latestVersion + ? '' + : metadataItem.version!) ?? ''; + + // To show the sidebar, get the sidebar key of available sibling item. + metadataItem.sidebar = ( + metadataItems[i - 1] ?? metadataItems[i + 1] + ).sidebar; + const docsBaseMetadata = createDocsBaseMetadata( + metadataItem.version!, + ); + docsBaseMetadata.isHomePage = true; + docsBaseMetadata.homePagePath = normalizeUrl([ + baseUrl, + homeDocsRoutePath, + versionDocsPathPrefix, + options.homePageId, + ]); + const docsBaseMetadataPath = await createData( + `${docuHash(metadataItem.source)}-base.json`, + JSON.stringify(docsBaseMetadata, null, 2), ); - return { + // Add a route for docs home page. + addRoute({ + path: normalizeUrl([ + baseUrl, + homeDocsRoutePath, + versionDocsPathPrefix, + ]), + component: docLayoutComponent, + exact: true, + modules: { + docsMetadata: aliasedSource(docsBaseMetadataPath), + content: metadataItem.source, + }, + }); + } + + await createData( + // Note that this created data path must be in sync with + // metadataPath provided to mdx-loader. + `${docuHash(metadataItem.source)}.json`, + JSON.stringify(metadataItem, null, 2), + ); + + // Do not create a route for a page created specifically for docs home page. + if (metadataItem.id !== REVERSED_DOCS_HOME_PAGE_ID) { + routes.push({ path: metadataItem.permalink, component: docItemComponent, exact: true, modules: { content: metadataItem.source, }, - }; - }), - ); + }); + } + }); return routes.sort((a, b) => a.path > b.path ? 1 : b.path > a.path ? -1 : 0, @@ -383,19 +454,7 @@ export default function pluginContentDocs( isLatestVersion ? '' : version, ]); const docsBaseRoute = normalizeUrl([docsBasePermalink, ':route']); - const neededSidebars: Set = - content.versionToSidebars[version] || new Set(); - const docsBaseMetadata: DocsBaseMetadata = { - docsSidebars: pick( - content.docsSidebars, - Array.from(neededSidebars), - ), - permalinkToSidebar: pickBy( - content.permalinkToSidebar, - (sidebar) => neededSidebars.has(sidebar), - ), - version, - }; + const docsBaseMetadata = createDocsBaseMetadata(version); // We want latest version route config to be placed last in the // generated routeconfig. Otherwise, `/docs/next/foo` will match @@ -410,16 +469,31 @@ export default function pluginContentDocs( ); } else { const routes = await genRoutes(Object.values(content.docsMetadata)); - const docsBaseMetadata: DocsBaseMetadata = { - docsSidebars: content.docsSidebars, - permalinkToSidebar: content.permalinkToSidebar, - }; + const docsBaseMetadata = createDocsBaseMetadata(); const docsBaseRoute = normalizeUrl([baseUrl, routeBasePath, ':route']); return addBaseRoute(docsBaseRoute, docsBaseMetadata, routes); } }, + async routesLoaded(routes) { + const normalizedHomeDocsRoutePath = `/${options.routeBasePath}`; + const homeDocsRoutes = routes.filter( + (routeConfig) => routeConfig.path === normalizedHomeDocsRoutePath, + ); + + // Remove the route for docs home page if there is a page with the same path (i.e. docs). + if (homeDocsRoutes.length > 1) { + const docsHomePageRouteIndex = routes.findIndex( + (route) => + route.component === options.docLayoutComponent && + route.path === normalizedHomeDocsRoutePath, + ); + + delete routes[docsHomePageRouteIndex!]; + } + }, + configureWebpack(_config, isServer, utils) { const {getBabelLoader, getCacheLoader} = utils; const {rehypePlugins, remarkPlugins} = options; diff --git a/packages/docusaurus-plugin-content-docs/src/types.ts b/packages/docusaurus-plugin-content-docs/src/types.ts index ee5ecce114..16c3a034ad 100644 --- a/packages/docusaurus-plugin-content-docs/src/types.ts +++ b/packages/docusaurus-plugin-content-docs/src/types.ts @@ -24,6 +24,7 @@ export interface PluginOptions extends MetadataOptions, PathOptions { remarkPlugins: ([Function, object] | Function)[]; rehypePlugins: string[]; admonitions: any; + homePageId: string; } export type SidebarItemDoc = { @@ -160,6 +161,8 @@ export type DocsBaseMetadata = Pick< 'docsSidebars' | 'permalinkToSidebar' > & { version?: string; + isHomePage?: boolean; + homePagePath?: string; }; export type VersioningEnv = { diff --git a/packages/docusaurus-theme-classic/src/theme/DocPage/index.js b/packages/docusaurus-theme-classic/src/theme/DocPage/index.js index 5f432ea0e0..6173f77af4 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocPage/index.js +++ b/packages/docusaurus-theme-classic/src/theme/DocPage/index.js @@ -11,6 +11,7 @@ import {MDXProvider} from '@mdx-js/react'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import renderRoutes from '@docusaurus/renderRoutes'; import Layout from '@theme/Layout'; +import DocItem from '@theme/DocItem'; import DocSidebar from '@theme/DocSidebar'; import MDXComponents from '@theme/MDXComponents'; import NotFound from '@theme/NotFound'; @@ -19,21 +20,31 @@ import {matchPath} from '@docusaurus/router'; import styles from './styles.module.css'; function DocPage(props) { - const {route: baseRoute, docsMetadata, location} = props; - // case-sensitive route such as it is defined in the sidebar - const currentRoute = - baseRoute.routes.find((route) => { - return matchPath(location.pathname, route); - }) || {}; - const {permalinkToSidebar, docsSidebars, version} = docsMetadata; - const sidebar = permalinkToSidebar[currentRoute.path]; + const {route: baseRoute, docsMetadata, location, content} = props; const { - siteConfig: {themeConfig = {}} = {}, + permalinkToSidebar, + docsSidebars, + version, + isHomePage, + homePagePath, + } = docsMetadata; + + // Get case-sensitive route such as it is defined in the sidebar. + const currentRoute = !isHomePage + ? baseRoute.routes.find((route) => { + return matchPath(location.pathname, route); + }) || {} + : {}; + + const sidebar = isHomePage + ? content.metadata.sidebar + : permalinkToSidebar[currentRoute.path]; + const { + siteConfig: {themeConfig: {sidebarCollapsible = true} = {}} = {}, isClient, } = useDocusaurusContext(); - const {sidebarCollapsible = true} = themeConfig; - if (Object.keys(currentRoute).length === 0) { + if (!isHomePage && Object.keys(currentRoute).length === 0) { return ; } @@ -44,7 +55,7 @@ function DocPage(props) {
@@ -52,7 +63,11 @@ function DocPage(props) { )}
- {renderRoutes(baseRoute.routes)} + {isHomePage ? ( + + ) : ( + renderRoutes(baseRoute.routes) + )}
diff --git a/packages/docusaurus-theme-classic/src/theme/DocSidebar/index.js b/packages/docusaurus-theme-classic/src/theme/DocSidebar/index.js index 7a247b5254..30b3bceaed 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocSidebar/index.js +++ b/packages/docusaurus-theme-classic/src/theme/DocSidebar/index.js @@ -17,7 +17,13 @@ import styles from './styles.module.css'; const MOBILE_TOGGLE_SIZE = 24; -function DocSidebarItem({item, onItemClick, collapsible, ...props}) { +function DocSidebarItem({ + item, + onItemClick, + collapsible, + activePath, + ...props +}) { const {items, href, label, type} = item; const [collapsed, setCollapsed] = useState(item.collapsed); const [prevCollapsedProp, setPreviousCollapsedProp] = useState(null); @@ -63,6 +69,7 @@ function DocSidebarItem({item, onItemClick, collapsible, ...props}) { item={childItem} onItemClick={onItemClick} collapsible={collapsible} + activePath={activePath} /> ))} @@ -75,7 +82,9 @@ function DocSidebarItem({item, onItemClick, collapsible, ...props}) { return (
  • ))} diff --git a/packages/docusaurus-types/src/index.d.ts b/packages/docusaurus-types/src/index.d.ts index 4c2c1cea0f..61b34feef3 100644 --- a/packages/docusaurus-types/src/index.d.ts +++ b/packages/docusaurus-types/src/index.d.ts @@ -105,6 +105,7 @@ export interface Plugin { content: T; actions: PluginContentLoadedActions; }): void; + routesLoaded?(routes: RouteConfig[]): void; postBuild?(props: Props): void; postStart?(props: Props): void; configureWebpack?( diff --git a/packages/docusaurus/src/server/plugins/index.ts b/packages/docusaurus/src/server/plugins/index.ts index 2341ab6b3d..88c8df6dc9 100644 --- a/packages/docusaurus/src/server/plugins/index.ts +++ b/packages/docusaurus/src/server/plugins/index.ts @@ -40,6 +40,12 @@ export function sortConfig(routeConfigs: RouteConfig[]) { return a.path > b.path ? 1 : b.path > a.path ? -1 : 0; }); + + routeConfigs.forEach((routeConfig) => { + routeConfig.routes?.sort((a, b) => { + return a.path > b.path ? 1 : b.path > a.path ? -1 : 0; + }); + }); } export async function loadPlugins({ @@ -100,6 +106,20 @@ export async function loadPlugins({ }), ); + // 4. Plugin Lifecycle - routesLoaded. + // Currently plugins run lifecycle methods in parallel and are not order-dependent. + // We could change this in future if there are plugins which need to + // run in certain order or depend on others for data. + await Promise.all( + plugins.map(async (plugin) => { + if (!plugin.routesLoaded) { + return null; + } + + return await plugin.routesLoaded(pluginsRouteConfigs); + }), + ); + // Sort the route config. This ensures that route with nested // routes are always placed last. sortConfig(pluginsRouteConfigs); diff --git a/website/docs/docs.md b/website/docs/docs.md index d9194d7427..421b85ec24 100644 --- a/website/docs/docs.md +++ b/website/docs/docs.md @@ -29,6 +29,42 @@ id: part1 Lorem ipsum ``` +## Home page docs + +Using the `homePageId` property, you can create a home page of your docs. To do this, you can create a new document, especially for this purpose with the id as `_index`, or you could specify an existing document id. + +```js {8} title="docusaurus.config.js" +module.exports = { + // ... + presets: [ + [ + '@docusaurus/preset-classic', + { + docs: { + homePageId: 'getting-started', // Defaults to `_index` + // ... + }, + }, + ], + ], + // ... +}; +``` + +Given the example above, now when you navigate to the path `/docs` you will see that the document content with id is `getting-started`. This functionality also works for docs with versioning enabled. + +:::important + +The document id of `_index` is reserved exclusively for the home doc page, so it will not work as a standalone route. + +::: + +:::note + +The page `docs` that you created (eg. `src/pages/docs.js`) will take precedence over the route generated via the `homePageId` option. + +::: + ## Sidebar To generate a sidebar to your Docusaurus site, you need to define a file that exports a sidebar object and pass that into the `@docusaurus/plugin-docs` plugin directly or via `@docusaurus/preset-classic`. @@ -258,11 +294,17 @@ module.exports = { ## Docs-only mode -If you just want the documentation feature, you can follow the instructions for a "docs-only mode": +If you just want the documentation feature, you can enable "docs-only mode". -1. Set the `routeBasePath` property of the `docs` object in `@docusaurus/preset-classic` in `docusaurus.config.js` to the root of your site: +To achieve this, set the `routeBasePath` property of the `docs` object in `@docusaurus/preset-classic` in `docusaurus.config.js` to the root of your site, and also in that object set the `homePageId` property with the value of the document ID that you show as root of the docs. -```js {8} title="docusaurus.config.js" +:::note + +More details on functionality of home page for docs can be found in [appropriate section](#home-page-docs). + +::: + +```js {8-9} title="docusaurus.config.js" module.exports = { // ... presets: [ @@ -271,6 +313,7 @@ module.exports = { { docs: { routeBasePath: '/', // Set this value to '/'. + homePageId: 'getting-started', // Set to existing document id. // ... }, }, @@ -280,21 +323,6 @@ module.exports = { }; ``` -2. Set up a redirect to the initial document on the home page in `/src/pages/index.js`, e.g. for the document `getting-started`. This is needed because by default there's no page created for the root of the docs. - -```jsx title="src/pages/index.js" -import React from 'react'; - -import {Redirect} from '@docusaurus/router'; -import useBaseUrl from '@docusaurus/useBaseUrl'; - -function Home() { - return ; -} - -export default Home; -``` - Now, when visiting your site, it will show your initial document instead of a landing page. :::tip diff --git a/website/docs/lifecycle-apis.md b/website/docs/lifecycle-apis.md index c841d9770a..17725e6166 100644 --- a/website/docs/lifecycle-apis.md +++ b/website/docs/lifecycle-apis.md @@ -52,6 +52,10 @@ module.exports = function (context, options) { Plugins should use the data loaded in `loadContent` and construct the pages/routes that consume the loaded data (optional). +## `async routesLoaded(routes)` + +Plugins can modify the routes that were generated by all plugins. `routesLoaded` is called after `contentLoaded` hook. + ### `content` `contentLoaded` will be called _after_ `loadContent` is done, the return value of `loadContent()` will be passed to `contentLoaded` as `content`. @@ -373,6 +377,11 @@ module.exports = function (context, opts) { // actions are set of functional API provided by Docusaurus. e.g: addRoute }, + async routesLoaded(routes) { + // routesLoaded hook is done after contentLoaded hook is done + // This can be useful if you need to change any route. + }, + async postBuild(props) { // after docusaurus finish }, diff --git a/website/docs/using-plugins.md b/website/docs/using-plugins.md index dede1d5e1e..81f1eed6c5 100644 --- a/website/docs/using-plugins.md +++ b/website/docs/using-plugins.md @@ -236,6 +236,7 @@ module.exports = { * do not include trailing slash */ routeBasePath: 'docs', + homePageId: '_index', // Document id for docs home page. include: ['**/*.md', '**/*.mdx'], // Extensions to include. /** * Path to sidebar configuration for showing a list of markdown pages. diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 31d59dc20c..2090e1c94f 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -36,6 +36,7 @@ module.exports = { '@docusaurus/preset-classic', { docs: { + homePageId: 'introduction', path: 'docs', sidebarPath: require.resolve('./sidebars.js'), editUrl: