feat(v2): auto-generated sidebars, frontmatter-less sites (#4582)

* POC of autogenerated sidebars

* use combine-promises utility lib

* autogenerated sidebar poc working

* Revert "autogenerated sidebar poc working"

This reverts commit c81da980

* POC of auto-generated sidebars for community docs

* update tests

* add initial test suite for autogenerated sidebars + fix some edge cases

* Improve autogen sidebars: strip more number prefixes in folder breadcrumb + slugs

* fix typo!

* Add tests for partially generated sidebars + fix edge cases + extract sidebar generation code

* Ability to read category metadatas file from a file in the category

* fix tests

* change position of API

* ability to extract number prefix

* stable system to enable position frontmatter

* fix tests for autogen sidebar position

* renamings

* restore community sidebars

* rename frontmatter position -> sidebar_position

* make sidebarItemsGenerator fn configurable

* minor changes

* rename dirPath => dirName

* Make the init template use autogenerated sidebars

* fix options

* fix docusaurus site: remove test docs

* add _category_ file to docs pathsToWatch

* add _category_ file to docs pathsToWatch

* tutorial: use sidebar_position instead of file number prefixes

* Adapt Docusaurus tutorial for autogenerated sidebars

* remove slug: /

* polish the homepage template

* rename _category_ sidebar_position to just "position"

* test for custom sidebarItemsGenerator fn

* fix category metadata + add link to report tutorial issues

* fix absolute path breaking tests

* fix absolute path breaking tests

* Add test for floating number sidebar_position

* add sidebarItemsGenerator unit tests

* add processSidebars unit tests

* Fix init template broken links

* windows test

* increase code translations test timeout

* cleanup mockCategoryMetadataFiles after windows test fixed

* update init template positions

* fix windows tests

* fix comment

* Add autogenerated sidebar items documentation + rewrite the full sidebars page doc

* add useful comment

* fix code block title
This commit is contained in:
Sébastien Lorber 2021-04-15 16:20:11 +02:00 committed by GitHub
parent 836f92708a
commit db79d462ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
67 changed files with 2887 additions and 306 deletions

View file

@ -1,38 +0,0 @@
---
title: Create a Document
---
Documents are a **group of pages** connected through a **sidebar**, a **previous/next navigation** and **versioning**.
## Create a Document
Create a markdown file at `docs/my-doc.md`:
```mdx title="docs/hello.md"
---
title: Hello, World!
---
## Hello, World!
This is your first document in **Docusaurus**, Congratulations!
```
A new document is now available at `http://localhost:3000/docs/hello`.
## Add your document to the sidebar
Add `hello` to the `sidebars.js` file:
```diff title="sidebars.js"
module.exports = {
docs: [
{
type: 'category',
label: 'Docusaurus Tutorial',
- items: ['getting-started', 'create-a-doc', ...],
+ items: ['getting-started', 'create-a-doc', 'hello', ...],
},
],
};
```

View file

@ -1,11 +1,16 @@
---
title: Getting Started
slug: /
sidebar_position: 1
---
Get started by **creating a new site**
# Tutorial Intro
Or **try Docusaurus immediately** with **[new.docusaurus.io](https://new.docusaurus.io)** (CodeSandbox).
Let's discover **Docusaurus in less than 5 minutes**.
## Getting Started
Get started by **creating a new site**.
Or **try Docusaurus immediately** with **[new.docusaurus.io](https://new.docusaurus.io)**.
## Generate a new site

View file

@ -0,0 +1,4 @@
{
"label": "Tutorial - Basics",
"position": 2
}

View file

@ -1,14 +1,16 @@
---
title: Congratulations!
sidebar_position: 6
---
Congratulations on making it this far!
# Congratulations!
You have learned the **basics of Docusaurus** and made some changes to the **initial template**.
You have just learned the **basics of Docusaurus** and made some changes to the **initial template**.
Docusaurus has **much more to offer**!
Have 5 more minutes? Take a look at **[versioning](./manage-docs-versions.md)** and **[i18n](./translate-your-site.md)**.
Have **5 more minutes**? Take a look at **[versioning](../tutorial-extras/manage-docs-versions.md)** and **[i18n](../tutorial-extras/translate-your-site.md)**.
Anything **unclear** or **buggy** in this tutorial? [Please report it!](https://github.com/facebook/docusaurus/discussions/4610)
## What's next?

View file

@ -1,10 +1,12 @@
---
title: Create a Blog Post
sidebar_position: 3
---
# Create a Blog Post
Docusaurus creates a **page for each blog post**, but also a **blog index page**, a **tag system**, an **RSS** feed...
## Create a Blog Post
## Create your first Post
Create a file at `blog/2021-02-28-greetings.md`:

View file

@ -0,0 +1,56 @@
---
sidebar_position: 2
---
# Create a Document
Documents are **groups of pages** connected through:
- a **sidebar**
- **previous/next navigation**
- **versioning**
## Create your first Doc
Create a markdown file at `docs/hello.md`:
```md title="docs/hello.md"
# Hello
This is my **first Docusaurus document**!
```
A new document is now available at `http://localhost:3000/docs/hello`.
## Configure the Sidebar
Docusaurus automatically **creates a sidebar** from the `docs` folder.
Add metadatas to customize the sidebar label and position:
```diff title="docs/hello.md"
+ ---
+ sidebar_label: "Hi!"
+ sidebar_position: 3
+ ---
# Hello
This is my **first Docusaurus document**!
```
It is also possible to create your sidebar explicitly in `sidebars.js`:
```diff title="sidebars.js"
module.exports = {
tutorialSidebar: [
{
type: 'category',
label: 'Tutorial',
- items: [...],
+ items: ['hello'],
},
],
};
```

View file

@ -1,14 +1,16 @@
---
title: Create a Page
sidebar_position: 1
---
Add **Markdown or React** files to `src/pages` to create **standalone pages**:
# Create a Page
Add **Markdown or React** files to `src/pages` to create a **standalone page**:
- `src/pages/index.js` -> `localhost:3000/`
- `src/pages/foo.md` -> `localhost:3000/foo`
- `src/pages/foo/bar.js` -> `localhost:3000/foo/bar`
## Create a React Page
## Create your first React Page
Create a file at `src/pages/my-react-page.js`:
@ -28,15 +30,11 @@ export default function MyReactPage() {
A new page is now available at `http://localhost:3000/my-react-page`.
## Create a Markdown Page
## Create your first Markdown Page
Create a file at `src/pages/my-markdown-page.md`:
```mdx title="src/pages/my-markdown-page.md"
---
title: My Markdown page
---
# My Markdown page
This is a Markdown page

View file

@ -1,8 +1,12 @@
---
title: Deploy your site
sidebar_position: 5
---
Docusaurus is a **static-site-generator** (also called [Jamstack](https://jamstack.org/)), and builds your site as **static HTML, JavaScript and CSS files**.
# Deploy your site
Docusaurus is a **static-site-generator** (also called **[Jamstack](https://jamstack.org/)**).
It builds your site as simple **static HTML, JavaScript and CSS files**.
## Build your site
@ -12,7 +16,7 @@ Build your site **for production**:
npm run build
```
The static files are generated in the `build` directory.
The static files are generated in the `build` folder.
## Deploy your site

View file

@ -1,20 +1,24 @@
---
title: Markdown Features
sidebar_position: 4
---
# Markdown Features
Docusaurus supports **[Markdown](https://daringfireball.net/projects/markdown/syntax)** and a few **additional features**.
## Front Matter
Markdown documents have metadata at the very top called [Front Matter](https://jekyllrb.com/docs/front-matter/):
Markdown documents have metadata at the top called [Front Matter](https://jekyllrb.com/docs/front-matter/):
```md
```text title="my-doc.md"
// highlight-start
---
id: my-doc
id: my-doc-id
title: My document title
description: My document description
sidebar_label: My doc
slug: /my-custom-url
---
// highlight-end
## Markdown heading

View file

@ -0,0 +1,4 @@
{
"label": "Tutorial - Extras",
"position": 3
}

View file

@ -1,7 +1,9 @@
---
title: Manage Docs Versions
sidebar_position: 1
---
# Manage Docs Versions
Docusaurus can manage multiple versions of your docs.
## Create a docs version
@ -12,7 +14,7 @@ Release a version 1.0 of your project:
npm run docusaurus docs:version 1.0
```
The `docs` directory is copied into `versioned_docs/version-1.0` and `versions.json` is created.
The `docs` folder is copied into `versioned_docs/version-1.0` and `versions.json` is created.
Your docs now have 2 versions:

View file

@ -1,7 +1,9 @@
---
title: Translate your site
sidebar_position: 2
---
# Translate your site
Let's translate `docs/getting-started.md` to French.
## Configure i18n
@ -19,7 +21,7 @@ module.exports = {
## Translate a doc
Copy the `docs/getting-started.md` file to the `i18n/fr` directory:
Copy the `docs/getting-started.md` file to the `i18n/fr` folder:
```bash
mkdir -p i18n/fr/docusaurus-plugin-content-docs/current/
@ -39,7 +41,7 @@ npm run start -- --locale fr
Your localized site is accessible at `http://localhost:3000/fr/` and the `Getting Started` page is translated.
:::warning
:::caution
In development, you can only use one locale at a same time.

View file

@ -19,7 +19,7 @@ module.exports = {
items: [
{
type: 'doc',
docId: 'getting-started',
docId: 'intro',
position: 'left',
label: 'Tutorial',
},
@ -38,8 +38,8 @@ module.exports = {
title: 'Docs',
items: [
{
label: 'Getting Started',
to: '/docs/',
label: 'Tutorial',
to: '/docs/intro',
},
],
},

View file

@ -1,22 +1,26 @@
/**
* Creating a sidebar enables you to:
- create an ordered group of docs
- render a sidebar for each doc of that group
- provide next/previous navigation
The sidebars can be generated from the filesystem, or explicitly defined here.
Create as many sidebars as you want.
*/
module.exports = {
tutorial: [
// By default, Docusaurus generates a sidebar from the docs folder structure
tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
// But you can create a sidebar manually
/*
tutorialSidebar: [
{
type: 'category',
label: 'Tutorial - Basics',
items: [
'getting-started',
'create-a-page',
'create-a-document',
'create-a-blog-post',
'markdown-features',
'deploy-your-site',
'congratulations',
],
},
{
type: 'category',
label: 'Tutorial - Extras',
items: ['manage-docs-versions', 'translate-your-site'],
label: 'Tutorial',
items: ['hello'],
},
],
*/
};

View file

@ -41,8 +41,10 @@ function Feature({Svg, title, description}) {
<div className="text--center">
<Svg className={styles.featureSvg} alt={title} />
</div>
<h3>{title}</h3>
<p>{description}</p>
<div className="text--center padding-horiz--md">
<h3>{title}</h3>
<p>{description}</p>
</div>
</div>
);
}

View file

@ -14,8 +14,10 @@ function HomepageHeader() {
<h1 className="hero__title">{siteConfig.title}</h1>
<p className="hero__subtitle">{siteConfig.tagline}</p>
<div className={styles.buttons}>
<Link className="button button--secondary button--lg" to="/docs">
Get Started - Docusaurus Tutorial
<Link
className="button button--secondary button--lg"
to="/docs/intro">
Docusaurus Tutorial - 5min
</Link>
</div>
</div>

View file

@ -20,6 +20,7 @@
"devDependencies": {
"@docusaurus/module-type-aliases": "2.0.0-alpha.72",
"@types/picomatch": "^2.2.1",
"@types/js-yaml": "^4.0.0",
"commander": "^5.1.0",
"picomatch": "^2.1.1"
},
@ -30,10 +31,12 @@
"@docusaurus/utils": "2.0.0-alpha.72",
"@docusaurus/utils-validation": "2.0.0-alpha.72",
"chalk": "^4.1.0",
"combine-promises": "^1.1.0",
"execa": "^5.0.0",
"fs-extra": "^9.1.0",
"globby": "^11.0.2",
"import-fresh": "^3.2.2",
"js-yaml": "^4.0.0",
"loader-utils": "^1.2.3",
"lodash": "^4.17.20",
"remark-admonitions": "^1.2.1",

View file

@ -0,0 +1,3 @@
{
"label": "API (label from _category_.json)"
}

View file

@ -0,0 +1,8 @@
---
id: guide2.5
sidebar_position: 2.5
---
# Guide 2.5
Guide 2.5 text

View file

@ -0,0 +1,7 @@
---
id: guide2
---
# Guide 2
Guide 2 text

View file

@ -0,0 +1,7 @@
---
id: guide4
---
# Guide 4
Guide 4 text

View file

@ -0,0 +1,7 @@
---
id: guide5
---
# Guide 5
Guide 5 text

View file

@ -0,0 +1,8 @@
---
id: guide3
sidebar_position: 3
---
# Guide 3
Guide 3 text

View file

@ -0,0 +1,8 @@
---
id: guide1
sidebar_position: 1
---
# Guide 1
Guide 1 text

View file

@ -0,0 +1,14 @@
/**
* 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.
*/
module.exports = {
title: 'My Site',
tagline: 'The tagline of my site',
url: 'https://your-docusaurus-test-site.com',
baseUrl: '/',
favicon: 'img/favicon.ico',
};

View file

@ -0,0 +1,23 @@
/**
* 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.
*/
module.exports = {
someSidebar: [
{type: 'doc', id: 'API/api-end'},
{
type: 'category',
label: 'Some category',
items: [
{type: 'doc', id: 'API/api-overview'},
{
type: 'autogenerated',
dirName: '3-API/02_Extension APIs',
},
],
},
],
};

View file

@ -149,6 +149,7 @@ Object {
\\"title\\": \\"Bar\\",
\\"description\\": \\"This is custom description\\",
\\"source\\": \\"@site/docs/foo/bar.md\\",
\\"sourceDirName\\": \\"foo\\",
\\"slug\\": \\"/foo/bar\\",
\\"permalink\\": \\"/docs/foo/bar\\",
\\"version\\": \\"current\\",
@ -170,6 +171,7 @@ Object {
\\"title\\": \\"baz\\",
\\"description\\": \\"Images\\",
\\"source\\": \\"@site/docs/foo/baz.md\\",
\\"sourceDirName\\": \\"foo\\",
\\"slug\\": \\"/foo/bazSlug.html\\",
\\"permalink\\": \\"/docs/foo/bazSlug.html\\",
\\"version\\": \\"current\\",
@ -195,6 +197,7 @@ Object {
\\"title\\": \\"My heading as title\\",
\\"description\\": \\"\\",
\\"source\\": \\"@site/docs/headingAsTitle.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/headingAsTitle\\",
\\"permalink\\": \\"/docs/headingAsTitle\\",
\\"version\\": \\"current\\",
@ -207,6 +210,7 @@ Object {
\\"title\\": \\"Hello, World !\\",
\\"description\\": \\"Hi, Endilie here :)\\",
\\"source\\": \\"@site/docs/hello.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/\\",
\\"permalink\\": \\"/docs/\\",
\\"version\\": \\"current\\",
@ -227,6 +231,7 @@ Object {
\\"title\\": \\"ipsum\\",
\\"description\\": \\"Lorem ipsum.\\",
\\"source\\": \\"@site/docs/ipsum.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/ipsum\\",
\\"permalink\\": \\"/docs/ipsum\\",
\\"editUrl\\": null,
@ -242,6 +247,7 @@ Object {
\\"title\\": \\"lorem\\",
\\"description\\": \\"Lorem ipsum.\\",
\\"source\\": \\"@site/docs/lorem.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/lorem\\",
\\"permalink\\": \\"/docs/lorem\\",
\\"editUrl\\": \\"https://github.com/customUrl/docs/lorem.md\\",
@ -258,6 +264,7 @@ Object {
\\"title\\": \\"rootAbsoluteSlug\\",
\\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/docs/rootAbsoluteSlug.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/rootAbsoluteSlug\\",
\\"permalink\\": \\"/docs/rootAbsoluteSlug\\",
\\"version\\": \\"current\\",
@ -272,6 +279,7 @@ Object {
\\"title\\": \\"rootRelativeSlug\\",
\\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/docs/rootRelativeSlug.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/rootRelativeSlug\\",
\\"permalink\\": \\"/docs/rootRelativeSlug\\",
\\"version\\": \\"current\\",
@ -286,6 +294,7 @@ Object {
\\"title\\": \\"rootResolvedSlug\\",
\\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/docs/rootResolvedSlug.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/hey/rootResolvedSlug\\",
\\"permalink\\": \\"/docs/hey/rootResolvedSlug\\",
\\"version\\": \\"current\\",
@ -300,6 +309,7 @@ Object {
\\"title\\": \\"rootTryToEscapeSlug\\",
\\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/docs/rootTryToEscapeSlug.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/rootTryToEscapeSlug\\",
\\"permalink\\": \\"/docs/rootTryToEscapeSlug\\",
\\"version\\": \\"current\\",
@ -314,6 +324,7 @@ Object {
\\"title\\": \\"absoluteSlug\\",
\\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/docs/slugs/absoluteSlug.md\\",
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/absoluteSlug\\",
\\"permalink\\": \\"/docs/absoluteSlug\\",
\\"version\\": \\"current\\",
@ -328,6 +339,7 @@ Object {
\\"title\\": \\"relativeSlug\\",
\\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/docs/slugs/relativeSlug.md\\",
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/slugs/relativeSlug\\",
\\"permalink\\": \\"/docs/slugs/relativeSlug\\",
\\"version\\": \\"current\\",
@ -342,6 +354,7 @@ Object {
\\"title\\": \\"resolvedSlug\\",
\\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/docs/slugs/resolvedSlug.md\\",
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/slugs/hey/resolvedSlug\\",
\\"permalink\\": \\"/docs/slugs/hey/resolvedSlug\\",
\\"version\\": \\"current\\",
@ -356,6 +369,7 @@ Object {
\\"title\\": \\"tryToEscapeSlug\\",
\\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/docs/slugs/tryToEscapeSlug.md\\",
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/tryToEscapeSlug\\",
\\"permalink\\": \\"/docs/tryToEscapeSlug\\",
\\"version\\": \\"current\\",
@ -646,6 +660,134 @@ Array [
]
`;
exports[`site with custom sidebar items generator sidebarItemsGenerator is called with appropriate data 1`] = `
Object {
"docs": Array [
Object {
"frontMatter": Object {},
"id": "API/Core APIs/Client API",
"sidebarPosition": 0,
"source": "@site/docs/3-API/01_Core APIs/0 --- Client API.md",
"sourceDirName": "3-API/01_Core APIs",
},
Object {
"frontMatter": Object {},
"id": "API/Core APIs/Server API",
"sidebarPosition": 1,
"source": "@site/docs/3-API/01_Core APIs/1 --- Server API.md",
"sourceDirName": "3-API/01_Core APIs",
},
Object {
"frontMatter": Object {},
"id": "API/Extension APIs/Plugin API",
"sidebarPosition": 0,
"source": "@site/docs/3-API/02_Extension APIs/0. Plugin API.md",
"sourceDirName": "3-API/02_Extension APIs",
},
Object {
"frontMatter": Object {},
"id": "API/Extension APIs/Theme API",
"sidebarPosition": 1,
"source": "@site/docs/3-API/02_Extension APIs/1. Theme API.md",
"sourceDirName": "3-API/02_Extension APIs",
},
Object {
"frontMatter": Object {},
"id": "API/api-end",
"sidebarPosition": 3,
"source": "@site/docs/3-API/03_api-end.md",
"sourceDirName": "3-API",
},
Object {
"frontMatter": Object {},
"id": "API/api-overview",
"sidebarPosition": 0,
"source": "@site/docs/3-API/00_api-overview.md",
"sourceDirName": "3-API",
},
Object {
"frontMatter": Object {
"id": "guide1",
"sidebar_position": 1,
},
"id": "Guides/guide1",
"sidebarPosition": 1,
"source": "@site/docs/Guides/z-guide1.md",
"sourceDirName": "Guides",
},
Object {
"frontMatter": Object {
"id": "guide2",
},
"id": "Guides/guide2",
"sidebarPosition": 2,
"source": "@site/docs/Guides/02-guide2.md",
"sourceDirName": "Guides",
},
Object {
"frontMatter": Object {
"id": "guide2.5",
"sidebar_position": 2.5,
},
"id": "Guides/guide2.5",
"sidebarPosition": 2.5,
"source": "@site/docs/Guides/0-guide2.5.md",
"sourceDirName": "Guides",
},
Object {
"frontMatter": Object {
"id": "guide3",
"sidebar_position": 3,
},
"id": "Guides/guide3",
"sidebarPosition": 3,
"source": "@site/docs/Guides/guide3.md",
"sourceDirName": "Guides",
},
Object {
"frontMatter": Object {
"id": "guide4",
},
"id": "Guides/guide4",
"sidebarPosition": undefined,
"source": "@site/docs/Guides/a-guide4.md",
"sourceDirName": "Guides",
},
Object {
"frontMatter": Object {
"id": "guide5",
},
"id": "Guides/guide5",
"sidebarPosition": undefined,
"source": "@site/docs/Guides/b-guide5.md",
"sourceDirName": "Guides",
},
Object {
"frontMatter": Object {},
"id": "getting-started",
"sidebarPosition": 0,
"source": "@site/docs/0-getting-started.md",
"sourceDirName": ".",
},
Object {
"frontMatter": Object {},
"id": "installation",
"sidebarPosition": 1,
"source": "@site/docs/1-installation.md",
"sourceDirName": ".",
},
],
"item": Object {
"dirName": ".",
"type": "autogenerated",
},
"version": Object {
"contentPath": "docs",
"versionName": "current",
},
}
`;
exports[`site with wrong sidebar file 1`] = `
"Bad sidebars file.
These sidebar document ids do not exist:
@ -699,6 +841,7 @@ Object {
\\"title\\": \\"team\\",
\\"description\\": \\"Team 1.0.0\\",
\\"source\\": \\"@site/community_versioned_docs/version-1.0.0/team.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/team\\",
\\"permalink\\": \\"/community/team\\",
\\"version\\": \\"1.0.0\\",
@ -712,6 +855,7 @@ Object {
\\"title\\": \\"Team title translated\\",
\\"description\\": \\"Team current version (translated)\\",
\\"source\\": \\"@site/i18n/en/docusaurus-plugin-content-docs-community/current/team.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/team\\",
\\"permalink\\": \\"/community/next/team\\",
\\"version\\": \\"current\\",
@ -942,6 +1086,7 @@ Object {
\\"title\\": \\"bar\\",
\\"description\\": \\"This is next version of bar.\\",
\\"source\\": \\"@site/docs/foo/bar.md\\",
\\"sourceDirName\\": \\"foo\\",
\\"slug\\": \\"/foo/barSlug\\",
\\"permalink\\": \\"/docs/next/foo/barSlug\\",
\\"version\\": \\"current\\",
@ -961,6 +1106,7 @@ Object {
\\"title\\": \\"hello\\",
\\"description\\": \\"Hello next !\\",
\\"source\\": \\"@site/docs/hello.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/\\",
\\"permalink\\": \\"/docs/next/\\",
\\"version\\": \\"current\\",
@ -978,6 +1124,7 @@ Object {
\\"title\\": \\"absoluteSlug\\",
\\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/docs/slugs/absoluteSlug.md\\",
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/absoluteSlug\\",
\\"permalink\\": \\"/docs/next/absoluteSlug\\",
\\"version\\": \\"current\\",
@ -992,6 +1139,7 @@ Object {
\\"title\\": \\"relativeSlug\\",
\\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/docs/slugs/relativeSlug.md\\",
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/slugs/relativeSlug\\",
\\"permalink\\": \\"/docs/next/slugs/relativeSlug\\",
\\"version\\": \\"current\\",
@ -1006,6 +1154,7 @@ Object {
\\"title\\": \\"resolvedSlug\\",
\\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/docs/slugs/resolvedSlug.md\\",
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/slugs/hey/resolvedSlug\\",
\\"permalink\\": \\"/docs/next/slugs/hey/resolvedSlug\\",
\\"version\\": \\"current\\",
@ -1020,6 +1169,7 @@ Object {
\\"title\\": \\"tryToEscapeSlug\\",
\\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/docs/slugs/tryToEscapeSlug.md\\",
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/tryToEscapeSlug\\",
\\"permalink\\": \\"/docs/next/tryToEscapeSlug\\",
\\"version\\": \\"current\\",
@ -1034,6 +1184,7 @@ Object {
\\"title\\": \\"hello\\",
\\"description\\": \\"Hello 1.0.0 ! (translated en)\\",
\\"source\\": \\"@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/\\",
\\"permalink\\": \\"/docs/1.0.0/\\",
\\"version\\": \\"1.0.0\\",
@ -1051,6 +1202,7 @@ Object {
\\"title\\": \\"bar\\",
\\"description\\": \\"Bar 1.0.0 !\\",
\\"source\\": \\"@site/versioned_docs/version-1.0.0/foo/bar.md\\",
\\"sourceDirName\\": \\"foo\\",
\\"slug\\": \\"/foo/barSlug\\",
\\"permalink\\": \\"/docs/1.0.0/foo/barSlug\\",
\\"version\\": \\"1.0.0\\",
@ -1070,6 +1222,7 @@ Object {
\\"title\\": \\"baz\\",
\\"description\\": \\"Baz 1.0.0 ! This will be deleted in next subsequent versions.\\",
\\"source\\": \\"@site/versioned_docs/version-1.0.0/foo/baz.md\\",
\\"sourceDirName\\": \\"foo\\",
\\"slug\\": \\"/foo/baz\\",
\\"permalink\\": \\"/docs/1.0.0/foo/baz\\",
\\"version\\": \\"1.0.0\\",
@ -1091,6 +1244,7 @@ Object {
\\"title\\": \\"bar\\",
\\"description\\": \\"Bar 1.0.1 !\\",
\\"source\\": \\"@site/versioned_docs/version-1.0.1/foo/bar.md\\",
\\"sourceDirName\\": \\"foo\\",
\\"slug\\": \\"/foo/bar\\",
\\"permalink\\": \\"/docs/foo/bar\\",
\\"version\\": \\"1.0.1\\",
@ -1108,6 +1262,7 @@ Object {
\\"title\\": \\"hello\\",
\\"description\\": \\"Hello 1.0.1 !\\",
\\"source\\": \\"@site/versioned_docs/version-1.0.1/hello.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/\\",
\\"permalink\\": \\"/docs/\\",
\\"version\\": \\"1.0.1\\",
@ -1125,6 +1280,7 @@ Object {
\\"title\\": \\"rootAbsoluteSlug\\",
\\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/versioned_docs/version-withSlugs/rootAbsoluteSlug.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/rootAbsoluteSlug\\",
\\"permalink\\": \\"/docs/withSlugs/rootAbsoluteSlug\\",
\\"version\\": \\"withSlugs\\",
@ -1140,6 +1296,7 @@ Object {
\\"title\\": \\"rootRelativeSlug\\",
\\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/versioned_docs/version-withSlugs/rootRelativeSlug.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/rootRelativeSlug\\",
\\"permalink\\": \\"/docs/withSlugs/rootRelativeSlug\\",
\\"version\\": \\"withSlugs\\",
@ -1154,6 +1311,7 @@ Object {
\\"title\\": \\"rootResolvedSlug\\",
\\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/versioned_docs/version-withSlugs/rootResolvedSlug.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/hey/rootResolvedSlug\\",
\\"permalink\\": \\"/docs/withSlugs/hey/rootResolvedSlug\\",
\\"version\\": \\"withSlugs\\",
@ -1168,6 +1326,7 @@ Object {
\\"title\\": \\"rootTryToEscapeSlug\\",
\\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/versioned_docs/version-withSlugs/rootTryToEscapeSlug.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/rootTryToEscapeSlug\\",
\\"permalink\\": \\"/docs/withSlugs/rootTryToEscapeSlug\\",
\\"version\\": \\"withSlugs\\",
@ -1182,6 +1341,7 @@ Object {
\\"title\\": \\"absoluteSlug\\",
\\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/versioned_docs/version-withSlugs/slugs/absoluteSlug.md\\",
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/absoluteSlug\\",
\\"permalink\\": \\"/docs/withSlugs/absoluteSlug\\",
\\"version\\": \\"withSlugs\\",
@ -1196,6 +1356,7 @@ Object {
\\"title\\": \\"relativeSlug\\",
\\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/versioned_docs/version-withSlugs/slugs/relativeSlug.md\\",
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/slugs/relativeSlug\\",
\\"permalink\\": \\"/docs/withSlugs/slugs/relativeSlug\\",
\\"version\\": \\"withSlugs\\",
@ -1210,6 +1371,7 @@ Object {
\\"title\\": \\"resolvedSlug\\",
\\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/versioned_docs/version-withSlugs/slugs/resolvedSlug.md\\",
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/slugs/hey/resolvedSlug\\",
\\"permalink\\": \\"/docs/withSlugs/slugs/hey/resolvedSlug\\",
\\"version\\": \\"withSlugs\\",
@ -1224,6 +1386,7 @@ Object {
\\"title\\": \\"tryToEscapeSlug\\",
\\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/versioned_docs/version-withSlugs/slugs/tryToEscapeSlug.md\\",
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/tryToEscapeSlug\\",
\\"permalink\\": \\"/docs/withSlugs/tryToEscapeSlug\\",
\\"version\\": \\"withSlugs\\",

View file

@ -177,6 +177,7 @@ describe('simple site', () => {
version: 'current',
id: 'foo/bar',
unversionedId: 'foo/bar',
sourceDirName: 'foo',
isDocsHomePage: false,
permalink: '/docs/foo/bar',
slug: '/foo/bar',
@ -192,6 +193,7 @@ describe('simple site', () => {
version: 'current',
id: 'hello',
unversionedId: 'hello',
sourceDirName: '.',
isDocsHomePage: false,
permalink: '/docs/hello',
slug: '/hello',
@ -220,6 +222,7 @@ describe('simple site', () => {
version: 'current',
id: 'hello',
unversionedId: 'hello',
sourceDirName: '.',
isDocsHomePage: true,
permalink: '/docs/',
slug: '/',
@ -248,6 +251,7 @@ describe('simple site', () => {
version: 'current',
id: 'foo/bar',
unversionedId: 'foo/bar',
sourceDirName: 'foo',
isDocsHomePage: true,
permalink: '/docs/',
slug: '/',
@ -279,6 +283,7 @@ describe('simple site', () => {
version: 'current',
id: 'foo/baz',
unversionedId: 'foo/baz',
sourceDirName: 'foo',
isDocsHomePage: false,
permalink: '/docs/foo/bazSlug.html',
slug: '/foo/bazSlug.html',
@ -301,6 +306,7 @@ describe('simple site', () => {
version: 'current',
id: 'lorem',
unversionedId: 'lorem',
sourceDirName: '.',
isDocsHomePage: false,
permalink: '/docs/lorem',
slug: '/lorem',
@ -336,6 +342,7 @@ describe('simple site', () => {
version: 'current',
id: 'foo/baz',
unversionedId: 'foo/baz',
sourceDirName: 'foo',
isDocsHomePage: false,
permalink: '/docs/foo/bazSlug.html',
slug: '/foo/bazSlug.html',
@ -378,6 +385,7 @@ describe('simple site', () => {
version: 'current',
id: 'lorem',
unversionedId: 'lorem',
sourceDirName: '.',
isDocsHomePage: false,
permalink: '/docs/lorem',
slug: '/lorem',
@ -549,6 +557,7 @@ describe('versioned site', () => {
await currentVersionTestUtils.testMeta(path.join('foo', 'bar.md'), {
id: 'foo/bar',
unversionedId: 'foo/bar',
sourceDirName: 'foo',
isDocsHomePage: false,
permalink: '/docs/next/foo/barSlug',
slug: '/foo/barSlug',
@ -560,6 +569,7 @@ describe('versioned site', () => {
await currentVersionTestUtils.testMeta(path.join('hello.md'), {
id: 'hello',
unversionedId: 'hello',
sourceDirName: '.',
isDocsHomePage: false,
permalink: '/docs/next/hello',
slug: '/hello',
@ -576,6 +586,7 @@ describe('versioned site', () => {
await version100TestUtils.testMeta(path.join('foo', 'bar.md'), {
id: 'version-1.0.0/foo/bar',
unversionedId: 'foo/bar',
sourceDirName: 'foo',
isDocsHomePage: false,
permalink: '/docs/1.0.0/foo/barSlug',
slug: '/foo/barSlug',
@ -587,6 +598,7 @@ describe('versioned site', () => {
await version100TestUtils.testMeta(path.join('hello.md'), {
id: 'version-1.0.0/hello',
unversionedId: 'hello',
sourceDirName: '.',
isDocsHomePage: false,
permalink: '/docs/1.0.0/hello',
slug: '/hello',
@ -600,6 +612,7 @@ describe('versioned site', () => {
await version101TestUtils.testMeta(path.join('foo', 'bar.md'), {
id: 'version-1.0.1/foo/bar',
unversionedId: 'foo/bar',
sourceDirName: 'foo',
isDocsHomePage: false,
permalink: '/docs/foo/bar',
slug: '/foo/bar',
@ -611,6 +624,7 @@ describe('versioned site', () => {
await version101TestUtils.testMeta(path.join('hello.md'), {
id: 'version-1.0.1/hello',
unversionedId: 'hello',
sourceDirName: '.',
isDocsHomePage: false,
permalink: '/docs/hello',
slug: '/hello',
@ -701,6 +715,7 @@ describe('versioned site', () => {
await testUtilsLocal.testMeta(path.join('hello.md'), {
id: 'version-1.0.0/hello',
unversionedId: 'hello',
sourceDirName: '.',
isDocsHomePage: false,
permalink: '/docs/1.0.0/hello',
slug: '/hello',
@ -741,6 +756,7 @@ describe('versioned site', () => {
await testUtilsLocal.testMeta(path.join('hello.md'), {
id: 'version-1.0.0/hello',
unversionedId: 'hello',
sourceDirName: '.',
isDocsHomePage: false,
permalink: '/docs/1.0.0/hello',
slug: '/hello',
@ -773,6 +789,7 @@ describe('versioned site', () => {
await testUtilsLocal.testMeta(path.join('hello.md'), {
id: 'version-1.0.0/hello',
unversionedId: 'hello',
sourceDirName: '.',
isDocsHomePage: false,
permalink: '/docs/1.0.0/hello',
slug: '/hello',
@ -806,6 +823,7 @@ describe('versioned site', () => {
await testUtilsLocal.testMeta(path.join('hello.md'), {
id: 'version-1.0.0/hello',
unversionedId: 'hello',
sourceDirName: '.',
isDocsHomePage: false,
permalink: '/fr/docs/1.0.0/hello',
slug: '/hello',
@ -840,6 +858,7 @@ describe('versioned site', () => {
await testUtilsLocal.testMeta(path.join('hello.md'), {
id: 'version-1.0.0/hello',
unversionedId: 'hello',
sourceDirName: '.',
isDocsHomePage: false,
permalink: '/fr/docs/1.0.0/hello',
slug: '/hello',

View file

@ -10,7 +10,7 @@
import path from 'path';
import {isMatch} from 'picomatch';
import commander from 'commander';
import {kebabCase} from 'lodash';
import {kebabCase, orderBy} from 'lodash';
import fs from 'fs-extra';
import pluginContentDocs from '../index';
@ -24,7 +24,7 @@ import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
import * as cliDocs from '../cli';
import {OptionsSchema} from '../options';
import {normalizePluginOptions} from '@docusaurus/utils-validation';
import {DocMetadata, LoadedVersion} from '../types';
import {DocMetadata, LoadedVersion, SidebarItemsGenerator} from '../types';
import {toSidebarsProp} from '../props';
// @ts-expect-error: TODO typedefs missing?
@ -33,6 +33,17 @@ import {validate} from 'webpack';
function findDocById(version: LoadedVersion, unversionedId: string) {
return version.docs.find((item) => item.unversionedId === unversionedId);
}
function getDocById(version: LoadedVersion, unversionedId: string) {
const doc = findDocById(version, unversionedId);
if (!doc) {
throw new Error(
`No doc found with id=${unversionedId} in version ${version.versionName}.
Available ids=\n- ${version.docs.map((d) => d.unversionedId).join('\n- ')}`,
);
}
return doc;
}
const defaultDocMetadata: Partial<DocMetadata> = {
next: undefined,
previous: undefined,
@ -40,6 +51,7 @@ const defaultDocMetadata: Partial<DocMetadata> = {
lastUpdatedAt: undefined,
lastUpdatedBy: undefined,
sidebar_label: undefined,
formattedLastUpdatedAt: undefined,
};
const createFakeActions = (contentDir: string) => {
@ -203,6 +215,7 @@ describe('simple website', () => {
"sidebars.json",
"i18n/en/docusaurus-plugin-content-docs/current/**/*.{md,mdx}",
"docs/**/*.{md,mdx}",
"docs/**/_category_.{json,yml,yaml}",
]
`);
expect(isMatch('docs/hello.md', matchPattern)).toEqual(true);
@ -247,6 +260,7 @@ describe('simple website', () => {
version: 'current',
id: 'hello',
unversionedId: 'hello',
sourceDirName: '.',
isDocsHomePage: true,
permalink: '/docs/',
slug: '/',
@ -268,11 +282,12 @@ describe('simple website', () => {
},
});
expect(findDocById(currentVersion, 'foo/bar')).toEqual({
expect(getDocById(currentVersion, 'foo/bar')).toEqual({
...defaultDocMetadata,
version: 'current',
id: 'foo/bar',
unversionedId: 'foo/bar',
sourceDirName: 'foo',
isDocsHomePage: false,
next: {
title: 'baz',
@ -368,15 +383,19 @@ describe('versioned website', () => {
"sidebars.json",
"i18n/en/docusaurus-plugin-content-docs/current/**/*.{md,mdx}",
"docs/**/*.{md,mdx}",
"docs/**/_category_.{json,yml,yaml}",
"versioned_sidebars/version-1.0.1-sidebars.json",
"i18n/en/docusaurus-plugin-content-docs/version-1.0.1/**/*.{md,mdx}",
"versioned_docs/version-1.0.1/**/*.{md,mdx}",
"versioned_docs/version-1.0.1/**/_category_.{json,yml,yaml}",
"versioned_sidebars/version-1.0.0-sidebars.json",
"i18n/en/docusaurus-plugin-content-docs/version-1.0.0/**/*.{md,mdx}",
"versioned_docs/version-1.0.0/**/*.{md,mdx}",
"versioned_docs/version-1.0.0/**/_category_.{json,yml,yaml}",
"versioned_sidebars/version-withSlugs-sidebars.json",
"i18n/en/docusaurus-plugin-content-docs/version-withSlugs/**/*.{md,mdx}",
"versioned_docs/version-withSlugs/**/*.{md,mdx}",
"versioned_docs/version-withSlugs/**/_category_.{json,yml,yaml}",
]
`);
expect(isMatch('docs/hello.md', matchPattern)).toEqual(true);
@ -427,10 +446,11 @@ describe('versioned website', () => {
expect(findDocById(version101, 'foo/baz')).toBeUndefined();
expect(findDocById(versionWithSlugs, 'foo/baz')).toBeUndefined();
expect(findDocById(currentVersion, 'foo/bar')).toEqual({
expect(getDocById(currentVersion, 'foo/bar')).toEqual({
...defaultDocMetadata,
id: 'foo/bar',
unversionedId: 'foo/bar',
sourceDirName: 'foo',
isDocsHomePage: false,
permalink: '/docs/next/foo/barSlug',
slug: '/foo/barSlug',
@ -452,10 +472,11 @@ describe('versioned website', () => {
permalink: '/docs/next/',
},
});
expect(findDocById(currentVersion, 'hello')).toEqual({
expect(getDocById(currentVersion, 'hello')).toEqual({
...defaultDocMetadata,
id: 'hello',
unversionedId: 'hello',
sourceDirName: '.',
isDocsHomePage: true,
permalink: '/docs/next/',
slug: '/',
@ -474,10 +495,11 @@ describe('versioned website', () => {
permalink: '/docs/next/foo/barSlug',
},
});
expect(findDocById(version101, 'hello')).toEqual({
expect(getDocById(version101, 'hello')).toEqual({
...defaultDocMetadata,
id: 'version-1.0.1/hello',
unversionedId: 'hello',
sourceDirName: '.',
isDocsHomePage: true,
permalink: '/docs/',
slug: '/',
@ -496,10 +518,11 @@ describe('versioned website', () => {
permalink: '/docs/foo/bar',
},
});
expect(findDocById(version100, 'foo/baz')).toEqual({
expect(getDocById(version100, 'foo/baz')).toEqual({
...defaultDocMetadata,
id: 'version-1.0.0/foo/baz',
unversionedId: 'foo/baz',
sourceDirName: 'foo',
isDocsHomePage: false,
permalink: '/docs/1.0.0/foo/baz',
slug: '/foo/baz',
@ -611,9 +634,11 @@ describe('versioned website (community)', () => {
"community_sidebars.json",
"i18n/en/docusaurus-plugin-content-docs-community/current/**/*.{md,mdx}",
"community/**/*.{md,mdx}",
"community/**/_category_.{json,yml,yaml}",
"community_versioned_sidebars/version-1.0.0-sidebars.json",
"i18n/en/docusaurus-plugin-content-docs-community/version-1.0.0/**/*.{md,mdx}",
"community_versioned_docs/version-1.0.0/**/*.{md,mdx}",
"community_versioned_docs/version-1.0.0/**/_category_.{json,yml,yaml}",
]
`);
expect(isMatch('community/team.md', matchPattern)).toEqual(true);
@ -644,10 +669,11 @@ describe('versioned website (community)', () => {
expect(content.loadedVersions.length).toEqual(2);
const [currentVersion, version100] = content.loadedVersions;
expect(findDocById(currentVersion, 'team')).toEqual({
expect(getDocById(currentVersion, 'team')).toEqual({
...defaultDocMetadata,
id: 'team',
unversionedId: 'team',
sourceDirName: '.',
isDocsHomePage: false,
permalink: '/community/next/team',
slug: '/team',
@ -659,10 +685,11 @@ describe('versioned website (community)', () => {
sidebar: 'community',
frontMatter: {title: 'Team title translated'},
});
expect(findDocById(version100, 'team')).toEqual({
expect(getDocById(version100, 'team')).toEqual({
...defaultDocMetadata,
id: 'version-1.0.0/team',
unversionedId: 'team',
sourceDirName: '.',
isDocsHomePage: false,
permalink: '/community/team',
slug: '/team',
@ -709,7 +736,7 @@ describe('site with doc label', () => {
}),
);
const content = await plugin.loadContent();
const content = (await plugin.loadContent?.())!;
return {content};
}
@ -730,3 +757,807 @@ describe('site with doc label', () => {
expect(sidebarProps.docs[1].label).toBe('Hello 2 From Doc');
});
});
describe('site with full autogenerated sidebar', () => {
async function loadSite() {
const siteDir = path.join(
__dirname,
'__fixtures__',
'site-with-autogenerated-sidebar',
);
const context = await loadContext(siteDir);
const plugin = pluginContentDocs(
context,
normalizePluginOptions(OptionsSchema, {
path: 'docs',
}),
);
const content = (await plugin.loadContent?.())!;
return {content, siteDir};
}
test('sidebar is fully autogenerated', async () => {
const {content} = await loadSite();
const version = content.loadedVersions[0];
expect(version.sidebars).toEqual({
defaultSidebar: [
{
type: 'doc',
id: 'getting-started',
},
{
type: 'doc',
id: 'installation',
},
{
type: 'category',
label: 'Guides',
collapsed: true,
items: [
{
type: 'doc',
id: 'Guides/guide1',
},
{
type: 'doc',
id: 'Guides/guide2',
},
{
type: 'doc',
id: 'Guides/guide2.5',
},
{
type: 'doc',
id: 'Guides/guide3',
},
{
type: 'doc',
id: 'Guides/guide4',
},
{
type: 'doc',
id: 'Guides/guide5',
},
],
},
{
type: 'category',
label: 'API (label from _category_.json)',
collapsed: true,
items: [
{
type: 'doc',
id: 'API/api-overview',
},
{
type: 'category',
label: 'Core APIs',
collapsed: true,
items: [
{
type: 'doc',
id: 'API/Core APIs/Client API',
},
{
type: 'doc',
id: 'API/Core APIs/Server API',
},
],
},
{
type: 'category',
label: 'Extension APIs (label from _category_.yml)',
collapsed: true,
items: [
{
type: 'doc',
id: 'API/Extension APIs/Plugin API',
},
{
type: 'doc',
id: 'API/Extension APIs/Theme API',
},
],
},
{
type: 'doc',
id: 'API/api-end',
},
],
},
],
});
});
test('docs in fully generated sidebar have correct metadatas', async () => {
const {content, siteDir} = await loadSite();
const version = content.loadedVersions[0];
expect(getDocById(version, 'getting-started')).toEqual({
...defaultDocMetadata,
id: 'getting-started',
unversionedId: 'getting-started',
sourceDirName: '.',
isDocsHomePage: false,
permalink: '/docs/getting-started',
slug: '/getting-started',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'0-getting-started.md',
),
title: 'Getting Started',
description: 'Getting started text',
version: 'current',
sidebar: 'defaultSidebar',
frontMatter: {},
sidebarPosition: 0,
previous: undefined,
next: {
permalink: '/docs/installation',
title: 'Installation',
},
});
expect(getDocById(version, 'installation')).toEqual({
...defaultDocMetadata,
id: 'installation',
unversionedId: 'installation',
sourceDirName: '.',
isDocsHomePage: false,
permalink: '/docs/installation',
slug: '/installation',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'1-installation.md',
),
title: 'Installation',
description: 'Installation text',
version: 'current',
sidebar: 'defaultSidebar',
frontMatter: {},
sidebarPosition: 1,
previous: {
permalink: '/docs/getting-started',
title: 'Getting Started',
},
next: {
permalink: '/docs/Guides/guide1',
title: 'Guide 1',
},
});
expect(getDocById(version, 'Guides/guide1')).toEqual({
...defaultDocMetadata,
id: 'Guides/guide1',
unversionedId: 'Guides/guide1',
sourceDirName: 'Guides',
isDocsHomePage: false,
permalink: '/docs/Guides/guide1',
slug: '/Guides/guide1',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'Guides',
'z-guide1.md',
),
title: 'Guide 1',
description: 'Guide 1 text',
version: 'current',
sidebar: 'defaultSidebar',
frontMatter: {
id: 'guide1',
sidebar_position: 1,
},
sidebarPosition: 1,
previous: {
permalink: '/docs/installation',
title: 'Installation',
},
next: {
permalink: '/docs/Guides/guide2',
title: 'Guide 2',
},
});
expect(getDocById(version, 'Guides/guide2')).toEqual({
...defaultDocMetadata,
id: 'Guides/guide2',
unversionedId: 'Guides/guide2',
sourceDirName: 'Guides',
isDocsHomePage: false,
permalink: '/docs/Guides/guide2',
slug: '/Guides/guide2',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'Guides',
'02-guide2.md',
),
title: 'Guide 2',
description: 'Guide 2 text',
version: 'current',
sidebar: 'defaultSidebar',
frontMatter: {
id: 'guide2',
},
sidebarPosition: 2,
previous: {
permalink: '/docs/Guides/guide1',
title: 'Guide 1',
},
next: {
permalink: '/docs/Guides/guide2.5',
title: 'Guide 2.5',
},
});
expect(getDocById(version, 'Guides/guide2.5')).toEqual({
...defaultDocMetadata,
id: 'Guides/guide2.5',
unversionedId: 'Guides/guide2.5',
sourceDirName: 'Guides',
isDocsHomePage: false,
permalink: '/docs/Guides/guide2.5',
slug: '/Guides/guide2.5',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'Guides',
'0-guide2.5.md',
),
title: 'Guide 2.5',
description: 'Guide 2.5 text',
version: 'current',
sidebar: 'defaultSidebar',
frontMatter: {
id: 'guide2.5',
sidebar_position: 2.5,
},
sidebarPosition: 2.5,
previous: {
permalink: '/docs/Guides/guide2',
title: 'Guide 2',
},
next: {
permalink: '/docs/Guides/guide3',
title: 'Guide 3',
},
});
expect(getDocById(version, 'Guides/guide3')).toEqual({
...defaultDocMetadata,
id: 'Guides/guide3',
unversionedId: 'Guides/guide3',
sourceDirName: 'Guides',
isDocsHomePage: false,
permalink: '/docs/Guides/guide3',
slug: '/Guides/guide3',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'Guides',
'guide3.md',
),
title: 'Guide 3',
description: 'Guide 3 text',
version: 'current',
sidebar: 'defaultSidebar',
frontMatter: {
id: 'guide3',
sidebar_position: 3,
},
sidebarPosition: 3,
previous: {
permalink: '/docs/Guides/guide2.5',
title: 'Guide 2.5',
},
next: {
permalink: '/docs/Guides/guide4',
title: 'Guide 4',
},
});
expect(getDocById(version, 'Guides/guide4')).toEqual({
...defaultDocMetadata,
id: 'Guides/guide4',
unversionedId: 'Guides/guide4',
sourceDirName: 'Guides',
isDocsHomePage: false,
permalink: '/docs/Guides/guide4',
slug: '/Guides/guide4',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'Guides',
'a-guide4.md',
),
title: 'Guide 4',
description: 'Guide 4 text',
version: 'current',
sidebar: 'defaultSidebar',
frontMatter: {
id: 'guide4',
},
sidebarPosition: undefined,
previous: {
permalink: '/docs/Guides/guide3',
title: 'Guide 3',
},
next: {
permalink: '/docs/Guides/guide5',
title: 'Guide 5',
},
});
expect(getDocById(version, 'Guides/guide5')).toEqual({
...defaultDocMetadata,
id: 'Guides/guide5',
unversionedId: 'Guides/guide5',
sourceDirName: 'Guides',
isDocsHomePage: false,
permalink: '/docs/Guides/guide5',
slug: '/Guides/guide5',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'Guides',
'b-guide5.md',
),
title: 'Guide 5',
description: 'Guide 5 text',
version: 'current',
sidebar: 'defaultSidebar',
frontMatter: {
id: 'guide5',
},
sidebarPosition: undefined,
previous: {
permalink: '/docs/Guides/guide4',
title: 'Guide 4',
},
next: {
permalink: '/docs/API/api-overview',
title: 'API Overview',
},
});
expect(getDocById(version, 'API/api-overview')).toEqual({
...defaultDocMetadata,
id: 'API/api-overview',
unversionedId: 'API/api-overview',
sourceDirName: '3-API',
isDocsHomePage: false,
permalink: '/docs/API/api-overview',
slug: '/API/api-overview',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'3-API',
'00_api-overview.md',
),
title: 'API Overview',
description: 'API Overview text',
version: 'current',
sidebar: 'defaultSidebar',
frontMatter: {},
sidebarPosition: 0,
previous: {
permalink: '/docs/Guides/guide5',
title: 'Guide 5',
},
next: {
permalink: '/docs/API/Core APIs/Client API',
title: 'Client API',
},
});
expect(getDocById(version, 'API/Core APIs/Client API')).toEqual({
...defaultDocMetadata,
id: 'API/Core APIs/Client API',
unversionedId: 'API/Core APIs/Client API',
sourceDirName: '3-API/01_Core APIs',
isDocsHomePage: false,
permalink: '/docs/API/Core APIs/Client API',
slug: '/API/Core APIs/Client API',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'3-API',
'01_Core APIs',
'0 --- Client API.md',
),
title: 'Client API',
description: 'Client API text',
version: 'current',
sidebar: 'defaultSidebar',
frontMatter: {},
sidebarPosition: 0,
previous: {
permalink: '/docs/API/api-overview',
title: 'API Overview',
},
next: {
permalink: '/docs/API/Core APIs/Server API',
title: 'Server API',
},
});
expect(getDocById(version, 'API/Core APIs/Server API')).toEqual({
...defaultDocMetadata,
id: 'API/Core APIs/Server API',
unversionedId: 'API/Core APIs/Server API',
sourceDirName: '3-API/01_Core APIs',
isDocsHomePage: false,
permalink: '/docs/API/Core APIs/Server API',
slug: '/API/Core APIs/Server API',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'3-API',
'01_Core APIs',
'1 --- Server API.md',
),
title: 'Server API',
description: 'Server API text',
version: 'current',
sidebar: 'defaultSidebar',
frontMatter: {},
sidebarPosition: 1,
previous: {
permalink: '/docs/API/Core APIs/Client API',
title: 'Client API',
},
next: {
permalink: '/docs/API/Extension APIs/Plugin API',
title: 'Plugin API',
},
});
expect(getDocById(version, 'API/Extension APIs/Plugin API')).toEqual({
...defaultDocMetadata,
id: 'API/Extension APIs/Plugin API',
unversionedId: 'API/Extension APIs/Plugin API',
sourceDirName: '3-API/02_Extension APIs',
isDocsHomePage: false,
permalink: '/docs/API/Extension APIs/Plugin API',
slug: '/API/Extension APIs/Plugin API',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'3-API',
'02_Extension APIs',
'0. Plugin API.md',
),
title: 'Plugin API',
description: 'Plugin API text',
version: 'current',
sidebar: 'defaultSidebar',
frontMatter: {},
sidebarPosition: 0,
previous: {
permalink: '/docs/API/Core APIs/Server API',
title: 'Server API',
},
next: {
permalink: '/docs/API/Extension APIs/Theme API',
title: 'Theme API',
},
});
expect(getDocById(version, 'API/Extension APIs/Theme API')).toEqual({
...defaultDocMetadata,
id: 'API/Extension APIs/Theme API',
unversionedId: 'API/Extension APIs/Theme API',
sourceDirName: '3-API/02_Extension APIs',
isDocsHomePage: false,
permalink: '/docs/API/Extension APIs/Theme API',
slug: '/API/Extension APIs/Theme API',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'3-API',
'02_Extension APIs',
'1. Theme API.md',
),
title: 'Theme API',
description: 'Theme API text',
version: 'current',
sidebar: 'defaultSidebar',
frontMatter: {},
sidebarPosition: 1,
previous: {
permalink: '/docs/API/Extension APIs/Plugin API',
title: 'Plugin API',
},
next: {
permalink: '/docs/API/api-end',
title: 'API End',
},
});
expect(getDocById(version, 'API/api-end')).toEqual({
...defaultDocMetadata,
id: 'API/api-end',
unversionedId: 'API/api-end',
sourceDirName: '3-API',
isDocsHomePage: false,
permalink: '/docs/API/api-end',
slug: '/API/api-end',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'3-API',
'03_api-end.md',
),
title: 'API End',
description: 'API End text',
version: 'current',
sidebar: 'defaultSidebar',
frontMatter: {},
sidebarPosition: 3,
previous: {
permalink: '/docs/API/Extension APIs/Theme API',
title: 'Theme API',
},
next: undefined,
});
});
});
describe('site with partial autogenerated sidebars', () => {
async function loadSite() {
const siteDir = path.join(
__dirname,
'__fixtures__',
'site-with-autogenerated-sidebar',
);
const context = await loadContext(siteDir, {});
const plugin = pluginContentDocs(
context,
normalizePluginOptions(OptionsSchema, {
path: 'docs',
sidebarPath: path.join(
__dirname,
'__fixtures__',
'site-with-autogenerated-sidebar',
'partialAutogeneratedSidebars.js',
),
}),
);
const content = (await plugin.loadContent?.())!;
return {content, siteDir};
}
test('sidebar is partially autogenerated', async () => {
const {content} = await loadSite();
const version = content.loadedVersions[0];
expect(version.sidebars).toEqual({
someSidebar: [
{
type: 'doc',
id: 'API/api-end',
},
{
type: 'category',
label: 'Some category',
collapsed: true,
items: [
{
type: 'doc',
id: 'API/api-overview',
},
{
type: 'doc',
id: 'API/Extension APIs/Plugin API',
},
{
type: 'doc',
id: 'API/Extension APIs/Theme API',
},
],
},
],
});
});
test('docs in partially generated sidebar have correct metadatas', async () => {
const {content, siteDir} = await loadSite();
const version = content.loadedVersions[0];
// Only looking at the docs of the autogen sidebar, others metadatas should not be affected
expect(getDocById(version, 'API/api-end')).toEqual({
...defaultDocMetadata,
id: 'API/api-end',
unversionedId: 'API/api-end',
sourceDirName: '3-API',
isDocsHomePage: false,
permalink: '/docs/API/api-end',
slug: '/API/api-end',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'3-API',
'03_api-end.md',
),
title: 'API End',
description: 'API End text',
version: 'current',
sidebar: 'someSidebar',
frontMatter: {},
sidebarPosition: 3, // ignored (not part of the autogenerated sidebar slice)
previous: undefined,
next: {
permalink: '/docs/API/api-overview',
title: 'API Overview',
},
});
expect(getDocById(version, 'API/api-overview')).toEqual({
...defaultDocMetadata,
id: 'API/api-overview',
unversionedId: 'API/api-overview',
sourceDirName: '3-API',
isDocsHomePage: false,
permalink: '/docs/API/api-overview',
slug: '/API/api-overview',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'3-API',
'00_api-overview.md',
),
title: 'API Overview',
description: 'API Overview text',
version: 'current',
sidebar: 'someSidebar',
frontMatter: {},
sidebarPosition: 0, // ignored (not part of the autogenerated sidebar slice)
previous: {
permalink: '/docs/API/api-end',
title: 'API End',
},
next: {
permalink: '/docs/API/Extension APIs/Plugin API',
title: 'Plugin API',
},
});
expect(getDocById(version, 'API/Extension APIs/Plugin API')).toEqual({
...defaultDocMetadata,
id: 'API/Extension APIs/Plugin API',
unversionedId: 'API/Extension APIs/Plugin API',
sourceDirName: '3-API/02_Extension APIs',
isDocsHomePage: false,
permalink: '/docs/API/Extension APIs/Plugin API',
slug: '/API/Extension APIs/Plugin API',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'3-API',
'02_Extension APIs',
'0. Plugin API.md',
),
title: 'Plugin API',
description: 'Plugin API text',
version: 'current',
sidebar: 'someSidebar',
frontMatter: {},
sidebarPosition: 0,
previous: {
permalink: '/docs/API/api-overview',
title: 'API Overview',
},
next: {
permalink: '/docs/API/Extension APIs/Theme API',
title: 'Theme API',
},
});
expect(getDocById(version, 'API/Extension APIs/Theme API')).toEqual({
...defaultDocMetadata,
id: 'API/Extension APIs/Theme API',
unversionedId: 'API/Extension APIs/Theme API',
sourceDirName: '3-API/02_Extension APIs',
isDocsHomePage: false,
permalink: '/docs/API/Extension APIs/Theme API',
slug: '/API/Extension APIs/Theme API',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'3-API',
'02_Extension APIs',
'1. Theme API.md',
),
title: 'Theme API',
description: 'Theme API text',
version: 'current',
sidebar: 'someSidebar',
frontMatter: {},
sidebarPosition: 1,
previous: {
permalink: '/docs/API/Extension APIs/Plugin API',
title: 'Plugin API',
},
next: undefined,
});
});
});
describe('site with custom sidebar items generator', () => {
async function loadSite(sidebarItemsGenerator: SidebarItemsGenerator) {
const siteDir = path.join(
__dirname,
'__fixtures__',
'site-with-autogenerated-sidebar',
);
const context = await loadContext(siteDir);
const plugin = pluginContentDocs(
context,
normalizePluginOptions(OptionsSchema, {
path: 'docs',
sidebarItemsGenerator,
}),
);
const content = (await plugin.loadContent?.())!;
return {content, siteDir};
}
test('sidebar is autogenerated according to custom sidebarItemsGenerator', async () => {
const customSidebarItemsGenerator: SidebarItemsGenerator = async () => {
return [
{type: 'doc', id: 'API/api-overview'},
{type: 'doc', id: 'API/api-end'},
];
};
const customSidebarItemsGeneratorMock: SidebarItemsGenerator = jest.fn(
customSidebarItemsGenerator,
);
const {content} = await loadSite(customSidebarItemsGeneratorMock);
const version = content.loadedVersions[0];
expect(version.sidebars).toEqual({
defaultSidebar: [
{type: 'doc', id: 'API/api-overview'},
{type: 'doc', id: 'API/api-end'},
],
});
});
test('sidebarItemsGenerator is called with appropriate data', async () => {
type GeneratorArg = Parameters<SidebarItemsGenerator>[0];
const customSidebarItemsGeneratorMock = jest.fn(
async (_arg: GeneratorArg) => [],
);
const {siteDir} = await loadSite(customSidebarItemsGeneratorMock);
const generatorArg: GeneratorArg =
customSidebarItemsGeneratorMock.mock.calls[0][0];
// Make test pass even if docs are in different order and paths are absolutes
function makeDeterministic(arg: GeneratorArg): GeneratorArg {
return {
...arg,
docs: orderBy(arg.docs, 'id'),
version: {
...arg.version,
contentPath: path.relative(siteDir, arg.version.contentPath),
},
};
}
expect(makeDeterministic(generatorArg)).toMatchSnapshot();
});
});

View file

@ -0,0 +1,115 @@
/**
* 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 {
extractNumberPrefix,
stripNumberPrefix,
stripPathNumberPrefixes,
} from '../numberPrefix';
const BadNumberPrefixPatterns = [
'a1-My Doc',
'My Doc-000',
'00abc01-My Doc',
'My 001- Doc',
'My -001 Doc',
];
describe('stripNumberPrefix', () => {
test('should strip number prefix if present', () => {
expect(stripNumberPrefix('1-My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('01-My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('001-My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('001 - My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('001 - My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('999 - My Doc')).toEqual('My Doc');
//
expect(stripNumberPrefix('1---My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('01---My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('001---My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('001 --- My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('001 --- My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('999 --- My Doc')).toEqual('My Doc');
//
expect(stripNumberPrefix('1___My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('01___My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('001___My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('001 ___ My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('001 ___ My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('999 ___ My Doc')).toEqual('My Doc');
//
expect(stripNumberPrefix('1.My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('01.My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('001.My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('001 . My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('001 . My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('999 . My Doc')).toEqual('My Doc');
});
test('should not strip number prefix if pattern does not match', () => {
BadNumberPrefixPatterns.forEach((badPattern) => {
expect(stripNumberPrefix(badPattern)).toEqual(badPattern);
});
});
});
describe('stripPathNumberPrefix', () => {
test('should strip number prefixes in paths', () => {
expect(
stripPathNumberPrefixes(
'0-MyRootFolder0/1 - MySubFolder1/2. MyDeepFolder2/3 _MyDoc3',
),
).toEqual('MyRootFolder0/MySubFolder1/MyDeepFolder2/MyDoc3');
});
});
describe('extractNumberPrefix', () => {
test('should extract number prefix if present', () => {
expect(extractNumberPrefix('0-My Doc')).toEqual({
filename: 'My Doc',
numberPrefix: 0,
});
expect(extractNumberPrefix('1-My Doc')).toEqual({
filename: 'My Doc',
numberPrefix: 1,
});
expect(extractNumberPrefix('01-My Doc')).toEqual({
filename: 'My Doc',
numberPrefix: 1,
});
expect(extractNumberPrefix('001-My Doc')).toEqual({
filename: 'My Doc',
numberPrefix: 1,
});
expect(extractNumberPrefix('001 - My Doc')).toEqual({
filename: 'My Doc',
numberPrefix: 1,
});
expect(extractNumberPrefix('001 - My Doc')).toEqual({
filename: 'My Doc',
numberPrefix: 1,
});
expect(extractNumberPrefix('999 - My Doc')).toEqual({
filename: 'My Doc',
numberPrefix: 999,
});
expect(extractNumberPrefix('0046036 - My Doc')).toEqual({
filename: 'My Doc',
numberPrefix: 46036,
});
});
test('should not extract number prefix if pattern does not match', () => {
BadNumberPrefixPatterns.forEach((badPattern) => {
expect(extractNumberPrefix(badPattern)).toEqual({
filename: badPattern,
numberPrefix: undefined,
});
});
});
});

View file

@ -7,6 +7,7 @@
import {OptionsSchema, DEFAULT_OPTIONS} from '../options';
import {normalizePluginOptions} from '@docusaurus/utils-validation';
import {DefaultSidebarItemsGenerator} from '../sidebarItemsGenerator';
// the type of remark/rehype plugins is function
const markdownPluginsFunctionStub = () => {};
@ -26,6 +27,7 @@ describe('normalizeDocsPluginOptions', () => {
homePageId: 'home', // Document id for docs home page.
include: ['**/*.{md,mdx}'], // Extensions to include.
sidebarPath: 'my-sidebar', // Path to sidebar configuration for showing a list of markdown pages.
sidebarItemsGenerator: DefaultSidebarItemsGenerator,
docLayoutComponent: '@theme/DocPage',
docItemComponent: '@theme/DocItem',
remarkPlugins: [markdownPluginsObjectStub],

View file

@ -0,0 +1,268 @@
/**
* 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 {
CategoryMetadatasFile,
DefaultSidebarItemsGenerator,
} from '../sidebarItemsGenerator';
import {DefaultCategoryCollapsedValue} from '../sidebars';
import {Sidebar, SidebarItemsGenerator} from '../types';
import fs from 'fs-extra';
describe('DefaultSidebarItemsGenerator', () => {
function testDefaultSidebarItemsGenerator(
options: Partial<Parameters<SidebarItemsGenerator>[0]>,
) {
return DefaultSidebarItemsGenerator({
item: {
type: 'autogenerated',
dirName: '.',
},
version: {
versionName: 'current',
contentPath: 'docs',
},
docs: [],
...options,
});
}
function mockCategoryMetadataFiles(
categoryMetadataFiles: Record<string, Partial<CategoryMetadatasFile>>,
) {
jest.spyOn(fs, 'pathExists').mockImplementation((metadataFilePath) => {
return typeof categoryMetadataFiles[metadataFilePath] !== 'undefined';
});
jest.spyOn(fs, 'readFile').mockImplementation(
// @ts-expect-error: annoying TS error due to overrides
async (metadataFilePath: string) => {
return JSON.stringify(categoryMetadataFiles[metadataFilePath]);
},
);
}
test('generates empty sidebar slice when no docs and emit a warning', async () => {
const consoleWarn = jest.spyOn(console, 'warn');
const sidebarSlice = await testDefaultSidebarItemsGenerator({
docs: [],
});
expect(sidebarSlice).toEqual([]);
expect(consoleWarn).toHaveBeenCalledWith(
expect.stringMatching(
/No docs found in dir .: can't auto-generate a sidebar/,
),
);
});
test('generates simple flat sidebar', async () => {
const sidebarSlice = await DefaultSidebarItemsGenerator({
item: {
type: 'autogenerated',
dirName: '.',
},
version: {
versionName: 'current',
contentPath: '',
},
docs: [
{
id: 'doc1',
source: 'doc1.md',
sourceDirName: '.',
sidebarPosition: 2,
frontMatter: {
sidebar_label: 'doc1 sidebar label',
},
},
{
id: 'doc2',
source: 'doc2.md',
sourceDirName: '.',
sidebarPosition: 3,
frontMatter: {},
},
{
id: 'doc3',
source: 'doc3.md',
sourceDirName: '.',
sidebarPosition: 1,
frontMatter: {},
},
{
id: 'doc4',
source: 'doc4.md',
sourceDirName: '.',
sidebarPosition: 1.5,
frontMatter: {},
},
{
id: 'doc5',
source: 'doc5.md',
sourceDirName: '.',
sidebarPosition: undefined,
frontMatter: {},
},
],
});
expect(sidebarSlice).toEqual([
{type: 'doc', id: 'doc3'},
{type: 'doc', id: 'doc4'},
{type: 'doc', id: 'doc1', label: 'doc1 sidebar label'},
{type: 'doc', id: 'doc2'},
{type: 'doc', id: 'doc5'},
] as Sidebar);
});
test('generates complex nested sidebar', async () => {
mockCategoryMetadataFiles({
'02-Guides/_category_.json': {collapsed: false},
'02-Guides/01-SubGuides/_category_.yml': {
label: 'SubGuides (metadata file label)',
},
});
const sidebarSlice = await DefaultSidebarItemsGenerator({
item: {
type: 'autogenerated',
dirName: '.',
},
version: {
versionName: 'current',
contentPath: '',
},
docs: [
{
id: 'intro',
source: 'intro.md',
sourceDirName: '.',
sidebarPosition: 1,
frontMatter: {},
},
{
id: 'tutorial2',
source: 'tutorial2.md',
sourceDirName: '01-Tutorials',
sidebarPosition: 2,
frontMatter: {},
},
{
id: 'tutorial1',
source: 'tutorial1.md',
sourceDirName: '01-Tutorials',
sidebarPosition: 1,
frontMatter: {},
},
{
id: 'guide2',
source: 'guide2.md',
sourceDirName: '02-Guides',
sidebarPosition: 2,
frontMatter: {},
},
{
id: 'guide1',
source: 'guide1.md',
sourceDirName: '02-Guides',
sidebarPosition: 1,
frontMatter: {},
},
{
id: 'nested-guide',
source: 'nested-guide.md',
sourceDirName: '02-Guides/01-SubGuides',
sidebarPosition: undefined,
frontMatter: {},
},
{
id: 'end',
source: 'end.md',
sourceDirName: '.',
sidebarPosition: 3,
frontMatter: {},
},
],
});
expect(sidebarSlice).toEqual([
{type: 'doc', id: 'intro'},
{
type: 'category',
label: 'Tutorials',
collapsed: DefaultCategoryCollapsedValue,
items: [
{type: 'doc', id: 'tutorial1'},
{type: 'doc', id: 'tutorial2'},
],
},
{
type: 'category',
label: 'Guides',
collapsed: false,
items: [
{type: 'doc', id: 'guide1'},
{
type: 'category',
label: 'SubGuides (metadata file label)',
collapsed: DefaultCategoryCollapsedValue,
items: [{type: 'doc', id: 'nested-guide'}],
},
{type: 'doc', id: 'guide2'},
],
},
{type: 'doc', id: 'end'},
] as Sidebar);
});
test('generates subfolder sidebar', async () => {
const sidebarSlice = await DefaultSidebarItemsGenerator({
item: {
type: 'autogenerated',
dirName: 'subfolder/subsubfolder',
},
version: {
versionName: 'current',
contentPath: '',
},
docs: [
{
id: 'doc1',
source: 'doc1.md',
sourceDirName: 'subfolder/subsubfolder',
sidebarPosition: undefined,
frontMatter: {},
},
{
id: 'doc2',
source: 'doc2.md',
sourceDirName: 'subfolder',
sidebarPosition: undefined,
frontMatter: {},
},
{
id: 'doc3',
source: 'doc3.md',
sourceDirName: '.',
sidebarPosition: undefined,
frontMatter: {},
},
{
id: 'doc4',
source: 'doc4.md',
sourceDirName: 'subfolder/subsubfolder',
sidebarPosition: undefined,
frontMatter: {},
},
],
});
expect(sidebarSlice).toEqual([
{type: 'doc', id: 'doc1'},
{type: 'doc', id: 'doc4'},
] as Sidebar);
});
});

View file

@ -14,8 +14,16 @@ import {
collectSidebarCategories,
collectSidebarLinks,
transformSidebarItems,
DefaultSidebars,
processSidebars,
} from '../sidebars';
import {Sidebar, Sidebars} from '../types';
import {
Sidebar,
SidebarItem,
SidebarItemsGenerator,
Sidebars,
UnprocessedSidebars,
} from '../types';
/* eslint-disable global-require, import/no-dynamic-require */
@ -124,7 +132,7 @@ describe('loadSidebars', () => {
);
*/
// See https://github.com/facebook/docusaurus/issues/3366
expect(loadSidebars('badpath')).toEqual({});
expect(loadSidebars('badpath')).toEqual(DefaultSidebars);
});
test('undefined path', () => {
@ -443,6 +451,131 @@ describe('transformSidebarItems', () => {
});
});
describe('processSidebars', () => {
const StaticGeneratedSidebarSlice: SidebarItem[] = [
{type: 'doc', id: 'doc-generated-id-1'},
{type: 'doc', id: 'doc-generated-id-2'},
];
const StaticSidebarItemsGenerator: SidebarItemsGenerator = jest.fn(
async () => {
return StaticGeneratedSidebarSlice;
},
);
async function testProcessSidebars(unprocessedSidebars: UnprocessedSidebars) {
return processSidebars({
sidebarItemsGenerator: StaticSidebarItemsGenerator,
unprocessedSidebars,
docs: [],
// @ts-expect-error: useless for this test
version: {},
});
}
test('let sidebars without autogenerated items untouched', async () => {
const unprocessedSidebars: UnprocessedSidebars = {
someSidebar: [
{type: 'doc', id: 'doc1'},
{
type: 'category',
collapsed: false,
items: [{type: 'doc', id: 'doc2'}],
label: 'Category',
},
{type: 'link', href: 'https://facebook.com', label: 'FB'},
],
secondSidebar: [
{type: 'doc', id: 'doc3'},
{type: 'link', href: 'https://instagram.com', label: 'IG'},
{
type: 'category',
collapsed: false,
items: [{type: 'doc', id: 'doc4'}],
label: 'Category',
},
],
};
const processedSidebar = await testProcessSidebars(unprocessedSidebars);
expect(processedSidebar).toEqual(unprocessedSidebars);
});
test('replace autogenerated items by generated sidebars slices', async () => {
const unprocessedSidebars: UnprocessedSidebars = {
someSidebar: [
{type: 'doc', id: 'doc1'},
{
type: 'category',
collapsed: false,
items: [
{type: 'doc', id: 'doc2'},
{type: 'autogenerated', dirName: 'dir1'},
],
label: 'Category',
},
{type: 'link', href: 'https://facebook.com', label: 'FB'},
],
secondSidebar: [
{type: 'doc', id: 'doc3'},
{type: 'autogenerated', dirName: 'dir2'},
{type: 'link', href: 'https://instagram.com', label: 'IG'},
{type: 'autogenerated', dirName: 'dir3'},
{
type: 'category',
collapsed: false,
items: [{type: 'doc', id: 'doc4'}],
label: 'Category',
},
],
};
const processedSidebar = await testProcessSidebars(unprocessedSidebars);
expect(StaticSidebarItemsGenerator).toHaveBeenCalledTimes(3);
expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({
item: {type: 'autogenerated', dirName: 'dir1'},
docs: [],
version: {},
});
expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({
item: {type: 'autogenerated', dirName: 'dir2'},
docs: [],
version: {},
});
expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({
item: {type: 'autogenerated', dirName: 'dir3'},
docs: [],
version: {},
});
expect(processedSidebar).toEqual({
someSidebar: [
{type: 'doc', id: 'doc1'},
{
type: 'category',
collapsed: false,
items: [{type: 'doc', id: 'doc2'}, ...StaticGeneratedSidebarSlice],
label: 'Category',
},
{type: 'link', href: 'https://facebook.com', label: 'FB'},
],
secondSidebar: [
{type: 'doc', id: 'doc3'},
...StaticGeneratedSidebarSlice,
{type: 'link', href: 'https://instagram.com', label: 'IG'},
...StaticGeneratedSidebarSlice,
{
type: 'category',
collapsed: false,
items: [{type: 'doc', id: 'doc4'}],
label: 'Category',
},
],
} as Sidebars);
});
});
describe('createSidebarsUtils', () => {
const sidebar1: Sidebar = [
{

View file

@ -15,6 +15,23 @@ describe('getSlug', () => {
);
});
test('can strip dir number prefixes', () => {
expect(
getSlug({
baseID: 'doc',
dirName: '/001-dir1/002-dir2',
stripDirNumberPrefixes: true,
}),
).toEqual('/dir1/dir2/doc');
expect(
getSlug({
baseID: 'doc',
dirName: '/001-dir1/002-dir2',
stripDirNumberPrefixes: false,
}),
).toEqual('/001-dir1/002-dir2/doc');
});
// See https://github.com/facebook/docusaurus/issues/3223
test('should handle special chars in doc path', () => {
expect(

View file

@ -12,7 +12,11 @@ import {
} from './versions';
import fs from 'fs-extra';
import path from 'path';
import {Sidebars, PathOptions, SidebarItem} from './types';
import {
PathOptions,
UnprocessedSidebarItem,
UnprocessedSidebars,
} from './types';
import {loadSidebars} from './sidebars';
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
@ -90,10 +94,14 @@ export function cliDocsVersionCommand(
// Load current sidebar and create a new versioned sidebars file.
if (fs.existsSync(sidebarPath)) {
const loadedSidebars: Sidebars = loadSidebars(sidebarPath);
const loadedSidebars = loadSidebars(sidebarPath);
// TODO @slorber: this "version prefix" in versioned sidebars looks like a bad idea to me
// TODO try to get rid of it
// Transform id in original sidebar to versioned id.
const normalizeItem = (item: SidebarItem): SidebarItem => {
const normalizeItem = (
item: UnprocessedSidebarItem,
): UnprocessedSidebarItem => {
switch (item.type) {
case 'category':
return {...item, items: item.items.map(normalizeItem)};
@ -108,14 +116,13 @@ export function cliDocsVersionCommand(
}
};
const versionedSidebar: Sidebars = Object.entries(loadedSidebars).reduce(
(acc: Sidebars, [sidebarId, sidebarItems]) => {
const newVersionedSidebarId = `version-${version}/${sidebarId}`;
acc[newVersionedSidebarId] = sidebarItems.map(normalizeItem);
return acc;
},
{},
);
const versionedSidebar: UnprocessedSidebars = Object.entries(
loadedSidebars,
).reduce((acc: UnprocessedSidebars, [sidebarId, sidebarItems]) => {
const newVersionedSidebarId = `version-${version}/${sidebarId}`;
acc[newVersionedSidebarId] = sidebarItems.map(normalizeItem);
return acc;
}, {});
const versionedSidebarsDir = getVersionedSidebarsDirPath(siteDir, pluginId);
const newSidebarFile = path.join(

View file

@ -14,7 +14,9 @@ type DocFrontMatter = {
description?: string;
slug?: string;
sidebar_label?: string;
sidebar_position?: number;
custom_edit_url?: string;
strip_number_prefixes?: boolean;
};
const DocFrontMatterSchema = Joi.object<DocFrontMatter>({
@ -23,7 +25,9 @@ const DocFrontMatterSchema = Joi.object<DocFrontMatter>({
description: Joi.string(),
slug: Joi.string(),
sidebar_label: Joi.string(),
sidebar_position: Joi.number(),
custom_edit_url: Joi.string().allow(null),
strip_number_prefixes: Joi.boolean(),
}).unknown();
export function assertDocFrontMatter(

View file

@ -30,6 +30,7 @@ import getSlug from './slug';
import {CURRENT_VERSION_NAME} from './constants';
import globby from 'globby';
import {getDocsDirPaths} from './versions';
import {extractNumberPrefix, stripPathNumberPrefixes} from './numberPrefix';
import {assertDocFrontMatter} from './docFrontMatter';
type LastUpdateOptions = Pick<
@ -121,37 +122,66 @@ export function processDocMetadata({
});
assertDocFrontMatter(frontMatter);
// ex: api/myDoc -> api
// ex: myDoc -> .
const docsFileDirName = path.dirname(source);
const {
sidebar_label: sidebarLabel,
custom_edit_url: customEditURL,
// Strip number prefixes by default (01-MyFolder/01-MyDoc.md => MyFolder/MyDoc) by default,
// but ability to disable this behavior with frontmatterr
strip_number_prefixes: stripNumberPrefixes = true,
} = frontMatter;
const baseID: string =
frontMatter.id || path.basename(source, path.extname(source));
// ex: api/plugins/myDoc -> myDoc
// ex: myDoc -> myDoc
const sourceFileNameWithoutExtension = path.basename(
source,
path.extname(source),
);
// ex: api/plugins/myDoc -> api/plugins
// ex: myDoc -> .
const sourceDirName = path.dirname(source);
const {filename: unprefixedFileName, numberPrefix} = stripNumberPrefixes
? extractNumberPrefix(sourceFileNameWithoutExtension)
: {filename: sourceFileNameWithoutExtension, numberPrefix: undefined};
const baseID: string = frontMatter.id ?? unprefixedFileName;
if (baseID.includes('/')) {
throw new Error(`Document id [${baseID}] cannot include "/".`);
}
// For autogenerated sidebars, sidebar position can come from filename number prefix or frontmatter
const sidebarPosition: number | undefined =
frontMatter.sidebar_position ?? numberPrefix;
// TODO legacy retrocompatibility
// The same doc in 2 distinct version could keep the same id,
// we just need to namespace the data by version
const versionIdPart =
const versionIdPrefix =
versionMetadata.versionName === CURRENT_VERSION_NAME
? ''
: `version-${versionMetadata.versionName}/`;
? undefined
: `version-${versionMetadata.versionName}`;
// TODO legacy retrocompatibility
// I think it's bad to affect the frontmatter id with the dirname
const dirNameIdPart = docsFileDirName === '.' ? '' : `${docsFileDirName}/`;
// I think it's bad to affect the frontmatter id with the dirname?
function computeDirNameIdPrefix() {
if (sourceDirName === '.') {
return undefined;
}
// Eventually remove the number prefixes from intermediate directories
return stripNumberPrefixes
? stripPathNumberPrefixes(sourceDirName)
: sourceDirName;
}
// TODO legacy composite id, requires a breaking change to modify this
const id = `${versionIdPart}${dirNameIdPart}${baseID}`;
const unversionedId = [computeDirNameIdPrefix(), baseID]
.filter(Boolean)
.join('/');
const unversionedId = `${dirNameIdPart}${baseID}`;
// TODO is versioning the id very useful in practice?
// legacy versioned id, requires a breaking change to modify this
const id = [versionIdPrefix, unversionedId].filter(Boolean).join('/');
// TODO remove soon, deprecated homePageId
const isDocsHomePage = unversionedId === (homePageId ?? '_index');
@ -165,8 +195,9 @@ export function processDocMetadata({
? '/'
: getSlug({
baseID,
dirName: docsFileDirName,
dirName: sourceDirName,
frontmatterSlug: frontMatter.slug,
stripDirNumberPrefixes: stripNumberPrefixes,
});
// Default title is the id.
@ -212,6 +243,7 @@ export function processDocMetadata({
title,
description,
source: aliasedSitePath(filePath, siteDir),
sourceDirName,
slug: docSlug,
permalink,
editUrl: customEditURL !== undefined ? customEditURL : getDocEditUrl(),
@ -224,6 +256,7 @@ export function processDocMetadata({
)
: undefined,
sidebar_label: sidebarLabel,
sidebarPosition,
frontMatter,
};
}

View file

@ -20,8 +20,7 @@ import {
addTrailingPathSeparator,
} from '@docusaurus/utils';
import {LoadContext, Plugin, RouteConfig} from '@docusaurus/types';
import {loadSidebars, createSidebarsUtils} from './sidebars';
import {loadSidebars, createSidebarsUtils, processSidebars} from './sidebars';
import {readVersionDocs, processDocMetadata} from './docs';
import {getDocsDirPaths, readVersionsMetadata} from './versions';
@ -49,6 +48,7 @@ import {
translateLoadedContent,
getLoadedContentTranslationFiles,
} from './translations';
import {CategoryMetadataFilenamePattern} from './sidebarItemsGenerator';
export default function pluginContentDocs(
context: LoadContext,
@ -127,6 +127,7 @@ export default function pluginContentDocs(
),
),
),
`${version.contentPath}/**/${CategoryMetadataFilenamePattern}`,
];
}
@ -162,8 +163,9 @@ export default function pluginContentDocs(
async function loadVersion(
versionMetadata: VersionMetadata,
): Promise<LoadedVersion> {
const sidebars = loadSidebars(versionMetadata.sidebarFilePath);
const sidebarsUtils = createSidebarsUtils(sidebars);
const unprocessedSidebars = loadSidebars(
versionMetadata.sidebarFilePath,
);
const docsBase: DocMetadataBase[] = await loadVersionDocsBase(
versionMetadata,
@ -173,6 +175,15 @@ export default function pluginContentDocs(
(doc) => doc.id,
);
const sidebars = await processSidebars({
sidebarItemsGenerator: options.sidebarItemsGenerator,
unprocessedSidebars,
docs: docsBase,
version: versionMetadata,
});
const sidebarsUtils = createSidebarsUtils(sidebars);
const validDocIds = Object.keys(docsBaseById);
sidebarsUtils.checkSidebarsDocIds(validDocIds);

View file

@ -0,0 +1,35 @@
/**
* 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.
*/
const NumberPrefixRegex = /^(?<numberPrefix>\d+)(?<separator>\s*[-_.]+\s*)(?<suffix>.*)$/;
// 0-myDoc => myDoc
export function stripNumberPrefix(str: string) {
return NumberPrefixRegex.exec(str)?.groups?.suffix ?? str;
}
// 0-myFolder/0-mySubfolder/0-myDoc => myFolder/mySubfolder/myDoc
export function stripPathNumberPrefixes(path: string) {
return path.split('/').map(stripNumberPrefix).join('/');
}
// 0-myDoc => {filename: myDoc, numberPrefix: 0}
// 003 - myDoc => {filename: myDoc, numberPrefix: 3}
export function extractNumberPrefix(
filename: string,
): {filename: string; numberPrefix?: number} {
const match = NumberPrefixRegex.exec(filename);
const cleanFileName = match?.groups?.suffix ?? filename;
const numberPrefixString = match?.groups?.numberPrefix;
const numberPrefix = numberPrefixString
? parseInt(numberPrefixString, 10)
: undefined;
return {
filename: cleanFileName,
numberPrefix,
};
}

View file

@ -15,13 +15,15 @@ import {
import {OptionValidationContext, ValidationResult} from '@docusaurus/types';
import chalk from 'chalk';
import admonitions from 'remark-admonitions';
import {DefaultSidebarItemsGenerator} from './sidebarItemsGenerator';
export const DEFAULT_OPTIONS: Omit<PluginOptions, 'id'> = {
path: 'docs', // Path to data on filesystem, relative to site dir.
routeBasePath: 'docs', // URL Route.
homePageId: undefined, // TODO remove soon, deprecated
include: ['**/*.{md,mdx}'], // Extensions to include.
sidebarPath: 'sidebars.json', // Path to sidebar configuration for showing a list of markdown pages.
sidebarPath: 'sidebars.json', // Path to the sidebars configuration file
sidebarItemsGenerator: DefaultSidebarItemsGenerator,
docLayoutComponent: '@theme/DocPage',
docItemComponent: '@theme/DocItem',
remarkPlugins: [],
@ -61,6 +63,9 @@ export const OptionsSchema = Joi.object({
homePageId: Joi.string().optional(),
include: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.include),
sidebarPath: Joi.string().allow('').default(DEFAULT_OPTIONS.sidebarPath),
sidebarItemsGenerator: Joi.function().default(
() => DEFAULT_OPTIONS.sidebarItemsGenerator,
),
docLayoutComponent: Joi.string().default(DEFAULT_OPTIONS.docLayoutComponent),
docItemComponent: Joi.string().default(DEFAULT_OPTIONS.docItemComponent),
remarkPlugins: RemarkPluginsSchema.default(DEFAULT_OPTIONS.remarkPlugins),

View file

@ -0,0 +1,305 @@
/**
* 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 {
SidebarItem,
SidebarItemDoc,
SidebarItemCategory,
SidebarItemsGenerator,
SidebarItemsGeneratorDoc,
} from './types';
import {sortBy, take, last, orderBy} from 'lodash';
import {addTrailingSlash, posixPath} from '@docusaurus/utils';
import {Joi} from '@docusaurus/utils-validation';
import {extractNumberPrefix} from './numberPrefix';
import chalk from 'chalk';
import path from 'path';
import fs from 'fs-extra';
import Yaml from 'js-yaml';
import {DefaultCategoryCollapsedValue} from './sidebars';
const BreadcrumbSeparator = '/';
export const CategoryMetadataFilenameBase = '_category_';
export const CategoryMetadataFilenamePattern = '_category_.{json,yml,yaml}';
export type CategoryMetadatasFile = {
label?: string;
position?: number;
collapsed?: boolean;
// TODO should we allow "items" here? how would this work? would an "autogenerated" type be allowed?
// This mkdocs plugin do something like that: https://github.com/lukasgeiter/mkdocs-awesome-pages-plugin/
// cf comment: https://github.com/facebook/docusaurus/issues/3464#issuecomment-784765199
};
type WithPosition = {position?: number};
type SidebarItemWithPosition = SidebarItem & WithPosition;
const CategoryMetadatasFileSchema = Joi.object<CategoryMetadatasFile>({
label: Joi.string().optional(),
position: Joi.number().optional(),
collapsed: Joi.boolean().optional(),
});
// TODO later if there is `CategoryFolder/index.md`, we may want to read the metadata as yaml on it
// see https://github.com/facebook/docusaurus/issues/3464#issuecomment-818670449
async function readCategoryMetadatasFile(
categoryDirPath: string,
): Promise<CategoryMetadatasFile | null> {
function assertCategoryMetadataFile(
content: unknown,
): asserts content is CategoryMetadatasFile {
Joi.attempt(content, CategoryMetadatasFileSchema);
}
async function tryReadFile(
fileNameWithExtension: string,
parse: (content: string) => unknown,
): Promise<CategoryMetadatasFile | null> {
// Simpler to use only posix paths for mocking file metadatas in tests
const filePath = posixPath(
path.join(categoryDirPath, fileNameWithExtension),
);
if (await fs.pathExists(filePath)) {
const contentString = await fs.readFile(filePath, {encoding: 'utf8'});
const unsafeContent: unknown = parse(contentString);
try {
assertCategoryMetadataFile(unsafeContent);
return unsafeContent;
} catch (e) {
console.error(
chalk.red(
`The docs sidebar category metadata file looks invalid!\nPath=${filePath}`,
),
);
throw e;
}
}
return null;
}
return (
(await tryReadFile(`${CategoryMetadataFilenameBase}.json`, JSON.parse)) ??
(await tryReadFile(`${CategoryMetadataFilenameBase}.yml`, Yaml.load)) ??
// eslint-disable-next-line no-return-await
(await tryReadFile(`${CategoryMetadataFilenameBase}.yaml`, Yaml.load))
);
}
// [...parents, tail]
function parseBreadcrumb(
breadcrumb: string[],
): {parents: string[]; tail: string} {
return {
parents: take(breadcrumb, breadcrumb.length - 1),
tail: last(breadcrumb)!,
};
}
// Comment for this feature: https://github.com/facebook/docusaurus/issues/3464#issuecomment-818670449
export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async function defaultSidebarItemsGenerator({
item,
docs: allDocs,
version,
}): Promise<SidebarItem[]> {
// Doc at the root of the autogenerated sidebar dir
function isRootDoc(doc: SidebarItemsGeneratorDoc) {
return doc.sourceDirName === item.dirName;
}
// Doc inside a subfolder of the autogenerated sidebar dir
function isCategoryDoc(doc: SidebarItemsGeneratorDoc) {
if (isRootDoc(doc)) {
return false;
}
return (
// autogen dir is . and doc is in subfolder
item.dirName === '.' ||
// autogen dir is not . and doc is in subfolder
// "api/myDoc" startsWith "api/" (note "api2/myDoc" is not included)
doc.sourceDirName.startsWith(addTrailingSlash(item.dirName))
);
}
function isInAutogeneratedDir(doc: SidebarItemsGeneratorDoc) {
return isRootDoc(doc) || isCategoryDoc(doc);
}
// autogenDir=a/b and docDir=a/b/c/d => returns c/d
// autogenDir=a/b and docDir=a/b => returns .
function getDocDirRelativeToAutogenDir(
doc: SidebarItemsGeneratorDoc,
): string {
if (!isInAutogeneratedDir(doc)) {
throw new Error(
'getDocDirRelativeToAutogenDir() can only be called for subdocs of the sidebar autogen dir',
);
}
// Is there a node API to compare 2 relative paths more easily?
// path.relative() does not give good results
if (item.dirName === '.') {
return doc.sourceDirName;
} else if (item.dirName === doc.sourceDirName) {
return '.';
} else {
return doc.sourceDirName.replace(addTrailingSlash(item.dirName), '');
}
}
// Get only docs in the autogen dir
// Sort by folder+filename at once
const docs = sortBy(allDocs.filter(isInAutogeneratedDir), (d) => d.source);
if (docs.length === 0) {
console.warn(
chalk.yellow(
`No docs found in dir ${item.dirName}: can't auto-generate a sidebar`,
),
);
}
function createDocSidebarItem(
doc: SidebarItemsGeneratorDoc,
): SidebarItemDoc & WithPosition {
return {
type: 'doc',
id: doc.id,
...(doc.frontMatter.sidebar_label && {
label: doc.frontMatter.sidebar_label,
}),
...(typeof doc.sidebarPosition !== 'undefined' && {
position: doc.sidebarPosition,
}),
};
}
async function createCategorySidebarItem({
breadcrumb,
}: {
breadcrumb: string[];
}): Promise<SidebarItemCategory & WithPosition> {
const categoryDirPath = path.join(
version.contentPath,
breadcrumb.join(BreadcrumbSeparator),
);
const categoryMetadatas = await readCategoryMetadatasFile(categoryDirPath);
const {tail} = parseBreadcrumb(breadcrumb);
const {filename, numberPrefix} = extractNumberPrefix(tail);
const position = categoryMetadatas?.position ?? numberPrefix;
return {
type: 'category',
label: categoryMetadatas?.label ?? filename,
items: [],
collapsed: categoryMetadatas?.collapsed ?? DefaultCategoryCollapsedValue,
...(typeof position !== 'undefined' && {position}),
};
}
// Not sure how to simplify this algorithm :/
async function autogenerateSidebarItems(): Promise<
SidebarItemWithPosition[]
> {
const sidebarItems: SidebarItem[] = []; // mutable result
const categoriesByBreadcrumb: Record<string, SidebarItemCategory> = {}; // mutable cache of categories already created
async function getOrCreateCategoriesForBreadcrumb(
breadcrumb: string[],
): Promise<SidebarItemCategory | null> {
if (breadcrumb.length === 0) {
return null;
}
const {parents} = parseBreadcrumb(breadcrumb);
const parentCategory = await getOrCreateCategoriesForBreadcrumb(parents);
const existingCategory =
categoriesByBreadcrumb[breadcrumb.join(BreadcrumbSeparator)];
if (existingCategory) {
return existingCategory;
} else {
const newCategory = await createCategorySidebarItem({
breadcrumb,
});
if (parentCategory) {
parentCategory.items.push(newCategory);
} else {
sidebarItems.push(newCategory);
}
categoriesByBreadcrumb[
breadcrumb.join(BreadcrumbSeparator)
] = newCategory;
return newCategory;
}
}
// Get the category breadcrumb of a doc (relative to the dir of the autogenerated sidebar item)
function getRelativeBreadcrumb(doc: SidebarItemsGeneratorDoc): string[] {
const relativeDirPath = getDocDirRelativeToAutogenDir(doc);
if (relativeDirPath === '.') {
return [];
} else {
return relativeDirPath.split(BreadcrumbSeparator);
}
}
async function handleDocItem(doc: SidebarItemsGeneratorDoc): Promise<void> {
const breadcrumb = getRelativeBreadcrumb(doc);
const category = await getOrCreateCategoriesForBreadcrumb(breadcrumb);
const docSidebarItem = createDocSidebarItem(doc);
if (category) {
category.items.push(docSidebarItem);
} else {
sidebarItems.push(docSidebarItem);
}
}
// async process made sequential on purpose! order matters
for (const doc of docs) {
// eslint-disable-next-line no-await-in-loop
await handleDocItem(doc);
}
return sidebarItems;
}
const sidebarItems = await autogenerateSidebarItems();
return sortSidebarItems(sidebarItems);
};
// Recursively sort the categories/docs + remove the "position" attribute from final output
// Note: the "position" is only used to sort "inside" a sidebar slice
// It is not used to sort across multiple consecutive sidebar slices (ie a whole Category composed of multiple autogenerated items)
function sortSidebarItems(
sidebarItems: SidebarItemWithPosition[],
): SidebarItem[] {
const processedSidebarItems = sidebarItems.map((item) => {
if (item.type === 'category') {
return {
...item,
items: sortSidebarItems(item.items),
};
}
return item;
});
const sortedSidebarItems = orderBy(
processedSidebarItems,
(item) => item.position,
['asc'],
);
return sortedSidebarItems.map(({position: _removed, ...item}) => item);
}

View file

@ -16,9 +16,18 @@ import {
Sidebar,
SidebarItemCategory,
SidebarItemType,
UnprocessedSidebarItem,
UnprocessedSidebars,
UnprocessedSidebar,
DocMetadataBase,
VersionMetadata,
SidebarItemsGenerator,
SidebarItemsGeneratorDoc,
SidebarItemsGeneratorVersion,
} from './types';
import {mapValues, flatten, flatMap, difference} from 'lodash';
import {mapValues, flatten, flatMap, difference, pick, memoize} from 'lodash';
import {getElementsAround} from '@docusaurus/utils';
import combinePromises from 'combine-promises';
type SidebarItemCategoryJSON = SidebarItemBase & {
type: 'category';
@ -27,12 +36,18 @@ type SidebarItemCategoryJSON = SidebarItemBase & {
collapsed?: boolean;
};
type SidebarItemAutogeneratedJSON = SidebarItemBase & {
type: 'autogenerated';
dirName: string;
};
type SidebarItemJSON =
| string
| SidebarCategoryShorthandJSON
| SidebarItemDoc
| SidebarItemLink
| SidebarItemCategoryJSON
| SidebarItemAutogeneratedJSON
| {
type: string;
[key: string]: unknown;
@ -56,7 +71,7 @@ function isCategoryShorthand(
}
// categories are collapsed by default, unless user set collapsed = false
const defaultCategoryCollapsedValue = true;
export const DefaultCategoryCollapsedValue = true;
/**
* Convert {category1: [item1,item2]} shorthand syntax to long-form syntax
@ -66,7 +81,7 @@ function normalizeCategoryShorthand(
): SidebarItemCategoryJSON[] {
return Object.entries(sidebar).map(([label, items]) => ({
type: 'category',
collapsed: defaultCategoryCollapsedValue,
collapsed: DefaultCategoryCollapsedValue,
label,
items,
}));
@ -78,7 +93,7 @@ function normalizeCategoryShorthand(
function assertItem<K extends string>(
item: Record<string, unknown>,
keys: K[],
): asserts item is Record<K, never> {
): asserts item is Record<K, unknown> {
const unknownKeys = Object.keys(item).filter(
// @ts-expect-error: key is always string
(key) => !keys.includes(key as string) && key !== 'type',
@ -115,6 +130,24 @@ function assertIsCategory(
}
}
function assertIsAutogenerated(
item: Record<string, unknown>,
): asserts item is SidebarItemAutogeneratedJSON {
assertItem(item, ['dirName', 'customProps']);
if (typeof item.dirName !== 'string') {
throw new Error(
`Error loading ${JSON.stringify(item)}. "dirName" must be a string.`,
);
}
if (item.dirName.startsWith('/') || item.dirName.endsWith('/')) {
throw new Error(
`Error loading ${JSON.stringify(
item,
)}. "dirName" must be a dir path relative to the docs folder root, and should not start or end with /`,
);
}
}
function assertIsDoc(
item: Record<string, unknown>,
): asserts item is SidebarItemDoc {
@ -152,7 +185,7 @@ function assertIsLink(
* Normalizes recursively item and all its children. Ensures that at the end
* each item will be an object with the corresponding type.
*/
function normalizeItem(item: SidebarItemJSON): SidebarItem[] {
function normalizeItem(item: SidebarItemJSON): UnprocessedSidebarItem[] {
if (typeof item === 'string') {
return [
{
@ -169,11 +202,14 @@ function normalizeItem(item: SidebarItemJSON): SidebarItem[] {
assertIsCategory(item);
return [
{
collapsed: defaultCategoryCollapsedValue,
collapsed: DefaultCategoryCollapsedValue,
...item,
items: flatMap(item.items, normalizeItem),
},
];
case 'autogenerated':
assertIsAutogenerated(item);
return [item];
case 'link':
assertIsLink(item);
return [item];
@ -195,7 +231,7 @@ function normalizeItem(item: SidebarItemJSON): SidebarItem[] {
}
}
function normalizeSidebar(sidebar: SidebarJSON) {
function normalizeSidebar(sidebar: SidebarJSON): UnprocessedSidebar {
const normalizedSidebar: SidebarItemJSON[] = Array.isArray(sidebar)
? sidebar
: normalizeCategoryShorthand(sidebar);
@ -203,21 +239,29 @@ function normalizeSidebar(sidebar: SidebarJSON) {
return flatMap(normalizedSidebar, normalizeItem);
}
function normalizeSidebars(sidebars: SidebarsJSON): Sidebars {
function normalizeSidebars(sidebars: SidebarsJSON): UnprocessedSidebars {
return mapValues(sidebars, normalizeSidebar);
}
export const DefaultSidebars: UnprocessedSidebars = {
defaultSidebar: [
{
type: 'autogenerated',
dirName: '.',
},
],
};
// TODO refactor: make async
export function loadSidebars(sidebarFilePath: string): Sidebars {
export function loadSidebars(sidebarFilePath: string): UnprocessedSidebars {
if (!sidebarFilePath) {
throw new Error(`sidebarFilePath not provided: ${sidebarFilePath}`);
}
// sidebars file is optional, some users use docs without sidebars!
// See https://github.com/facebook/docusaurus/issues/3366
// No sidebars file: by default we use the file-system structure to generate the sidebar
// See https://github.com/facebook/docusaurus/pull/4582
if (!fs.existsSync(sidebarFilePath)) {
// throw new Error(`No sidebar file exist at path: ${sidebarFilePath}`);
return {};
return DefaultSidebars;
}
// We don't want sidebars to be cached because of hot reloading.
@ -225,6 +269,87 @@ export function loadSidebars(sidebarFilePath: string): Sidebars {
return normalizeSidebars(sidebarJson);
}
export function toSidebarItemsGeneratorDoc(
doc: DocMetadataBase,
): SidebarItemsGeneratorDoc {
return pick(doc, [
'id',
'frontMatter',
'source',
'sourceDirName',
'sidebarPosition',
]);
}
export function toSidebarItemsGeneratorVersion(
version: VersionMetadata,
): SidebarItemsGeneratorVersion {
return pick(version, ['versionName', 'contentPath']);
}
// Handle the generation of autogenerated sidebar items
export async function processSidebar({
sidebarItemsGenerator,
unprocessedSidebar,
docs,
version,
}: {
sidebarItemsGenerator: SidebarItemsGenerator;
unprocessedSidebar: UnprocessedSidebar;
docs: DocMetadataBase[];
version: VersionMetadata;
}): Promise<Sidebar> {
// Just a minor lazy transformation optimization
const getSidebarItemsGeneratorDocsAndVersion = memoize(() => ({
docs: docs.map(toSidebarItemsGeneratorDoc),
version: toSidebarItemsGeneratorVersion(version),
}));
async function processRecursive(
item: UnprocessedSidebarItem,
): Promise<SidebarItem[]> {
if (item.type === 'category') {
return [
{
...item,
items: (await Promise.all(item.items.map(processRecursive))).flat(),
},
];
}
if (item.type === 'autogenerated') {
return sidebarItemsGenerator({
item,
...getSidebarItemsGeneratorDocsAndVersion(),
});
}
return [item];
}
return (await Promise.all(unprocessedSidebar.map(processRecursive))).flat();
}
export async function processSidebars({
sidebarItemsGenerator,
unprocessedSidebars,
docs,
version,
}: {
sidebarItemsGenerator: SidebarItemsGenerator;
unprocessedSidebars: UnprocessedSidebars;
docs: DocMetadataBase[];
version: VersionMetadata;
}): Promise<Sidebars> {
return combinePromises(
mapValues(unprocessedSidebars, (unprocessedSidebar) =>
processSidebar({
sidebarItemsGenerator,
unprocessedSidebar,
docs,
version,
}),
),
);
}
function collectSidebarItemsOfType<
Type extends SidebarItemType,
Item extends SidebarItem & {type: SidebarItemType}

View file

@ -11,23 +11,31 @@ import {
isValidPathname,
resolvePathname,
} from '@docusaurus/utils';
import {stripPathNumberPrefixes} from './numberPrefix';
export default function getSlug({
baseID,
frontmatterSlug,
dirName,
stripDirNumberPrefixes = true,
}: {
baseID: string;
frontmatterSlug?: string;
dirName: string;
stripDirNumberPrefixes?: boolean;
}): string {
const baseSlug = frontmatterSlug || baseID;
let slug: string;
if (baseSlug.startsWith('/')) {
slug = baseSlug;
} else {
const dirNameStripped = stripDirNumberPrefixes
? stripPathNumberPrefixes(dirName)
: dirName;
const resolveDirname =
dirName === '.' ? '/' : addLeadingSlash(addTrailingSlash(dirName));
dirName === '.'
? '/'
: addLeadingSlash(addTrailingSlash(dirNameStripped));
slug = resolvePathname(baseSlug, resolveDirname);
}

View file

@ -83,6 +83,7 @@ export type PluginOptions = MetadataOptions &
disableVersioning: boolean;
excludeNextVersionDocs?: boolean;
includeCurrentVersion: boolean;
sidebarItemsGenerator: SidebarItemsGenerator;
};
export type SidebarItemBase = {
@ -108,6 +109,27 @@ export type SidebarItemCategory = SidebarItemBase & {
collapsed: boolean;
};
export type UnprocessedSidebarItemAutogenerated = {
type: 'autogenerated';
dirName: string;
};
export type UnprocessedSidebarItemCategory = SidebarItemBase & {
type: 'category';
label: string;
items: UnprocessedSidebarItem[];
collapsed: boolean;
};
export type UnprocessedSidebarItem =
| SidebarItemDoc
| SidebarItemLink
| UnprocessedSidebarItemCategory
| UnprocessedSidebarItemAutogenerated;
export type UnprocessedSidebar = UnprocessedSidebarItem[];
export type UnprocessedSidebars = Record<string, UnprocessedSidebar>;
export type SidebarItem =
| SidebarItemDoc
| SidebarItemLink
@ -115,9 +137,25 @@ export type SidebarItem =
export type Sidebar = SidebarItem[];
export type SidebarItemType = SidebarItem['type'];
export type Sidebars = Record<string, Sidebar>;
// Reduce API surface for options.sidebarItemsGenerator
// The user-provided generator fn should receive only a subset of metadatas
// A change to any of these metadatas can be considered as a breaking change
export type SidebarItemsGeneratorDoc = Pick<
DocMetadataBase,
'id' | 'frontMatter' | 'source' | 'sourceDirName' | 'sidebarPosition'
>;
export type SidebarItemsGeneratorVersion = Pick<
VersionMetadata,
'versionName' | 'contentPath'
>;
export type SidebarItemsGenerator = (generatorArgs: {
item: UnprocessedSidebarItemAutogenerated;
version: SidebarItemsGeneratorVersion;
docs: SidebarItemsGeneratorDoc[];
}) => Promise<SidebarItem[]>;
export type OrderMetadata = {
previous?: string;
next?: string;
@ -143,10 +181,12 @@ export type DocMetadataBase = LastUpdateData & {
title: string;
description: string;
source: string;
sourceDirName: string; // relative to the docs folder (can be ".")
slug: string;
permalink: string;
// eslint-disable-next-line camelcase
sidebar_label?: string;
sidebarPosition?: number;
editUrl?: string | null;
frontMatter: FrontMatter;
};

View file

@ -434,6 +434,7 @@ function filterVersions(
}
}
// TODO make this async (requires plugin init to be async)
export function readVersionsMetadata({
context,
options,

View file

@ -91,6 +91,7 @@ export default function CodeBlock({
children,
className: languageClassName,
metastring,
title,
}: Props): JSX.Element {
const {prism} = useThemeConfig();
@ -107,9 +108,13 @@ export default function CodeBlock({
setMounted(true);
}, []);
// TODO: the title is provided by MDX as props automatically
// so we probably don't need to parse the metastring
// (note: title="xyz" => title prop still has the quotes)
const codeBlockTitle = parseCodeBlockTitle(metastring) || title;
const button = useRef(null);
let highlightLines: number[] = [];
const codeBlockTitle = parseCodeBlockTitle(metastring);
const prismTheme = usePrismTheme();

View file

@ -55,6 +55,7 @@ declare module '@theme/CodeBlock' {
readonly children: string;
readonly className?: string;
readonly metastring?: string;
readonly title?: string;
};
const CodeBlock: (props: Props) => JSX.Element;

View file

@ -10,6 +10,9 @@ const path = require('path');
const fs = require('fs-extra');
const {mapValues, pickBy} = require('lodash');
// Seems the 5s default timeout fails sometimes
jest.setTimeout(15000);
describe('update-code-translations', () => {
test(`to have base.json contain all the translations extracted from the theme. Please run "yarn workspace @docusaurus/theme-classic update-code-translations" to keep base.json up-to-date.`, async () => {
const baseMessages = pickBy(

View file

@ -1,8 +1,4 @@
---
id: support
title: Support
slug: /support
---
# Support
Docusaurus has a community of thousands of developers.

View file

@ -1,8 +1,4 @@
---
id: team
title: Team
slug: /team
---
# Team
import {
ActiveTeamRow,

View file

@ -1,8 +1,4 @@
---
id: resources
title: Awesome Resources
slug: /resources
---
# Awesome Resources
A curated list of interesting Docusaurus community projects.

View file

@ -70,9 +70,16 @@ module.exports = {
include: ['**/*.md', '**/*.mdx'], // Extensions to include.
/**
* Path to sidebar configuration for showing a list of markdown pages.
* Warning: will change
*/
sidebarPath: '',
sidebarPath: 'sidebars.js',
/**
* Function used to replace the sidebar items of type "autogenerated"
* by real sidebar items (docs, categories, links...)
*/
sidebarItemsGenerator: function ({item, version, docs}) {
// Use the provided data to create a custom "sidebar slice"
return [{type: 'doc', id: 'doc1'}];
},
/**
* Theme components used by the docs pages
*/
@ -154,14 +161,16 @@ Markdown documents can use the following markdown frontmatter metadata fields, e
- `id`: A unique document id. If this field is not present, the document's `id` will default to its file name (without the extension)
- `title`: The title of your document. If this field is not present, the document's `title` will default to its `id`
- `hide_title`: Whether to hide the title at the top of the doc. By default it is `false`
- `hide_title`: Whether to hide the title at the top of the doc. By default, it is `false`
- `hide_table_of_contents`: Whether to hide the table of contents to the right. By default it is `false`
- `sidebar_label`: The text shown in the document sidebar and in the next/previous button for this document. If this field is not present, the document's `sidebar_label` will default to its `title`
- `sidebar_position`: Permits to control the position of a doc inside the generated sidebar slice, when using `autogenerated` sidebar items. Can be Int or Float.
- `strip_number_prefixes`: When a document has a number prefix (`001 - My Doc.md`, `2. MyDoc.md`...), it is automatically removed, and the prefix is used as `sidebar_position`. Use `strip_number_prefixes: false` if you want to disable this behavior
- `custom_edit_url`: The URL for editing this document. If this field is not present, the document's edit URL will fall back to `editUrl` from options fields passed to `docusaurus-plugin-content-docs`
- `keywords`: Keywords meta tag for the document page, for search engines
- `description`: The description of your document, which will become the `<meta name="description" content="..."/>` and `<meta property="og:description" content="..."/>` in `<head>`, used by search engines. If this field is not present, it will default to the first line of the contents
- `image`: Cover or thumbnail image that will be used when displaying the link to your post
- `slug`: Allows to customize the document url
- `slug`: Allows to customize the document url (`/<routeBasePath>/<slug>`). Support multiple patterns: `slug: my-doc`, `slug: /my/path/myDoc`, `slug: /`
Example:

View file

@ -4,12 +4,20 @@ title: Creating Pages
slug: /creating-pages
---
In this section, we will learn about creating ad-hoc pages in Docusaurus using React. This is most useful for creating one-off standalone pages like a showcase page, playground page or support page.
In this section, we will learn about creating pages in Docusaurus.
This is useful for creating **one-off standalone pages** like a showcase page, playground page or support page.
The functionality of pages is powered by `@docusaurus/plugin-content-pages`.
You can use React components, or Markdown.
:::note
Pages do not have sidebars, only [docs](./docs/docs-introduction.md) have.
:::
## Add a React page {#add-a-react-page}
Create a file `/src/pages/helloReact.js`:

View file

@ -4,170 +4,241 @@ title: Sidebar
slug: /sidebar
---
To generate a sidebar to your Docusaurus site:
Creating a sidebar is useful to:
- Group multiple **related documents**
- **Display a sidebar** on each of those documents
- Provide a **paginated navigation**, with next/previous button
To use sidebars on your Docusaurus site:
1. Define a file that exports a [sidebar object](#sidebar-object).
1. Pass this object into the `@docusaurus/plugin-docs` plugin directly or via `@docusaurus/preset-classic`.
```js {8-9} title="docusaurus.config.js"
```js title="docusaurus.config.js"
module.exports = {
// ...
presets: [
[
'@docusaurus/preset-classic',
{
docs: {
// Sidebars filepath relative to the site dir.
// highlight-start
sidebarPath: require.resolve('./sidebars.js'),
// highlight-end
},
// ...
},
],
],
};
```
## Sidebar object {#sidebar-object}
## Default sidebar
A sidebar object contains [sidebar items](#understanding-sidebar-items) and it is defined like this:
```typescript
type Sidebar = {
[sidebarId: string]:
| {
[sidebarCategory: string]: SidebarItem[];
}
| SidebarItem[];
};
```
For example:
By default, Docusaurus [automatically generates a sidebar](#sidebar-item-autogenerated) for you, by using the filesystem structure of the `docs` folder:
```js title="sidebars.js"
module.exports = {
docs: [
mySidebar: [
{
type: 'category',
label: 'Getting Started',
items: ['greeting'],
},
{
type: 'category',
label: 'Docusaurus',
items: ['doc1'],
type: 'autogenerated',
dirName: '.', // generate sidebar slice from the docs folder (or versioned_docs/<version>)
},
],
};
```
In this example, notice the following:
You can also define your sidebars explicitly.
- The key `docs` is the id of the sidebar. The id can be any value, not necessarily `docs`.
- `Getting Started` is a category within the sidebar.
- `greeting` and `doc1` are both [sidebar item](#understanding-sidebar-items).
## Sidebar object {#sidebar-object}
Shorthand notation can also be used:
A sidebar is a **tree of [sidebar items](#understanding-sidebar-items)**.
```typescript
type Sidebar =
// Normal syntax
| SidebarItem[]
// Shorthand syntax
| Record<
string, // category label
SidebarItem[] // category items
>;
```
A sidebars file can contain **multiple sidebar objects**.
```typescript
type SidebarsFile = Record<
string, // sidebar id
Sidebar
>;
```
Example:
```js title="sidebars.js"
module.exports = {
docs: {
'Getting started': ['greeting'],
Docusaurus: ['doc1'],
},
mySidebar: [
{
type: 'category',
label: 'Getting Started',
items: ['doc1'],
},
{
type: 'category',
label: 'Docusaurus',
items: ['doc2', 'doc3'],
},
],
};
```
:::note
Notice the following:
Shorthand notation relies on the iteration order of JavaScript object keys for the category name. When using this notation, keep in mind that EcmaScript does not guarantee `Object.keys({a,b}) === ['a','b']`, yet this is generally true.
- There is a single sidebar `mySidebar`, containing 5 [sidebar items](#understanding-sidebar-items)
- `Getting Started` and `Docusaurus` are sidebar categories
- `doc1`, `doc2` and `doc3` are sidebar documents
:::tip
Use the **shorthand syntax** to express this sidebar more concisely:
```js title="sidebars.js"
module.exports = {
mySidebar: {
'Getting started': ['doc1'],
Docusaurus: ['doc2', 'doc3'],
},
};
```
:::
## Using multiple sidebars {#using-multiple-sidebars}
You can have multiple sidebars for different Markdown files by adding more top-level keys to the exported object.
You can create a sidebar for each **set of markdown files** that you want to **group together**.
:::tip
The Docusaurus site is a good example of using multiple sidebars:
- [Docs](../../introduction.md)
- [API](../../cli.md)
:::
Example:
```js title="sidebars.js"
module.exports = {
firstSidebar: {
'Category A': ['doc1'],
},
secondSidebar: {
'Category A': ['doc2'],
'Category B': ['doc3'],
tutorialSidebar: {
'Category A': ['doc1', 'doc2'],
},
apiSidebar: ['doc3', 'doc4'],
};
```
By default, the doc page the user is reading will display the sidebar that it is part of. This can be customized with the [sidebar type](#understanding-sidebar-items).
:::note
For example, with the above example, when displaying the `doc2` page, the sidebar will contain the items of `secondSidebar` only. Another example of multiple sidebars is the `API` and `Docs` sections on the Docusaurus website.
The keys `tutorialSidebar` and `apiSidebar` are sidebar **technical ids** and do not matter much.
For more information about sidebars and how they relate to doc pages, see [Navbar doc link](../../api/themes/theme-configuration.md#navbar-doc-link).
:::
When browsing:
- `doc1` or `doc2`: the `tutorialSidebar` will be displayed
- `doc3` or `doc4`: the `apiSidebar` will be displayed
A **paginated navigation** link documents inside the same sidebar with **next and previous buttons**.
## Understanding sidebar items {#understanding-sidebar-items}
As the name implies, `SidebarItem` is an item defined in a Sidebar. A SidebarItem as a `type` that defines what the item links to.
`SidebarItem` is an item defined in a Sidebar tree.
`type` supports the following values
There are different types of sidebar items:
- [Doc](#linking-to-a-doc-page)
- [Link](#creating-a-generic-link)
- [Ref](#creating-a-link-to-page-without-sidebar)
- [Category](#creating-a-hierarchy)
- **[Doc](#sidebar-item-doc)**: link to a doc page, assigning it to the sidebar
- **[Ref](#sidebar-item-ref)**: link to a doc page, without assigning it to the sidebar
- **[Link](#sidebar-item-link)**: link to any internal or external page
- **[Category](#sidebar-item-category)**: create a hierarchy of sidebar items
- **[Autogenerated](#sidebar-item-autogenerated)**: generate a sidebar slice automatically
### Linking to a doc page {#linking-to-a-doc-page}
### Doc: link to a doc {#sidebar-item-doc}
Set `type` to `doc` to link to a documentation page. This is the default type.
Use the `doc` type to link to a doc page and assign that doc to a sidebar:
```typescript
type SidebarItemDoc =
| string
// Normal syntax
| {
type: 'doc';
id: string;
label: string; // Sidebar label text
};
}
// Shorthand syntax
| string; // docId shortcut
```
Example:
```js
{
type: 'doc',
id: 'doc1', // string - document id
label: 'Getting started' // Sidebar label text
}
```
The `sidebar_label` in the markdown frontmatter has a higher precedence over the `label` key in `SidebarItemDoc`. Using just the [Document ID](#document-id) is also valid, the following is equivalent to the above:
```js
'doc1'; // string - document id
```
Using this type will bind the linked doc to current sidebar. This means that if you access the `doc1` page, the sidebar displayed will be the sidebar that contains this doc page.
In the example below, `doc1` is bound to `firstSidebar`.
```js title="sidebars.js"
module.exports = {
firstSidebar: {
'Category A': ['doc1'],
},
secondSidebar: {
'Category A': ['doc2'],
'Category B': ['doc3'],
},
mySidebar: [
// Normal syntax:
// highlight-start
{
type: 'doc',
id: 'doc1', // document id
label: 'Getting started', // sidebar label
},
// highlight-end
// Shorthand syntax:
// highlight-start
'doc2', // document id
// highlight-end
],
};
```
### Creating a generic link {#creating-a-generic-link}
The `sidebar_label` markdown frontmatter has a higher precedence over the `label` key in `SidebarItemDoc`.
Set `type` to `link` to link to a non-documentation page. For example, to create an external link.
:::note
Don't assign the same doc to multiple sidebars: use a [ref](#sidebar-item-ref) instead.
:::
### Ref: link to a doc, without sidebar {#sidebar-item-ref}
Use the `ref` type to link to a doc page without assigning it to a sidebar.
```typescript
type SidebarItemRef = {
type: 'ref';
id: string;
};
```
Example:
```js title="sidebars.js"
module.exports = {
mySidebar: [
{
type: 'ref',
id: 'doc1', // Document id (string).
},
],
};
```
When browsing `doc1`, Docusaurus **will not display** the `mySidebar` sidebar.
### Link: link to any page {#sidebar-item-link}
Use the `link` type to link to any page (internal or external) that is not a doc.
```typescript
type SidebarItemLink = {
@ -179,43 +250,41 @@ type SidebarItemLink = {
Example:
```js
{
type: 'link',
label: 'Custom Label', // The label that should be displayed (string).
href: 'https://example.com' // The target URL (string).
}
```
```js title="sidebars.js"
module.exports = {
myLinksSidebar: [
// highlight-start
// External link
{
type: 'link',
label: 'Facebook', // The link label
href: 'https://facebook.com', // The external URL
},
// highlight-end
### Creating a link to page without sidebar {#creating-a-link-to-page-without-sidebar}
Set `type` to `ref` to link to a documentation page without binding it to a sidebar. This means the sidebar disappears when the user displays the linked page.
```typescript
type SidebarItemRef = {
type: 'ref';
id: string;
// highlight-start
// Internal link
{
type: 'link',
label: 'Home', // The link label
href: '/', // The internal path
},
// highlight-end
],
};
```
Example:
### Category: create a hierarchy {#sidebar-item-category}
```js
{
type: 'ref',
id: 'doc1', // Document id (string).
}
```
### Creating a hierarchy {#creating-a-hierarchy}
The Sidebar item type that creates a hierarchy in the sidebar. Set `type` to `category`.
Use the `category` type to create a hierarchy of sidebar items.
```typescript
type SidebarItemCategory = {
type: 'category';
label: string; // Sidebar label text.
items: SidebarItem[]; // Array of sidebar items.
// Category options:
collapsed: boolean; // Set the category to be collapsed or open by default
};
```
@ -225,16 +294,16 @@ Example:
```js title="sidebars.js"
module.exports = {
docs: [
{
...
},
{
type: 'category',
label: 'Guides',
collapsed: false,
items: [
'guides/creating-pages',
'creating-pages',
{
Docs: ['docs-introduction', 'docs-sidebar', 'markdown-features', 'versioning'],
type: 'category',
label: 'Docs',
items: ['introduction', 'sidebar', 'markdown-features', 'versioning'],
},
],
},
@ -242,7 +311,9 @@ module.exports = {
};
```
**Note**: it's possible to use the shorthand syntax to create nested categories:
:::tip
Use the **shorthand syntax** when you don't need **category options**:
```js title="sidebars.js"
module.exports = {
@ -250,28 +321,25 @@ module.exports = {
Guides: [
'creating-pages',
{
Docs: [
'docs-introduction',
'docs-sidebar',
'markdown-features',
'versioning',
],
Docs: ['introduction', 'sidebar', 'markdown-features', 'versioning'],
},
],
},
};
```
:::
#### Collapsible categories {#collapsible-categories}
For sites with a sizable amount of content, we support the option to expand/collapse a category to toggle the display of its contents. Categories are collapsible by default. If you want them to be always expanded, set `themeConfig.sidebarCollapsible` to `false`:
```js {4} title="docusaurus.config.js"
```js title="docusaurus.config.js"
module.exports = {
// ...
themeConfig: {
// highlight-start
sidebarCollapsible: false,
// ...
// highlight-end
},
};
```
@ -296,16 +364,189 @@ module.exports = {
};
```
### Autogenerated: generate a sidebar {#sidebar-item-autogenerated}
Docusaurus can **create a sidebar automatically** from your **filesystem structure**: each folder creates a sidebar category.
An `autogenerated` item is converted by Docusaurus to a **sidebar slice**: a list of items of type `doc` and `category`.
```typescript
type SidebarItemAutogenerated = {
type: 'autogenerated';
dirName: string; // Source folder to generate the sidebar slice from (relative to docs)
};
```
Docusaurus can generate a sidebar from your docs folder:
```js title="sidebars.js"
module.exports = {
myAutogeneratedSidebar: [
// highlight-start
{
type: 'autogenerated',
dirName: '.', // '.' means the current docs folder
},
// highlight-end
],
};
```
You can also use **multiple `autogenerated` items** in a sidebar, and interleave them with regular sidebar items:
```js title="sidebars.js"
module.exports = {
mySidebar: [
'intro',
{
type: 'category',
label: 'Tutorials',
items: [
'tutorial-intro',
// highlight-start
{
type: 'autogenerated',
dirName: 'tutorials/easy', // Generate sidebar slice from docs/tutorials/easy
},
// highlight-end
'tutorial-medium',
// highlight-start
{
type: 'autogenerated',
dirName: 'tutorials/advanced', // Generate sidebar slice from docs/tutorials/hard
},
// highlight-end
'tutorial-end',
],
},
// highlight-start
{
type: 'autogenerated',
dirName: 'guides', // Generate sidebar slice from docs/guides
},
// highlight-end
{
type: 'category',
label: 'Community',
items: ['team', 'chat'],
},
],
};
```
#### Autogenerated sidebar metadatas {#autogenerated-sidebar-metadatas}
By default, the sidebar slice will be generated in **alphabetical order** (using files and folders names).
If the generated sidebar does not look good, you can assign additional metadatas to docs and categories.
**For docs**: use additional frontmatter:
```diff title="docs/tutorials/tutorial-easy.md"
+ ---
+ sidebar_label: Easy
+ sidebar_position: 2
+ ---
# Easy Tutorial
This is the easy tutorial!
```
**For categories**: add a `_category_.json` or `_category_.yml` file in the appropriate folder:
```json title="docs/tutorials/_category_.json"
{
"label": "Tutorial",
"position": 3
}
```
```yaml title="docs/tutorials/_category_.yml"
label: 'Tutorial'
position: 2.5 # float position is supported
collapsed: false # keep the category open by default
```
:::info
The position metadata is only used **inside a sidebar slice**: Docusaurus does not re-order other items of your sidebar.
:::
#### Using number prefixes
A simple way to order an autogenerated sidebar is to prefix docs and folders by number prefixes:
```bash
docs
├── 01-Intro.md
├── 02-Tutorial Easy
│   ├── 01-First Part.md
│   ├── 02-Second Part.md
│   └── 03-End.md
├── 03-Tutorial Hard
│   ├── 01-First Part.md
│   ├── 02-Second Part.md
│   ├── 03-Third Part.md
│   └── 04-End.md
└── 04-End.md
```
To make it **easier to adopt**, Docusaurus supports **multiple number prefix patterns**.
By default, Docusaurus will **remove the number prefix** from the doc id, title, label and url paths.
:::caution
**Prefer using [additional metadatas](#autogenerated-sidebar-metadatas)**.
Updating a number prefix can be annoying, as it can require **updating multiple existing markdown links**:
```diff title="docs/02-Tutorial Easy/01-First Part.md"
- Check the [Tutorial End](../04-End.md);
+ Check the [Tutorial End](../05-End.md);
```
:::
#### Customize the sidebar items generator
You can provide a custom `sidebarItemsGenerator` function in the docs plugin (or preset) config:
```js title="docusaurus.config.js"
module.exports = {
plugins: [
[
'@docusaurus/plugin-content-docs',
{
/**
* Function used to replace the sidebar items of type "autogenerated"
* by real sidebar items (docs, categories, links...)
*/
// highlight-start
sidebarItemsGenerator: function ({item, version, docs}) {
// Use the provided data to create a custom "sidebar slice"
return [{type: 'doc', id: 'doc1'}];
},
// highlight-end
},
],
],
};
```
## Hideable sidebar {#hideable-sidebar}
Using the enabled `themeConfig.hideableSidebar` option, you can make the entire sidebar hidden, allowing you to better focus your users on the content. This is especially useful when content consumption on medium screens (e.g. on tablets).
```js {4} title="docusaurus.config.js"
```js title="docusaurus.config.js"
module.exports = {
// ...
themeConfig: {
// highlight-starrt
hideableSidebar: true,
// ...
// highlight-end
},
};
```
@ -323,3 +564,21 @@ To pass in custom props to a swizzled sidebar item, add the optional `customProp
}
}
```
## Complex sidebars example {#complex-sidebars-example}
Real-world example from the Docusaurus site:
```mdx-code-block
import CodeBlock from '@theme/CodeBlock';
<CodeBlock className="language-js" title="sidebars.js">
{require('!!raw-loader!@site/sidebars.js')
.default
.split('\n')
// remove comments
.map((line) => !['#','/*','*'].some(commentPattern => line.trim().startsWith(commentPattern)) && line)
.filter(Boolean)
.join('\n')}
</CodeBlock>
```

View file

@ -514,13 +514,15 @@ It is currently **not possible to link to a specific file** in Crowdin.
The **Docusaurus v2 configuration file** is a good example of using versioning and multi-instance:
```mdx-code-block
import CrowdinConfigV2 from '!!raw-loader!@site/../crowdin-v2.yaml';
import CodeBlock from '@theme/CodeBlock';
<CodeBlock className="language-yaml" title="test">
<CodeBlock className="language-yaml" title="crowdin.yml">
{CrowdinConfigV2.split('\n')
// remove comments
.map((line) => !line.startsWith('#') && line)
.filter(Boolean)
.join('\n')}
</CodeBlock>
```

View file

@ -7,9 +7,10 @@
module.exports = {
community: [
'support',
'team',
'resources',
{
type: 'autogenerated',
dirName: '.',
},
{
type: 'link',
href: '/showcase',

View file

@ -3480,6 +3480,11 @@
jest-diff "^26.0.0"
pretty-format "^26.0.0"
"@types/js-yaml@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.0.tgz#d1a11688112091f2c711674df3a65ea2f47b5dfb"
integrity sha512-4vlpCM5KPCL5CfGmTbpjwVKbISRYhduEJvvUWsH5EB7QInhEj94XPZ3ts/9FPiLZFqYO0xoW4ZL8z2AabTGgJA==
"@types/jscodeshift@^0.7.1":
version "0.7.1"
resolved "https://registry.yarnpkg.com/@types/jscodeshift/-/jscodeshift-0.7.1.tgz#8afcda6c8ca2ce828c3b192f8a1ba0245987ac12"
@ -4504,6 +4509,11 @@ argparse@^1.0.10, argparse@^1.0.7:
dependencies:
sprintf-js "~1.0.2"
argparse@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
aria-query@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b"
@ -6292,6 +6302,11 @@ columnify@^1.5.4:
strip-ansi "^3.0.0"
wcwidth "^1.0.0"
combine-promises@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/combine-promises/-/combine-promises-1.1.0.tgz#72db90743c0ca7aab7d0d8d2052fd7b0f674de71"
integrity sha512-ZI9jvcLDxqwaXEixOhArm3r7ReIivsXkpbyEWyeOhzz1QS0iSgBPnWvEqvIQtYyamGCYA88gFhmUrs9hrrQ0pg==
combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
@ -12017,6 +12032,13 @@ js-yaml@^3.11.0, js-yaml@^3.13.1, js-yaml@^3.14.0, js-yaml@^3.8.1:
argparse "^1.0.7"
esprima "^4.0.0"
js-yaml@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f"
integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==
dependencies:
argparse "^2.0.1"
jsbn@~0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"