fix(v2): fix docs homepage permalink issues (#2905)

* better fixes for docs homepage

* fix tests

* create special route for docs homepage + cleanup existing code

* no need to create multiple docs parent paths

* useful comment

* add test for slug + doc home usage at the same time error

* remove confusing variable name

* fix tests by using same suffix as before for docs base metadata path

* metadata: use homePageId correctly for nested docs: the full docId (including /) should be used to compare against homePageId

* add folder/testNested test doc

* refactor a bit processMetadata, the home should be handled correctly for all versions

* Workaround to fix issue when parent layout route (DocPage) has same path as the child route (DocItem): see https://github.com/facebook/docusaurus/issues/2917

* revert homePageId

* remove test doc

* remove test doc

* add useful comment
This commit is contained in:
Sébastien Lorber 2020-06-17 14:54:08 +02:00 committed by GitHub
parent a3f54d747d
commit f6b1c85b01
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 264 additions and 218 deletions

View file

@ -0,0 +1,5 @@
---
slug: docWithSlug.html
---
Lorem

View file

@ -29,7 +29,7 @@ Object {
"type": "link", "type": "link",
}, },
Object { Object {
"href": "/docs", "href": "/docs/",
"label": "Hello, World !", "label": "Hello, World !",
"type": "link", "type": "link",
}, },
@ -41,7 +41,7 @@ Object {
"collapsed": true, "collapsed": true,
"items": Array [ "items": Array [
Object { Object {
"href": "/docs", "href": "/docs/",
"label": "Hello, World !", "label": "Hello, World !",
"type": "link", "type": "link",
}, },
@ -57,21 +57,21 @@ exports[`simple website content 2`] = `
Array [ Array [
Object { Object {
"component": "@theme/DocPage", "component": "@theme/DocPage",
"exact": true, "exact": false,
"modules": Object {
"content": "@site/docs/hello.md",
"docsMetadata": "~docs/site-docs-hello-md-9df-base.json",
},
"path": "/docs",
},
Object {
"component": "@theme/DocPage",
"modules": Object { "modules": Object {
"docsMetadata": "~docs/docs-route-ff2.json", "docsMetadata": "~docs/docs-route-ff2.json",
}, },
"path": "/docs/:route", "path": "/docs",
"priority": undefined, "priority": undefined,
"routes": Array [ "routes": Array [
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/docs/hello.md",
},
"path": "/docs/",
},
Object { Object {
"component": "@theme/DocItem", "component": "@theme/DocItem",
"exact": true, "exact": true,
@ -123,39 +123,21 @@ exports[`versioned website content 1`] = `
Array [ Array [
Object { Object {
"component": "@theme/DocPage", "component": "@theme/DocPage",
"exact": true, "exact": false,
"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 { "modules": Object {
"docsMetadata": "~docs/docs-1-0-0-route-660.json", "docsMetadata": "~docs/docs-1-0-0-route-660.json",
}, },
"path": "/docs/1.0.0/:route", "path": "/docs/1.0.0",
"priority": undefined, "priority": undefined,
"routes": Array [ "routes": Array [
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/versioned_docs/version-1.0.0/hello.md",
},
"path": "/docs/1.0.0/",
},
Object { Object {
"component": "@theme/DocItem", "component": "@theme/DocItem",
"exact": true, "exact": true,
@ -176,12 +158,21 @@ Array [
}, },
Object { Object {
"component": "@theme/DocPage", "component": "@theme/DocPage",
"exact": false,
"modules": Object { "modules": Object {
"docsMetadata": "~docs/docs-next-route-1c8.json", "docsMetadata": "~docs/docs-next-route-1c8.json",
}, },
"path": "/docs/next/:route", "path": "/docs/next",
"priority": undefined, "priority": undefined,
"routes": Array [ "routes": Array [
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/docs/hello.md",
},
"path": "/docs/next/",
},
Object { Object {
"component": "@theme/DocItem", "component": "@theme/DocItem",
"exact": true, "exact": true,
@ -194,12 +185,21 @@ Array [
}, },
Object { Object {
"component": "@theme/DocPage", "component": "@theme/DocPage",
"exact": false,
"modules": Object { "modules": Object {
"docsMetadata": "~docs/docs-route-ff2.json", "docsMetadata": "~docs/docs-route-ff2.json",
}, },
"path": "/docs/:route", "path": "/docs",
"priority": -1, "priority": -1,
"routes": Array [ "routes": Array [
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/versioned_docs/version-1.0.1/hello.md",
},
"path": "/docs/",
},
Object { Object {
"component": "@theme/DocItem", "component": "@theme/DocItem",
"exact": true, "exact": true,
@ -232,7 +232,7 @@ Object {
"collapsed": true, "collapsed": true,
"items": Array [ "items": Array [
Object { Object {
"href": "/docs/next", "href": "/docs/next/",
"label": "hello", "label": "hello",
"type": "link", "type": "link",
}, },
@ -263,7 +263,7 @@ Object {
"collapsed": true, "collapsed": true,
"items": Array [ "items": Array [
Object { Object {
"href": "/docs/1.0.0", "href": "/docs/1.0.0/",
"label": "hello", "label": "hello",
"type": "link", "type": "link",
}, },
@ -289,7 +289,7 @@ Object {
"collapsed": true, "collapsed": true,
"items": Array [ "items": Array [
Object { Object {
"href": "/docs", "href": "/docs/",
"label": "hello", "label": "hello",
"type": "link", "type": "link",
}, },
@ -326,7 +326,7 @@ Object {
"collapsed": true, "collapsed": true,
"items": Array [ "items": Array [
Object { Object {
"href": "/docs/1.0.0", "href": "/docs/1.0.0/",
"label": "hello", "label": "hello",
"type": "link", "type": "link",
}, },
@ -337,9 +337,9 @@ Object {
], ],
}, },
"permalinkToSidebar": Object { "permalinkToSidebar": Object {
"/docs/1.0.0/": "version-1.0.0/docs",
"/docs/1.0.0/foo/barSlug": "version-1.0.0/docs", "/docs/1.0.0/foo/barSlug": "version-1.0.0/docs",
"/docs/1.0.0/foo/baz": "version-1.0.0/docs", "/docs/1.0.0/foo/baz": "version-1.0.0/docs",
"/docs/1.0.0/hello": "version-1.0.0/docs",
}, },
"version": "1.0.0", "version": "1.0.0",
} }
@ -365,7 +365,7 @@ Object {
"collapsed": true, "collapsed": true,
"items": Array [ "items": Array [
Object { Object {
"href": "/docs", "href": "/docs/",
"label": "hello", "label": "hello",
"type": "link", "type": "link",
}, },
@ -376,8 +376,8 @@ Object {
], ],
}, },
"permalinkToSidebar": Object { "permalinkToSidebar": Object {
"/docs/": "version-1.0.1/docs",
"/docs/foo/bar": "version-1.0.1/docs", "/docs/foo/bar": "version-1.0.1/docs",
"/docs/hello": "version-1.0.1/docs",
}, },
"version": "1.0.1", "version": "1.0.1",
} }
@ -403,7 +403,7 @@ Object {
"collapsed": true, "collapsed": true,
"items": Array [ "items": Array [
Object { Object {
"href": "/docs/next", "href": "/docs/next/",
"label": "hello", "label": "hello",
"type": "link", "type": "link",
}, },
@ -414,8 +414,8 @@ Object {
], ],
}, },
"permalinkToSidebar": Object { "permalinkToSidebar": Object {
"/docs/next/": "docs",
"/docs/next/foo/barSlug": "docs", "/docs/next/foo/barSlug": "docs",
"/docs/next/hello": "docs",
}, },
"version": "next", "version": "next",
} }

View file

@ -154,7 +154,8 @@ describe('simple website', () => {
expect(versionToSidebars).toEqual({}); expect(versionToSidebars).toEqual({});
expect(docsMetadata.hello).toEqual({ expect(docsMetadata.hello).toEqual({
id: 'hello', id: 'hello',
permalink: '/docs/hello', isDocsHomePage: true,
permalink: '/docs/',
previous: { previous: {
title: 'baz', title: 'baz',
permalink: '/docs/foo/bazSlug.html', permalink: '/docs/foo/bazSlug.html',
@ -168,6 +169,7 @@ describe('simple website', () => {
expect(docsMetadata['foo/bar']).toEqual({ expect(docsMetadata['foo/bar']).toEqual({
id: 'foo/bar', id: 'foo/bar',
isDocsHomePage: false,
next: { next: {
title: 'baz', title: 'baz',
permalink: '/docs/foo/bazSlug.html', permalink: '/docs/foo/bazSlug.html',
@ -296,6 +298,7 @@ describe('versioned website', () => {
expect(docsMetadata['version-1.0.1/foo/baz']).toBeUndefined(); expect(docsMetadata['version-1.0.1/foo/baz']).toBeUndefined();
expect(docsMetadata['foo/bar']).toEqual({ expect(docsMetadata['foo/bar']).toEqual({
id: 'foo/bar', id: 'foo/bar',
isDocsHomePage: false,
permalink: '/docs/next/foo/barSlug', permalink: '/docs/next/foo/barSlug',
source: path.join('@site', routeBasePath, 'foo', 'bar.md'), source: path.join('@site', routeBasePath, 'foo', 'bar.md'),
title: 'bar', title: 'bar',
@ -304,12 +307,13 @@ describe('versioned website', () => {
sidebar: 'docs', sidebar: 'docs',
next: { next: {
title: 'hello', title: 'hello',
permalink: '/docs/next/hello', permalink: '/docs/next/',
}, },
}); });
expect(docsMetadata['hello']).toEqual({ expect(docsMetadata['hello']).toEqual({
id: 'hello', id: 'hello',
permalink: '/docs/next/hello', isDocsHomePage: true,
permalink: '/docs/next/',
source: path.join('@site', routeBasePath, 'hello.md'), source: path.join('@site', routeBasePath, 'hello.md'),
title: 'hello', title: 'hello',
description: 'Hello next !', description: 'Hello next !',
@ -322,7 +326,8 @@ describe('versioned website', () => {
}); });
expect(docsMetadata['version-1.0.1/hello']).toEqual({ expect(docsMetadata['version-1.0.1/hello']).toEqual({
id: 'version-1.0.1/hello', id: 'version-1.0.1/hello',
permalink: '/docs/hello', isDocsHomePage: true,
permalink: '/docs/',
source: path.join( source: path.join(
'@site', '@site',
path.relative(siteDir, versionedDir), path.relative(siteDir, versionedDir),
@ -341,6 +346,7 @@ describe('versioned website', () => {
}); });
expect(docsMetadata['version-1.0.0/foo/baz']).toEqual({ expect(docsMetadata['version-1.0.0/foo/baz']).toEqual({
id: 'version-1.0.0/foo/baz', id: 'version-1.0.0/foo/baz',
isDocsHomePage: false,
permalink: '/docs/1.0.0/foo/baz', permalink: '/docs/1.0.0/foo/baz',
source: path.join( source: path.join(
'@site', '@site',
@ -356,7 +362,7 @@ describe('versioned website', () => {
sidebar: 'version-1.0.0/docs', sidebar: 'version-1.0.0/docs',
next: { next: {
title: 'hello', title: 'hello',
permalink: '/docs/1.0.0/hello', permalink: '/docs/1.0.0/',
}, },
previous: { previous: {
title: 'bar', title: 'bar',

View file

@ -46,6 +46,7 @@ describe('simple site', () => {
expect(dataA).toEqual({ expect(dataA).toEqual({
id: 'foo/bar', id: 'foo/bar',
isDocsHomePage: false,
permalink: '/docs/foo/bar', permalink: '/docs/foo/bar',
source: path.join('@site', routeBasePath, sourceA), source: path.join('@site', routeBasePath, sourceA),
title: 'Bar', title: 'Bar',
@ -54,6 +55,7 @@ describe('simple site', () => {
}); });
expect(dataB).toEqual({ expect(dataB).toEqual({
id: 'hello', id: 'hello',
isDocsHomePage: false,
permalink: '/docs/hello', permalink: '/docs/hello',
source: path.join('@site', routeBasePath, sourceB), source: path.join('@site', routeBasePath, sourceB),
title: 'Hello, World !', title: 'Hello, World !',
@ -62,6 +64,56 @@ describe('simple site', () => {
}); });
}); });
test('homePageId doc', async () => {
const source = path.join('hello.md');
const options = {
routeBasePath,
homePageId: 'hello',
};
const data = await processMetadata({
source,
refDir: docsDir,
context,
options,
env,
});
expect(data).toEqual({
id: 'hello',
isDocsHomePage: true,
permalink: '/docs/',
source: path.join('@site', routeBasePath, source),
title: 'Hello, World !',
description: `Hi, Endilie here :)`,
});
});
test('homePageId doc nested', async () => {
const source = path.join('foo', 'bar.md');
const options = {
routeBasePath,
homePageId: 'foo/bar',
};
const data = await processMetadata({
source,
refDir: docsDir,
context,
options,
env,
});
expect(data).toEqual({
id: 'foo/bar',
isDocsHomePage: true,
permalink: '/docs/',
source: path.join('@site', routeBasePath, source),
title: 'Bar',
description: 'This is custom description',
});
});
test('docs with editUrl', async () => { test('docs with editUrl', async () => {
const editUrl = const editUrl =
'https://github.com/facebook/docusaurus/edit/master/website'; 'https://github.com/facebook/docusaurus/edit/master/website';
@ -81,6 +133,7 @@ describe('simple site', () => {
expect(data).toEqual({ expect(data).toEqual({
id: 'foo/baz', id: 'foo/baz',
isDocsHomePage: false,
permalink: '/docs/foo/bazSlug.html', permalink: '/docs/foo/bazSlug.html',
source: path.join('@site', routeBasePath, source), source: path.join('@site', routeBasePath, source),
title: 'baz', title: 'baz',
@ -107,6 +160,7 @@ describe('simple site', () => {
expect(data).toEqual({ expect(data).toEqual({
id: 'lorem', id: 'lorem',
isDocsHomePage: false,
permalink: '/docs/lorem', permalink: '/docs/lorem',
source: path.join('@site', routeBasePath, source), source: path.join('@site', routeBasePath, source),
title: 'lorem', title: 'lorem',
@ -137,6 +191,7 @@ describe('simple site', () => {
expect(data).toEqual({ expect(data).toEqual({
id: 'lorem', id: 'lorem',
isDocsHomePage: false,
permalink: '/docs/lorem', permalink: '/docs/lorem',
source: path.join('@site', routeBasePath, source), source: path.join('@site', routeBasePath, source),
title: 'lorem', title: 'lorem',
@ -166,6 +221,7 @@ describe('simple site', () => {
expect(data).toEqual({ expect(data).toEqual({
id: 'ipsum', id: 'ipsum',
isDocsHomePage: false,
permalink: '/docs/ipsum', permalink: '/docs/ipsum',
source: path.join('@site', routeBasePath, source), source: path.join('@site', routeBasePath, source),
title: 'ipsum', title: 'ipsum',
@ -214,6 +270,26 @@ describe('simple site', () => {
`"Document slug cannot include \\"/\\"."`, `"Document slug cannot include \\"/\\"."`,
); );
}); });
test('docs with slug on doc home', async () => {
const badSiteDir = path.join(fixtureDir, 'bad-slug-on-doc-home-site');
const options = {
routeBasePath,
homePageId: 'docWithSlug',
};
await expect(
processMetadata({
source: 'docWithSlug.md',
refDir: path.join(badSiteDir, 'docs'),
context,
options,
env,
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"The docs homepage (homePageId=docWithSlug) is not allowed to have a frontmatter slug=docWithSlug.html => you have to chooser either homePageId or slug, not both"`,
);
});
}); });
describe('versioned site', () => { describe('versioned site', () => {
@ -250,6 +326,7 @@ describe('versioned site', () => {
expect(dataA).toEqual({ expect(dataA).toEqual({
id: 'foo/bar', id: 'foo/bar',
isDocsHomePage: false,
permalink: '/docs/next/foo/barSlug', permalink: '/docs/next/foo/barSlug',
source: path.join('@site', routeBasePath, sourceA), source: path.join('@site', routeBasePath, sourceA),
title: 'bar', title: 'bar',
@ -258,6 +335,7 @@ describe('versioned site', () => {
}); });
expect(dataB).toEqual({ expect(dataB).toEqual({
id: 'hello', id: 'hello',
isDocsHomePage: false,
permalink: '/docs/next/hello', permalink: '/docs/next/hello',
source: path.join('@site', routeBasePath, sourceB), source: path.join('@site', routeBasePath, sourceB),
title: 'hello', title: 'hello',
@ -308,6 +386,7 @@ describe('versioned site', () => {
expect(dataA).toEqual({ expect(dataA).toEqual({
id: 'version-1.0.0/foo/bar', id: 'version-1.0.0/foo/bar',
isDocsHomePage: false,
permalink: '/docs/1.0.0/foo/barSlug', permalink: '/docs/1.0.0/foo/barSlug',
source: path.join('@site', path.relative(siteDir, versionedDir), sourceA), source: path.join('@site', path.relative(siteDir, versionedDir), sourceA),
title: 'bar', title: 'bar',
@ -316,6 +395,7 @@ describe('versioned site', () => {
}); });
expect(dataB).toEqual({ expect(dataB).toEqual({
id: 'version-1.0.0/hello', id: 'version-1.0.0/hello',
isDocsHomePage: false,
permalink: '/docs/1.0.0/hello', permalink: '/docs/1.0.0/hello',
source: path.join('@site', path.relative(siteDir, versionedDir), sourceB), source: path.join('@site', path.relative(siteDir, versionedDir), sourceB),
title: 'hello', title: 'hello',
@ -324,6 +404,7 @@ describe('versioned site', () => {
}); });
expect(dataC).toEqual({ expect(dataC).toEqual({
id: 'version-1.0.1/foo/bar', id: 'version-1.0.1/foo/bar',
isDocsHomePage: false,
permalink: '/docs/foo/bar', permalink: '/docs/foo/bar',
source: path.join('@site', path.relative(siteDir, versionedDir), sourceC), source: path.join('@site', path.relative(siteDir, versionedDir), sourceC),
title: 'bar', title: 'bar',
@ -332,6 +413,7 @@ describe('versioned site', () => {
}); });
expect(dataD).toEqual({ expect(dataD).toEqual({
id: 'version-1.0.1/hello', id: 'version-1.0.1/hello',
isDocsHomePage: false,
permalink: '/docs/hello', permalink: '/docs/hello',
source: path.join('@site', path.relative(siteDir, versionedDir), sourceD), source: path.join('@site', path.relative(siteDir, versionedDir), sourceD),
title: 'hello', title: 'hello',

View file

@ -85,7 +85,9 @@ export default function pluginContentDocs(
context: LoadContext, context: LoadContext,
opts: Partial<PluginOptions>, opts: Partial<PluginOptions>,
): Plugin<LoadedContent | null> { ): Plugin<LoadedContent | null> {
const options = {...DEFAULT_OPTIONS, ...opts}; const options: PluginOptions = {...DEFAULT_OPTIONS, ...opts};
const homePageDocsRoutePath =
options.routeBasePath === '' ? '/' : options.routeBasePath;
if (options.admonitions) { if (options.admonitions) {
options.remarkPlugins = options.remarkPlugins.concat([ options.remarkPlugins = options.remarkPlugins.concat([
@ -112,24 +114,6 @@ export default function pluginContentDocs(
} = versioning; } = versioning;
const versionsNames = versions.map((version) => `version-${version}`); const versionsNames = versions.map((version) => `version-${version}`);
// Docs home page.
const homePageDocsRoutePath =
options.routeBasePath === '' ? '/' : options.routeBasePath;
const isDocsHomePagePath = (permalink: string) => {
const documentIdMatch = new RegExp(
`^\/(?:${homePageDocsRoutePath}\/)?(?:(?:${versions.join(
'|',
)}|next)\/)?(.*)`,
'i',
).exec(permalink);
if (documentIdMatch) {
return documentIdMatch[1] === options.homePageId;
}
return false;
};
return { return {
name: 'docusaurus-plugin-content-docs', name: 'docusaurus-plugin-content-docs',
@ -307,9 +291,7 @@ Available document ids=
return { return {
type: 'link', type: 'link',
label: sidebar_label || title, label: sidebar_label || title,
href: isDocsHomePagePath(permalink) href: permalink,
? permalink.replace(`/${options.homePageId}`, '')
: permalink,
}; };
}; };
@ -376,50 +358,8 @@ Available document ids=
const genRoutes = async ( const genRoutes = async (
metadataItems: Metadata[], metadataItems: Metadata[],
): Promise<RouteConfig[]> => { ): Promise<RouteConfig[]> => {
const versionsRegex = new RegExp(versionsNames.join('|'), 'i');
const routes = await Promise.all( const routes = await Promise.all(
metadataItems.map(async (metadataItem) => { metadataItems.map(async (metadataItem) => {
const isDocsHomePage =
metadataItem.id.replace(versionsRegex, '').replace(/^\//, '') ===
options.homePageId;
if (isDocsHomePage) {
const versionDocsPathPrefix =
(metadataItem?.version === versioning.latestVersion
? ''
: metadataItem.version!) ?? '';
const docsBaseMetadata = createDocsBaseMetadata(
metadataItem.version!,
);
docsBaseMetadata.isHomePage = true;
docsBaseMetadata.homePagePath = normalizeUrl([
baseUrl,
homePageDocsRoutePath,
versionDocsPathPrefix,
]);
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,
homePageDocsRoutePath,
versionDocsPathPrefix,
]),
component: docLayoutComponent,
exact: true,
modules: {
docsMetadata: aliasedSource(docsBaseMetadataPath),
content: metadataItem.source,
},
});
}
await createData( await 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.
@ -438,15 +378,14 @@ Available document ids=
}), }),
); );
return ( return routes.sort((a, b) =>
routes a.path > b.path ? 1 : b.path > a.path ? -1 : 0,
// Do not create a route for a document serve as docs home page.
// TODO: need way to do this filtering when generating routes for better perf.
.filter(({path}) => !isDocsHomePagePath(path))
.sort((a, b) => (a.path > b.path ? 1 : b.path > a.path ? -1 : 0))
); );
}; };
// This is the base route of the document root (for a doc given version)
// (/docs, /docs/next, /docs/1.0 etc...)
// The component applies the layout and renders the appropriate doc
const addBaseRoute = async ( const addBaseRoute = async (
docsBaseRoute: string, docsBaseRoute: string,
docsBaseMetadata: DocsBaseMetadata, docsBaseMetadata: DocsBaseMetadata,
@ -454,14 +393,20 @@ Available document ids=
priority?: number, priority?: number,
) => { ) => {
const docsBaseMetadataPath = await createData( const docsBaseMetadataPath = await createData(
`${docuHash(docsBaseRoute)}.json`, `${docuHash(normalizeUrl([docsBaseRoute, ':route']))}.json`,
JSON.stringify(docsBaseMetadata, null, 2), JSON.stringify(docsBaseMetadata, null, 2),
); );
// Important: the layout component should not end with /,
// as it conflicts with the home doc
// Workaround fix for https://github.com/facebook/docusaurus/issues/2917
const path = docsBaseRoute === '/' ? '' : docsBaseRoute;
addRoute({ addRoute({
path: docsBaseRoute, path,
component: docLayoutComponent, exact: false, // allow matching /docs/* as well
routes, component: docLayoutComponent, // main docs component (DocPage)
routes, // subroute for each doc
modules: { modules: {
docsMetadata: aliasedSource(docsBaseMetadataPath), docsMetadata: aliasedSource(docsBaseMetadataPath),
}, },
@ -499,21 +444,20 @@ Available document ids=
); );
const isLatestVersion = version === versioning.latestVersion; const isLatestVersion = version === versioning.latestVersion;
const docsBasePermalink = normalizeUrl([ const docsBaseRoute = normalizeUrl([
baseUrl, baseUrl,
routeBasePath, routeBasePath,
isLatestVersion ? '' : version, isLatestVersion ? '' : version,
]); ]);
const docsBaseRoute = normalizeUrl([docsBasePermalink, ':route']);
const docsBaseMetadata = createDocsBaseMetadata(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
// `/docs/:route` instead of `/docs/next/:route`.
return addBaseRoute( return addBaseRoute(
docsBaseRoute, docsBaseRoute,
docsBaseMetadata, docsBaseMetadata,
routes, routes,
// We want latest version route config to be placed last in the
// generated routeconfig. Otherwise, `/docs/next/foo` will match
// `/docs/:route` instead of `/docs/next/:route`.
isLatestVersion ? -1 : undefined, isLatestVersion ? -1 : undefined,
); );
}), }),
@ -521,8 +465,7 @@ Available document ids=
} else { } else {
const routes = await genRoutes(Object.values(content.docsMetadata)); const routes = await genRoutes(Object.values(content.docsMetadata));
const docsBaseMetadata = createDocsBaseMetadata(); const docsBaseMetadata = createDocsBaseMetadata();
const docsBaseRoute = normalizeUrl([baseUrl, routeBasePath]);
const docsBaseRoute = normalizeUrl([baseUrl, routeBasePath, ':route']);
return addBaseRoute(docsBaseRoute, docsBaseMetadata, routes); return addBaseRoute(docsBaseRoute, docsBaseMetadata, routes);
} }
}, },

View file

@ -15,7 +15,43 @@ import {
import {LoadContext} from '@docusaurus/types'; import {LoadContext} from '@docusaurus/types';
import lastUpdate from './lastUpdate'; import lastUpdate from './lastUpdate';
import {MetadataRaw, LastUpdateData, MetadataOptions, Env} from './types'; import {
MetadataRaw,
LastUpdateData,
MetadataOptions,
Env,
VersioningEnv,
} from './types';
function removeVersionPrefix(str: string, version: string): string {
return str.replace(new RegExp(`^version-${version}/`), '');
}
function inferVersion(
dirName: string,
versioning: VersioningEnv,
): string | undefined {
if (!versioning.enabled) {
return undefined;
}
if (/^version-/.test(dirName)) {
const inferredVersion = dirName
.split('/', 1)
.shift()!
.replace(/^version-/, '');
if (inferredVersion && versioning.versions.includes(inferredVersion)) {
return inferredVersion;
} else {
throw new Error(
`Can't infer version from folder=${dirName}
Expected versions:
- ${versioning.versions.join('- ')}`,
);
}
} else {
return 'next';
}
}
type Args = { type Args = {
source: string; source: string;
@ -59,7 +95,7 @@ export default async function processMetadata({
options, options,
env, env,
}: Args): Promise<MetadataRaw> { }: Args): Promise<MetadataRaw> {
const {routeBasePath, editUrl} = options; const {routeBasePath, editUrl, homePageId} = options;
const {siteDir, baseUrl} = context; const {siteDir, baseUrl} = context;
const {versioning} = env; const {versioning} = env;
const filePath = path.join(refDir, source); const filePath = path.join(refDir, source);
@ -67,21 +103,8 @@ export default async function processMetadata({
const fileMarkdownPromise = parseMarkdownFile(filePath); const fileMarkdownPromise = parseMarkdownFile(filePath);
const lastUpdatedPromise = lastUpdated(filePath, options); const lastUpdatedPromise = lastUpdated(filePath, options);
let version;
const dirName = path.dirname(source); const dirName = path.dirname(source);
if (versioning.enabled) { const version = inferVersion(dirName, versioning);
if (/^version-/.test(dirName)) {
const inferredVersion = dirName
.split('/', 1)
.shift()!
.replace(/^version-/, '');
if (inferredVersion && versioning.versions.includes(inferredVersion)) {
version = inferredVersion;
}
} else {
version = 'next';
}
}
// The version portion of the url path. Eg: 'next', '1.0.0', and ''. // The version portion of the url path. Eg: 'next', '1.0.0', and ''.
const versionPath = const versionPath =
@ -100,14 +123,20 @@ export default async function processMetadata({
if (baseID.includes('/')) { if (baseID.includes('/')) {
throw new Error('Document id cannot include "/".'); throw new Error('Document id cannot include "/".');
} }
const id = dirName !== '.' ? `${dirName}/${baseID}` : baseID;
const idWithoutVersion = version ? removeVersionPrefix(id, version) : id;
const isDocsHomePage = idWithoutVersion === homePageId;
if (frontMatter.slug && isDocsHomePage) {
throw new Error(
`The docs homepage (homePageId=${homePageId}) is not allowed to have a frontmatter slug=${frontMatter.slug} => you have to chooser either homePageId or slug, not both`,
);
}
const baseSlug: string = frontMatter.slug || baseID; const baseSlug: string = frontMatter.slug || baseID;
if (baseSlug.includes('/')) { if (baseSlug.includes('/')) {
throw new Error('Document slug cannot include "/".'); throw new Error('Document slug cannot include "/".');
} }
// Append subdirectory as part of id/slug.
const id = dirName !== '.' ? `${dirName}/${baseID}` : baseID;
const slug = dirName !== '.' ? `${dirName}/${baseSlug}` : baseSlug; const slug = dirName !== '.' ? `${dirName}/${baseSlug}` : baseSlug;
// Default title is the id. // Default title is the id.
@ -116,10 +145,16 @@ export default async function processMetadata({
const description: string = frontMatter.description || excerpt; const description: string = frontMatter.description || excerpt;
// The last portion of the url path. Eg: 'foo/bar', 'bar'. // The last portion of the url path. Eg: 'foo/bar', 'bar'.
const routePath = let routePath;
version && version !== 'next' if (isDocsHomePage) {
? slug.replace(new RegExp(`^version-${version}/`), '') // TODO can we remove this trailing / ?
: slug; // Seems it's not that easy...
// Related to https://github.com/facebook/docusaurus/issues/2917
routePath = '/';
} else {
routePath =
version && version !== 'next' ? removeVersionPrefix(slug, version) : slug;
}
const permalink = normalizeUrl([ const permalink = normalizeUrl([
baseUrl, baseUrl,
@ -136,6 +171,7 @@ export default async function processMetadata({
// class transitions. // class transitions.
const metadata: MetadataRaw = { const metadata: MetadataRaw = {
id, id,
isDocsHomePage,
title, title,
description, description,
source: aliasedSitePath(filePath, siteDir), source: aliasedSitePath(filePath, siteDir),

View file

@ -7,6 +7,7 @@
export interface MetadataOptions { export interface MetadataOptions {
routeBasePath: string; routeBasePath: string;
homePageId?: string;
editUrl?: string; editUrl?: string;
showLastUpdateTime?: boolean; showLastUpdateTime?: boolean;
showLastUpdateAuthor?: boolean; showLastUpdateAuthor?: boolean;
@ -24,7 +25,6 @@ export interface PluginOptions extends MetadataOptions, PathOptions {
remarkPlugins: ([Function, object] | Function)[]; remarkPlugins: ([Function, object] | Function)[];
rehypePlugins: string[]; rehypePlugins: string[];
admonitions: any; admonitions: any;
homePageId: string;
} }
export type SidebarItemDoc = { export type SidebarItemDoc = {
@ -111,6 +111,7 @@ export interface LastUpdateData {
export interface MetadataRaw extends LastUpdateData { export interface MetadataRaw extends LastUpdateData {
id: string; id: string;
isDocsHomePage: boolean;
title: string; title: string;
description: string; description: string;
source: string; source: string;
@ -165,8 +166,6 @@ export type DocsBaseMetadata = Pick<
'docsSidebars' | 'permalinkToSidebar' 'docsSidebars' | 'permalinkToSidebar'
> & { > & {
version?: string; version?: string;
isHomePage?: boolean;
homePagePath?: string;
}; };
export type VersioningEnv = { export type VersioningEnv = {

View file

@ -8,7 +8,6 @@
import React from 'react'; import React from 'react';
import renderRoutes from '@docusaurus/renderRoutes'; import renderRoutes from '@docusaurus/renderRoutes';
import NotFound from '@theme/NotFound'; import NotFound from '@theme/NotFound';
import DocItem from '@theme/DocItem';
import DocSidebar from '@theme/DocSidebar'; import DocSidebar from '@theme/DocSidebar';
import MDXComponents from '@theme/MDXComponents'; import MDXComponents from '@theme/MDXComponents';
import Layout from '@theme/Layout'; import Layout from '@theme/Layout';
@ -16,24 +15,16 @@ import {MDXProvider} from '@mdx-js/react';
import {matchPath} from '@docusaurus/router'; import {matchPath} from '@docusaurus/router';
function DocPage(props) { function DocPage(props) {
const {route: baseRoute, docsMetadata, location, content} = props; const {route: baseRoute, docsMetadata, location} = props;
const {
permalinkToSidebar,
docsSidebars,
isHomePage,
homePagePath,
} = docsMetadata;
// case-sensitive route such as it is defined in the sidebar // case-sensitive route such as it is defined in the sidebar
const currentRoute = !isHomePage const currentRoute =
? baseRoute.routes.find((route) => { baseRoute.routes.find((route) => {
return matchPath(location.pathname, route); return matchPath(location.pathname, route);
}) || {} }) || {};
: {}; const {permalinkToSidebar, docsSidebars} = docsMetadata;
const sidebar = isHomePage const sidebar = permalinkToSidebar[currentRoute.path];
? content.metadata.sidebar
: permalinkToSidebar[currentRoute.path];
if (!isHomePage && Object.keys(currentRoute).length === 0) { if (Object.keys(currentRoute).length === 0) {
return <NotFound {...props} />; return <NotFound {...props} />;
} }
@ -41,16 +32,12 @@ function DocPage(props) {
<Layout title="Doc page" description="My Doc page"> <Layout title="Doc page" description="My Doc page">
<DocSidebar <DocSidebar
docsSidebars={docsSidebars} docsSidebars={docsSidebars}
path={isHomePage ? homePagePath : currentRoute.path} path={currentRoute.path}
sidebar={sidebar} sidebar={sidebar}
/> />
<section className="offset-1 mr-4 mt-4 col-xl-6 offset-xl-4 p-0 justify-content-center align-self-center overflow-hidden"> <section className="offset-1 mr-4 mt-4 col-xl-6 offset-xl-4 p-0 justify-content-center align-self-center overflow-hidden">
<MDXProvider components={MDXComponents}> <MDXProvider components={MDXComponents}>
{isHomePage ? ( {renderRoutes(baseRoute.routes)}
<DocItem content={content} />
) : (
renderRoutes(baseRoute.routes)
)}
</MDXProvider> </MDXProvider>
</section> </section>
</Layout> </Layout>

View file

@ -11,7 +11,6 @@ import {MDXProvider} from '@mdx-js/react';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import renderRoutes from '@docusaurus/renderRoutes'; import renderRoutes from '@docusaurus/renderRoutes';
import Layout from '@theme/Layout'; import Layout from '@theme/Layout';
import DocItem from '@theme/DocItem';
import DocSidebar from '@theme/DocSidebar'; import DocSidebar from '@theme/DocSidebar';
import MDXComponents from '@theme/MDXComponents'; import MDXComponents from '@theme/MDXComponents';
import NotFound from '@theme/NotFound'; import NotFound from '@theme/NotFound';
@ -20,35 +19,22 @@ import {matchPath} from '@docusaurus/router';
import styles from './styles.module.css'; import styles from './styles.module.css';
function DocPage(props) { function DocPage(props) {
const {route: baseRoute, docsMetadata, location, content} = props; const {route: baseRoute, docsMetadata, location} = props;
const { // case-sensitive route such as it is defined in the sidebar
permalinkToSidebar, const currentRoute =
docsSidebars, baseRoute.routes.find((route) => {
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); return matchPath(location.pathname, route);
}) || {} }) || {};
: {}; const {permalinkToSidebar, docsSidebars, version} = docsMetadata;
const sidebar = permalinkToSidebar[currentRoute.path];
const sidebar = isHomePage
? content.metadata.sidebar
: permalinkToSidebar[currentRoute.path];
const { const {
siteConfig: {themeConfig: {sidebarCollapsible = true} = {}} = {}, siteConfig: {themeConfig = {}} = {},
isClient, isClient,
} = useDocusaurusContext(); } = useDocusaurusContext();
if (isHomePage) { const {sidebarCollapsible = true} = themeConfig;
content.metadata.permalink = homePagePath;
}
if (!isHomePage && Object.keys(currentRoute).length === 0) { if (Object.keys(currentRoute).length === 0) {
return <NotFound {...props} />; return <NotFound {...props} />;
} }
@ -59,7 +45,7 @@ function DocPage(props) {
<div className={styles.docSidebarContainer} role="complementary"> <div className={styles.docSidebarContainer} role="complementary">
<DocSidebar <DocSidebar
docsSidebars={docsSidebars} docsSidebars={docsSidebars}
path={isHomePage ? homePagePath : currentRoute.path} path={currentRoute.path}
sidebar={sidebar} sidebar={sidebar}
sidebarCollapsible={sidebarCollapsible} sidebarCollapsible={sidebarCollapsible}
/> />
@ -67,11 +53,7 @@ function DocPage(props) {
)} )}
<main className={styles.docMainContainer}> <main className={styles.docMainContainer}>
<MDXProvider components={MDXComponents}> <MDXProvider components={MDXComponents}>
{isHomePage ? ( {renderRoutes(baseRoute.routes)}
<DocItem content={content} />
) : (
renderRoutes(baseRoute.routes)
)}
</MDXProvider> </MDXProvider>
</main> </main>
</div> </div>

View file

@ -28,9 +28,15 @@ function usePrevious(value) {
return ref.current; return ref.current;
} }
// Compare the 2 paths, ignoring trailing /
const isSamePath = (path1, path2) => {
const normalize = (str) => (str.endsWith('/') ? str : `${str}/`);
return normalize(path1) === normalize(path2);
};
const isActiveSidebarItem = (item, activePath) => { const isActiveSidebarItem = (item, activePath) => {
if (item.type === 'link') { if (item.type === 'link') {
return item.href === activePath; return isSamePath(item.href, activePath);
} }
if (item.type === 'category') { if (item.type === 'category') {
return item.items.some((subItem) => return item.items.some((subItem) =>