diff --git a/packages/docusaurus-init/templates/classic/docs/create-a-document.md b/packages/docusaurus-init/templates/classic/docs/create-a-document.md
deleted file mode 100644
index fbcc2782a9..0000000000
--- a/packages/docusaurus-init/templates/classic/docs/create-a-document.md
+++ /dev/null
@@ -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', ...],
- },
- ],
-};
-```
diff --git a/packages/docusaurus-init/templates/classic/docs/getting-started.md b/packages/docusaurus-init/templates/classic/docs/intro.md
similarity index 73%
rename from packages/docusaurus-init/templates/classic/docs/getting-started.md
rename to packages/docusaurus-init/templates/classic/docs/intro.md
index 34a553c907..cb9ab0b030 100644
--- a/packages/docusaurus-init/templates/classic/docs/getting-started.md
+++ b/packages/docusaurus-init/templates/classic/docs/intro.md
@@ -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
diff --git a/packages/docusaurus-init/templates/classic/docs/tutorial-basics/_category_.json b/packages/docusaurus-init/templates/classic/docs/tutorial-basics/_category_.json
new file mode 100644
index 0000000000..135e4a6858
--- /dev/null
+++ b/packages/docusaurus-init/templates/classic/docs/tutorial-basics/_category_.json
@@ -0,0 +1,4 @@
+{
+ "label": "Tutorial - Basics",
+ "position": 2
+}
diff --git a/packages/docusaurus-init/templates/classic/docs/congratulations.md b/packages/docusaurus-init/templates/classic/docs/tutorial-basics/congratulations.md
similarity index 50%
rename from packages/docusaurus-init/templates/classic/docs/congratulations.md
rename to packages/docusaurus-init/templates/classic/docs/tutorial-basics/congratulations.md
index 71010450e8..9ef99bbadd 100644
--- a/packages/docusaurus-init/templates/classic/docs/congratulations.md
+++ b/packages/docusaurus-init/templates/classic/docs/tutorial-basics/congratulations.md
@@ -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?
diff --git a/packages/docusaurus-init/templates/classic/docs/create-a-blog-post.md b/packages/docusaurus-init/templates/classic/docs/tutorial-basics/create-a-blog-post.md
similarity index 90%
rename from packages/docusaurus-init/templates/classic/docs/create-a-blog-post.md
rename to packages/docusaurus-init/templates/classic/docs/tutorial-basics/create-a-blog-post.md
index a50ccb337f..d893e1c74f 100644
--- a/packages/docusaurus-init/templates/classic/docs/create-a-blog-post.md
+++ b/packages/docusaurus-init/templates/classic/docs/tutorial-basics/create-a-blog-post.md
@@ -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`:
diff --git a/packages/docusaurus-init/templates/classic/docs/tutorial-basics/create-a-document.md b/packages/docusaurus-init/templates/classic/docs/tutorial-basics/create-a-document.md
new file mode 100644
index 0000000000..66b58543d4
--- /dev/null
+++ b/packages/docusaurus-init/templates/classic/docs/tutorial-basics/create-a-document.md
@@ -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'],
+ },
+ ],
+};
+```
diff --git a/packages/docusaurus-init/templates/classic/docs/create-a-page.md b/packages/docusaurus-init/templates/classic/docs/tutorial-basics/create-a-page.md
similarity index 80%
rename from packages/docusaurus-init/templates/classic/docs/create-a-page.md
rename to packages/docusaurus-init/templates/classic/docs/tutorial-basics/create-a-page.md
index 314bd87e9d..e112b0059c 100644
--- a/packages/docusaurus-init/templates/classic/docs/create-a-page.md
+++ b/packages/docusaurus-init/templates/classic/docs/tutorial-basics/create-a-page.md
@@ -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
diff --git a/packages/docusaurus-init/templates/classic/docs/deploy-your-site.md b/packages/docusaurus-init/templates/classic/docs/tutorial-basics/deploy-your-site.md
similarity index 61%
rename from packages/docusaurus-init/templates/classic/docs/deploy-your-site.md
rename to packages/docusaurus-init/templates/classic/docs/tutorial-basics/deploy-your-site.md
index cf0bff5277..492eae0276 100644
--- a/packages/docusaurus-init/templates/classic/docs/deploy-your-site.md
+++ b/packages/docusaurus-init/templates/classic/docs/tutorial-basics/deploy-your-site.md
@@ -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
diff --git a/packages/docusaurus-init/templates/classic/docs/markdown-features.mdx b/packages/docusaurus-init/templates/classic/docs/tutorial-basics/markdown-features.mdx
similarity index 91%
rename from packages/docusaurus-init/templates/classic/docs/markdown-features.mdx
rename to packages/docusaurus-init/templates/classic/docs/tutorial-basics/markdown-features.mdx
index 9b9f9ca076..8855626051 100644
--- a/packages/docusaurus-init/templates/classic/docs/markdown-features.mdx
+++ b/packages/docusaurus-init/templates/classic/docs/tutorial-basics/markdown-features.mdx
@@ -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
diff --git a/packages/docusaurus-init/templates/classic/docs/tutorial-extras/_category_.json b/packages/docusaurus-init/templates/classic/docs/tutorial-extras/_category_.json
new file mode 100644
index 0000000000..ca3f8e0640
--- /dev/null
+++ b/packages/docusaurus-init/templates/classic/docs/tutorial-extras/_category_.json
@@ -0,0 +1,4 @@
+{
+ "label": "Tutorial - Extras",
+ "position": 3
+}
diff --git a/packages/docusaurus-init/templates/classic/docs/manage-docs-versions.md b/packages/docusaurus-init/templates/classic/docs/tutorial-extras/manage-docs-versions.md
similarity index 88%
rename from packages/docusaurus-init/templates/classic/docs/manage-docs-versions.md
rename to packages/docusaurus-init/templates/classic/docs/tutorial-extras/manage-docs-versions.md
index 1f2cd1a8b3..6335b0ac94 100644
--- a/packages/docusaurus-init/templates/classic/docs/manage-docs-versions.md
+++ b/packages/docusaurus-init/templates/classic/docs/tutorial-extras/manage-docs-versions.md
@@ -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:
diff --git a/packages/docusaurus-init/templates/classic/docs/translate-your-site.md b/packages/docusaurus-init/templates/classic/docs/tutorial-extras/translate-your-site.md
similarity index 92%
rename from packages/docusaurus-init/templates/classic/docs/translate-your-site.md
rename to packages/docusaurus-init/templates/classic/docs/tutorial-extras/translate-your-site.md
index 4c2eaf0c96..0606070cac 100644
--- a/packages/docusaurus-init/templates/classic/docs/translate-your-site.md
+++ b/packages/docusaurus-init/templates/classic/docs/tutorial-extras/translate-your-site.md
@@ -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.
diff --git a/packages/docusaurus-init/templates/classic/docusaurus.config.js b/packages/docusaurus-init/templates/classic/docusaurus.config.js
index 023a5534a8..6dfe1342b1 100644
--- a/packages/docusaurus-init/templates/classic/docusaurus.config.js
+++ b/packages/docusaurus-init/templates/classic/docusaurus.config.js
@@ -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',
},
],
},
diff --git a/packages/docusaurus-init/templates/classic/sidebars.js b/packages/docusaurus-init/templates/classic/sidebars.js
index 5ed8bc649a..981a73cd7a 100644
--- a/packages/docusaurus-init/templates/classic/sidebars.js
+++ b/packages/docusaurus-init/templates/classic/sidebars.js
@@ -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'],
},
],
+ */
};
diff --git a/packages/docusaurus-init/templates/classic/src/components/HomepageFeatures.js b/packages/docusaurus-init/templates/classic/src/components/HomepageFeatures.js
index 19c618e64b..16f820b103 100644
--- a/packages/docusaurus-init/templates/classic/src/components/HomepageFeatures.js
+++ b/packages/docusaurus-init/templates/classic/src/components/HomepageFeatures.js
@@ -41,8 +41,10 @@ function Feature({Svg, title, description}) {
-
{title}
-
{description}
+
+
{title}
+
{description}
+
);
}
diff --git a/packages/docusaurus-init/templates/classic/src/pages/index.js b/packages/docusaurus-init/templates/classic/src/pages/index.js
index bbded37670..27c21e8f99 100644
--- a/packages/docusaurus-init/templates/classic/src/pages/index.js
+++ b/packages/docusaurus-init/templates/classic/src/pages/index.js
@@ -14,8 +14,10 @@ function HomepageHeader() {
{siteConfig.title}
{siteConfig.tagline}
-
- Get Started - Docusaurus Tutorial
+
+ Docusaurus Tutorial - 5min ⏱️
diff --git a/packages/docusaurus-plugin-content-docs/package.json b/packages/docusaurus-plugin-content-docs/package.json
index 4956f93069..ffa19b92d1 100644
--- a/packages/docusaurus-plugin-content-docs/package.json
+++ b/packages/docusaurus-plugin-content-docs/package.json
@@ -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",
diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/0-getting-started.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/0-getting-started.md
new file mode 100644
index 0000000000..1ace6c84ba
--- /dev/null
+++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/0-getting-started.md
@@ -0,0 +1,3 @@
+# Getting Started
+
+Getting started text
diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/1-installation.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/1-installation.md
new file mode 100644
index 0000000000..a8ef429a7a
--- /dev/null
+++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/1-installation.md
@@ -0,0 +1,3 @@
+# Installation
+
+Installation text
diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/00_api-overview.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/00_api-overview.md
new file mode 100644
index 0000000000..62db8a9152
--- /dev/null
+++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/00_api-overview.md
@@ -0,0 +1,3 @@
+# API Overview
+
+API Overview text
diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/01_Core APIs/0 --- Client API.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/01_Core APIs/0 --- Client API.md
new file mode 100644
index 0000000000..a6e52ca91a
--- /dev/null
+++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/01_Core APIs/0 --- Client API.md
@@ -0,0 +1 @@
+Client API text
diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/01_Core APIs/1 --- Server API.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/01_Core APIs/1 --- Server API.md
new file mode 100644
index 0000000000..3730187399
--- /dev/null
+++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/01_Core APIs/1 --- Server API.md
@@ -0,0 +1 @@
+Server API text
diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/02_Extension APIs/0. Plugin API.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/02_Extension APIs/0. Plugin API.md
new file mode 100644
index 0000000000..d7cfa7e3ea
--- /dev/null
+++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/02_Extension APIs/0. Plugin API.md
@@ -0,0 +1 @@
+Plugin API text
diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/02_Extension APIs/1. Theme API.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/02_Extension APIs/1. Theme API.md
new file mode 100644
index 0000000000..602b1f0340
--- /dev/null
+++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/02_Extension APIs/1. Theme API.md
@@ -0,0 +1 @@
+Theme API text
diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/02_Extension APIs/_category_.yml b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/02_Extension APIs/_category_.yml
new file mode 100644
index 0000000000..43ac56e8fb
--- /dev/null
+++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/02_Extension APIs/_category_.yml
@@ -0,0 +1 @@
+label: 'Extension APIs (label from _category_.yml)'
diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/03_api-end.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/03_api-end.md
new file mode 100644
index 0000000000..c3769f715a
--- /dev/null
+++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/03_api-end.md
@@ -0,0 +1,3 @@
+# API End
+
+API End text
diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/_category_.json b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/_category_.json
new file mode 100644
index 0000000000..927c1d7d4b
--- /dev/null
+++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/_category_.json
@@ -0,0 +1,3 @@
+{
+ "label": "API (label from _category_.json)"
+}
diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/0-guide2.5.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/0-guide2.5.md
new file mode 100644
index 0000000000..92f2d85128
--- /dev/null
+++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/0-guide2.5.md
@@ -0,0 +1,8 @@
+---
+id: guide2.5
+sidebar_position: 2.5
+---
+
+# Guide 2.5
+
+Guide 2.5 text
diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/02-guide2.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/02-guide2.md
new file mode 100644
index 0000000000..cf298d838f
--- /dev/null
+++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/02-guide2.md
@@ -0,0 +1,7 @@
+---
+id: guide2
+---
+
+# Guide 2
+
+Guide 2 text
diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/_category_.json b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/_category_.json
new file mode 100644
index 0000000000..046edaa301
--- /dev/null
+++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/_category_.json
@@ -0,0 +1,3 @@
+{
+ "position": 2
+}
diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/a-guide4.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/a-guide4.md
new file mode 100644
index 0000000000..54c8c7be3a
--- /dev/null
+++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/a-guide4.md
@@ -0,0 +1,7 @@
+---
+id: guide4
+---
+
+# Guide 4
+
+Guide 4 text
diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/b-guide5.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/b-guide5.md
new file mode 100644
index 0000000000..b9311ebbba
--- /dev/null
+++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/b-guide5.md
@@ -0,0 +1,7 @@
+---
+id: guide5
+---
+
+# Guide 5
+
+Guide 5 text
diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/guide3.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/guide3.md
new file mode 100644
index 0000000000..a11a28767c
--- /dev/null
+++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/guide3.md
@@ -0,0 +1,8 @@
+---
+id: guide3
+sidebar_position: 3
+---
+
+# Guide 3
+
+Guide 3 text
diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/z-guide1.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/z-guide1.md
new file mode 100644
index 0000000000..d3e4f3bcb2
--- /dev/null
+++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/z-guide1.md
@@ -0,0 +1,8 @@
+---
+id: guide1
+sidebar_position: 1
+---
+
+# Guide 1
+
+Guide 1 text
diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docusaurus.config.js b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docusaurus.config.js
new file mode 100644
index 0000000000..6fa02ca102
--- /dev/null
+++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docusaurus.config.js
@@ -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',
+};
diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/partialAutogeneratedSidebars.js b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/partialAutogeneratedSidebars.js
new file mode 100644
index 0000000000..65f30d493d
--- /dev/null
+++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/partialAutogeneratedSidebars.js
@@ -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',
+ },
+ ],
+ },
+ ],
+};
diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap
index 95a79b9d53..b5fda7366b 100644
--- a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap
+++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap
@@ -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\\",
diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts
index 0ffdeb3779..b1216944ea 100644
--- a/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts
+++ b/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts
@@ -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',
diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts
index d1ceaf4a1d..7c5bc8f94e 100644
--- a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts
+++ b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts
@@ -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 = {
next: undefined,
previous: undefined,
@@ -40,6 +51,7 @@ const defaultDocMetadata: Partial = {
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[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();
+ });
+});
diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/numberPrefix.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/numberPrefix.test.ts
new file mode 100644
index 0000000000..af116cb1db
--- /dev/null
+++ b/packages/docusaurus-plugin-content-docs/src/__tests__/numberPrefix.test.ts
@@ -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,
+ });
+ });
+ });
+});
diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/options.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/options.test.ts
index fd1c61ddf0..2f9e0ec2f1 100644
--- a/packages/docusaurus-plugin-content-docs/src/__tests__/options.test.ts
+++ b/packages/docusaurus-plugin-content-docs/src/__tests__/options.test.ts
@@ -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],
diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/sidebarItemsGenerator.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/sidebarItemsGenerator.test.ts
new file mode 100644
index 0000000000..38b8818cd5
--- /dev/null
+++ b/packages/docusaurus-plugin-content-docs/src/__tests__/sidebarItemsGenerator.test.ts
@@ -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[0]>,
+ ) {
+ return DefaultSidebarItemsGenerator({
+ item: {
+ type: 'autogenerated',
+ dirName: '.',
+ },
+ version: {
+ versionName: 'current',
+ contentPath: 'docs',
+ },
+ docs: [],
+ ...options,
+ });
+ }
+
+ function mockCategoryMetadataFiles(
+ categoryMetadataFiles: Record>,
+ ) {
+ 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);
+ });
+});
diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/sidebars.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/sidebars.test.ts
index 17b6f06d49..4e717cd10f 100644
--- a/packages/docusaurus-plugin-content-docs/src/__tests__/sidebars.test.ts
+++ b/packages/docusaurus-plugin-content-docs/src/__tests__/sidebars.test.ts
@@ -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 = [
{
diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/slug.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/slug.test.ts
index 9e31cf3ad5..a49486d87d 100644
--- a/packages/docusaurus-plugin-content-docs/src/__tests__/slug.test.ts
+++ b/packages/docusaurus-plugin-content-docs/src/__tests__/slug.test.ts
@@ -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(
diff --git a/packages/docusaurus-plugin-content-docs/src/cli.ts b/packages/docusaurus-plugin-content-docs/src/cli.ts
index 8e17e6d8ad..66988167c4 100644
--- a/packages/docusaurus-plugin-content-docs/src/cli.ts
+++ b/packages/docusaurus-plugin-content-docs/src/cli.ts
@@ -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(
diff --git a/packages/docusaurus-plugin-content-docs/src/docFrontMatter.ts b/packages/docusaurus-plugin-content-docs/src/docFrontMatter.ts
index b235bb779e..6ae0fe77b1 100644
--- a/packages/docusaurus-plugin-content-docs/src/docFrontMatter.ts
+++ b/packages/docusaurus-plugin-content-docs/src/docFrontMatter.ts
@@ -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({
@@ -23,7 +25,9 @@ const DocFrontMatterSchema = Joi.object({
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(
diff --git a/packages/docusaurus-plugin-content-docs/src/docs.ts b/packages/docusaurus-plugin-content-docs/src/docs.ts
index 632113c60d..7cdfc1c724 100644
--- a/packages/docusaurus-plugin-content-docs/src/docs.ts
+++ b/packages/docusaurus-plugin-content-docs/src/docs.ts
@@ -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,
};
}
diff --git a/packages/docusaurus-plugin-content-docs/src/index.ts b/packages/docusaurus-plugin-content-docs/src/index.ts
index 894da94cfe..72e12b8058 100644
--- a/packages/docusaurus-plugin-content-docs/src/index.ts
+++ b/packages/docusaurus-plugin-content-docs/src/index.ts
@@ -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 {
- 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);
diff --git a/packages/docusaurus-plugin-content-docs/src/numberPrefix.ts b/packages/docusaurus-plugin-content-docs/src/numberPrefix.ts
new file mode 100644
index 0000000000..0caa02709a
--- /dev/null
+++ b/packages/docusaurus-plugin-content-docs/src/numberPrefix.ts
@@ -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 = /^(?\d+)(?\s*[-_.]+\s*)(?.*)$/;
+
+// 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,
+ };
+}
diff --git a/packages/docusaurus-plugin-content-docs/src/options.ts b/packages/docusaurus-plugin-content-docs/src/options.ts
index e7f7bcb1c7..a38a9f58d4 100644
--- a/packages/docusaurus-plugin-content-docs/src/options.ts
+++ b/packages/docusaurus-plugin-content-docs/src/options.ts
@@ -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 = {
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),
diff --git a/packages/docusaurus-plugin-content-docs/src/sidebarItemsGenerator.ts b/packages/docusaurus-plugin-content-docs/src/sidebarItemsGenerator.ts
new file mode 100644
index 0000000000..f5a5bd5d51
--- /dev/null
+++ b/packages/docusaurus-plugin-content-docs/src/sidebarItemsGenerator.ts
@@ -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({
+ 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 {
+ function assertCategoryMetadataFile(
+ content: unknown,
+ ): asserts content is CategoryMetadatasFile {
+ Joi.attempt(content, CategoryMetadatasFileSchema);
+ }
+
+ async function tryReadFile(
+ fileNameWithExtension: string,
+ parse: (content: string) => unknown,
+ ): Promise {
+ // 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 {
+ // 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 {
+ 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 = {}; // mutable cache of categories already created
+
+ async function getOrCreateCategoriesForBreadcrumb(
+ breadcrumb: string[],
+ ): Promise {
+ 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 {
+ 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);
+}
diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars.ts b/packages/docusaurus-plugin-content-docs/src/sidebars.ts
index 0f4758d78c..56496e7038 100644
--- a/packages/docusaurus-plugin-content-docs/src/sidebars.ts
+++ b/packages/docusaurus-plugin-content-docs/src/sidebars.ts
@@ -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(
item: Record,
keys: K[],
-): asserts item is Record {
+): asserts item is Record {
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,
+): 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,
): 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 {
+ // Just a minor lazy transformation optimization
+ const getSidebarItemsGeneratorDocsAndVersion = memoize(() => ({
+ docs: docs.map(toSidebarItemsGeneratorDoc),
+ version: toSidebarItemsGeneratorVersion(version),
+ }));
+
+ async function processRecursive(
+ item: UnprocessedSidebarItem,
+ ): Promise {
+ 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 {
+ return combinePromises(
+ mapValues(unprocessedSidebars, (unprocessedSidebar) =>
+ processSidebar({
+ sidebarItemsGenerator,
+ unprocessedSidebar,
+ docs,
+ version,
+ }),
+ ),
+ );
+}
+
function collectSidebarItemsOfType<
Type extends SidebarItemType,
Item extends SidebarItem & {type: SidebarItemType}
diff --git a/packages/docusaurus-plugin-content-docs/src/slug.ts b/packages/docusaurus-plugin-content-docs/src/slug.ts
index 84d0397336..32556a8582 100644
--- a/packages/docusaurus-plugin-content-docs/src/slug.ts
+++ b/packages/docusaurus-plugin-content-docs/src/slug.ts
@@ -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);
}
diff --git a/packages/docusaurus-plugin-content-docs/src/types.ts b/packages/docusaurus-plugin-content-docs/src/types.ts
index 012e75ba0f..3c1633f0f5 100644
--- a/packages/docusaurus-plugin-content-docs/src/types.ts
+++ b/packages/docusaurus-plugin-content-docs/src/types.ts
@@ -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;
+
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;
+// 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;
+
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;
};
diff --git a/packages/docusaurus-plugin-content-docs/src/versions.ts b/packages/docusaurus-plugin-content-docs/src/versions.ts
index 732d9eecd7..de52d2d7d3 100644
--- a/packages/docusaurus-plugin-content-docs/src/versions.ts
+++ b/packages/docusaurus-plugin-content-docs/src/versions.ts
@@ -434,6 +434,7 @@ function filterVersions(
}
}
+// TODO make this async (requires plugin init to be async)
export function readVersionsMetadata({
context,
options,
diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/index.tsx b/packages/docusaurus-theme-classic/src/theme/CodeBlock/index.tsx
index de6f07a928..77bbc902de 100644
--- a/packages/docusaurus-theme-classic/src/theme/CodeBlock/index.tsx
+++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/index.tsx
@@ -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();
diff --git a/packages/docusaurus-theme-classic/src/types.d.ts b/packages/docusaurus-theme-classic/src/types.d.ts
index 29a4b7abaf..a22bfcd7f1 100644
--- a/packages/docusaurus-theme-classic/src/types.d.ts
+++ b/packages/docusaurus-theme-classic/src/types.d.ts
@@ -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;
diff --git a/packages/docusaurus-theme-classic/update-code-translations.test.js b/packages/docusaurus-theme-classic/update-code-translations.test.js
index 80254c10ed..825ea04b6b 100644
--- a/packages/docusaurus-theme-classic/update-code-translations.test.js
+++ b/packages/docusaurus-theme-classic/update-code-translations.test.js
@@ -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(
diff --git a/website/community/support.md b/website/community/0-support.md
similarity index 97%
rename from website/community/support.md
rename to website/community/0-support.md
index 75f7e034b7..1db2482d68 100644
--- a/website/community/support.md
+++ b/website/community/0-support.md
@@ -1,8 +1,4 @@
----
-id: support
-title: Support
-slug: /support
----
+# Support
Docusaurus has a community of thousands of developers.
diff --git a/website/community/team.mdx b/website/community/1-team.mdx
similarity index 97%
rename from website/community/team.mdx
rename to website/community/1-team.mdx
index 31d0a30b88..98bf339e27 100644
--- a/website/community/team.mdx
+++ b/website/community/1-team.mdx
@@ -1,8 +1,4 @@
----
-id: team
-title: Team
-slug: /team
----
+# Team
import {
ActiveTeamRow,
diff --git a/website/community/resources.md b/website/community/2-resources.md
similarity index 97%
rename from website/community/resources.md
rename to website/community/2-resources.md
index b021b869ff..1f0e348420 100644
--- a/website/community/resources.md
+++ b/website/community/2-resources.md
@@ -1,8 +1,4 @@
----
-id: resources
-title: Awesome Resources
-slug: /resources
----
+# Awesome Resources
A curated list of interesting Docusaurus community projects.
diff --git a/website/docs/api/plugins/plugin-content-docs.md b/website/docs/api/plugins/plugin-content-docs.md
index 4f4e62e200..690c2e279e 100644
--- a/website/docs/api/plugins/plugin-content-docs.md
+++ b/website/docs/api/plugins/plugin-content-docs.md
@@ -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 `` and `` in ``, 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 (`//`). Support multiple patterns: `slug: my-doc`, `slug: /my/path/myDoc`, `slug: /`
Example:
diff --git a/website/docs/guides/creating-pages.md b/website/docs/guides/creating-pages.md
index 8e2d405cd8..f29c474e73 100644
--- a/website/docs/guides/creating-pages.md
+++ b/website/docs/guides/creating-pages.md
@@ -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`:
diff --git a/website/docs/guides/docs/sidebar.md b/website/docs/guides/docs/sidebar.md
index ec68bd3ca8..e6fb08f0af 100644
--- a/website/docs/guides/docs/sidebar.md
+++ b/website/docs/guides/docs/sidebar.md
@@ -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/)
},
],
};
```
-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';
+
+
+ {require('!!raw-loader!@site/sidebars.js')
+ .default
+ .split('\n')
+ // remove comments
+ .map((line) => !['#','/*','*'].some(commentPattern => line.trim().startsWith(commentPattern)) && line)
+ .filter(Boolean)
+ .join('\n')}
+
+```
diff --git a/website/docs/i18n/i18n-crowdin.mdx b/website/docs/i18n/i18n-crowdin.mdx
index cb78d51b0f..84b25b9ed8 100644
--- a/website/docs/i18n/i18n-crowdin.mdx
+++ b/website/docs/i18n/i18n-crowdin.mdx
@@ -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';
-
+
{CrowdinConfigV2.split('\n')
// remove comments
.map((line) => !line.startsWith('#') && line)
.filter(Boolean)
.join('\n')}
+```
diff --git a/website/sidebarsCommunity.js b/website/sidebarsCommunity.js
index 7af164c055..e058dfa01a 100644
--- a/website/sidebarsCommunity.js
+++ b/website/sidebarsCommunity.js
@@ -7,9 +7,10 @@
module.exports = {
community: [
- 'support',
- 'team',
- 'resources',
+ {
+ type: 'autogenerated',
+ dirName: '.',
+ },
{
type: 'link',
href: '/showcase',
diff --git a/yarn.lock b/yarn.lock
index 75ad17a0c8..9d789a1229 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"