feat(v2): allow home page for docs (#2652)

* feat(v2): allow home page for docs

* Refactor

* Remove debugging info 🤦‍♂️

* Add sort routes for first test case

* Sort child routes for consistency
This commit is contained in:
Alexey Pyltsyn 2020-05-17 12:48:02 +03:00 committed by GitHub
parent 393adc5324
commit 00a8e9e365
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 264 additions and 61 deletions

View file

@ -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 {

View file

@ -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;

View file

@ -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,11 +316,77 @@ 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<string> =
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<RouteConfig[]> => {
const routes = await Promise.all(
metadataItems.map(async (metadataItem) => {
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),
);
// 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.
@ -325,16 +394,18 @@ export default function pluginContentDocs(
JSON.stringify(metadataItem, null, 2),
);
return {
// 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<string> =
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;

View file

@ -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 = {

View file

@ -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 <NotFound {...props} />;
}
@ -44,7 +55,7 @@ function DocPage(props) {
<div className={styles.docSidebarContainer}>
<DocSidebar
docsSidebars={docsSidebars}
path={currentRoute.path}
path={isHomePage ? homePagePath : currentRoute.path}
sidebar={sidebar}
sidebarCollapsible={sidebarCollapsible}
/>
@ -52,7 +63,11 @@ function DocPage(props) {
)}
<main className={styles.docMainContainer}>
<MDXProvider components={MDXComponents}>
{renderRoutes(baseRoute.routes)}
{isHomePage ? (
<DocItem content={content} />
) : (
renderRoutes(baseRoute.routes)
)}
</MDXProvider>
</main>
</div>

View file

@ -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}
/>
))}
</ul>
@ -75,7 +82,9 @@ function DocSidebarItem({item, onItemClick, collapsible, ...props}) {
return (
<li className="menu__list-item" key={label}>
<Link
className="menu__link"
className={classnames('menu__link', {
'menu__link--active': href === activePath,
})}
to={href}
{...(isInternalUrl(href)
? {
@ -219,6 +228,7 @@ function DocSidebar(props) {
setShowResponsiveSidebar(false);
}}
collapsible={sidebarCollapsible}
activePath={path}
/>
))}
</ul>

View file

@ -105,6 +105,7 @@ export interface Plugin<T> {
content: T;
actions: PluginContentLoadedActions;
}): void;
routesLoaded?(routes: RouteConfig[]): void;
postBuild?(props: Props): void;
postStart?(props: Props): void;
configureWebpack?(

View file

@ -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);

View file

@ -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 <Redirect to={useBaseUrl('/getting-started')} />;
}
export default Home;
```
Now, when visiting your site, it will show your initial document instead of a landing page.
:::tip

View file

@ -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 <build> finish
},

View file

@ -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.

View file

@ -36,6 +36,7 @@ module.exports = {
'@docusaurus/preset-classic',
{
docs: {
homePageId: 'introduction',
path: 'docs',
sidebarPath: require.resolve('./sidebars.js'),
editUrl: