From 7be1feaa0a6fbfd18faec8fe0488d24a61b15596 Mon Sep 17 00:00:00 2001 From: Rohan Thakur Date: Fri, 2 Aug 2024 22:20:48 +0530 Subject: [PATCH] feat(blog): add feed xlst options to render beautiful RSS and Atom feeds (#9252) Co-authored-by: ozakione <29860391+OzakIOne@users.noreply.github.com> Co-authored-by: sebastien --- .cspell.json | 2 + .../classic-typescript/docusaurus.config.ts | 4 + .../templates/classic/docusaurus.config.js | 4 + .../assets/atom.css | 75 ++ .../assets/atom.xsl | 92 ++ .../assets/rss.css | 75 ++ .../assets/rss.xsl | 86 ++ .../package.json | 3 +- .../__fixtures__/website/blog/custom-atom.css | 76 ++ .../__fixtures__/website/blog/custom-atom.xsl | 65 + .../__fixtures__/website/blog/custom-rss.css | 76 ++ .../__fixtures__/website/blog/custom-rss.xsl | 66 ++ .../website/build-snap/blog/atom.css | 75 ++ .../website/build-snap/blog/atom.xsl | 92 ++ .../website/build-snap/blog/custom-atom.css | 76 ++ .../website/build-snap/blog/custom-atom.xsl | 65 + .../website/build-snap/blog/custom-rss.css | 76 ++ .../website/build-snap/blog/custom-rss.xsl | 66 ++ .../website/build-snap/blog/rss.css | 75 ++ .../website/build-snap/blog/rss.xsl | 86 ++ .../__tests__/__snapshots__/feed.test.ts.snap | 1050 ++++++++++++++++- .../__snapshots__/options.test.ts.snap | 2 +- .../src/__tests__/feed.test.ts | 131 +- .../src/__tests__/options.test.ts | 235 +++- .../src/feed.ts | 157 ++- .../src/index.ts | 1 + .../src/options.ts | 132 ++- .../src/plugin-content-blog.d.ts | 26 + project-words.txt | 2 + .../_dogfooding/_blog tests/custom-atom.css | 76 ++ .../_dogfooding/_blog tests/custom-atom.xsl | 94 ++ .../_dogfooding/_blog tests/custom-rss.css | 76 ++ .../_dogfooding/_blog tests/custom-rss.xsl | 92 ++ website/_dogfooding/dogfooding.config.ts | 4 + .../docs/api/plugins/plugin-content-blog.mdx | 20 + website/docs/blog.mdx | 11 +- website/docusaurus.config.ts | 3 + 37 files changed, 3229 insertions(+), 118 deletions(-) create mode 100644 packages/docusaurus-plugin-content-blog/assets/atom.css create mode 100644 packages/docusaurus-plugin-content-blog/assets/atom.xsl create mode 100644 packages/docusaurus-plugin-content-blog/assets/rss.css create mode 100644 packages/docusaurus-plugin-content-blog/assets/rss.xsl create mode 100644 packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/custom-atom.css create mode 100644 packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/custom-atom.xsl create mode 100644 packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/custom-rss.css create mode 100644 packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/custom-rss.xsl create mode 100644 packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/atom.css create mode 100644 packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/atom.xsl create mode 100644 packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/custom-atom.css create mode 100644 packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/custom-atom.xsl create mode 100644 packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/custom-rss.css create mode 100644 packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/custom-rss.xsl create mode 100644 packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/rss.css create mode 100644 packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/rss.xsl create mode 100644 website/_dogfooding/_blog tests/custom-atom.css create mode 100644 website/_dogfooding/_blog tests/custom-atom.xsl create mode 100644 website/_dogfooding/_blog tests/custom-rss.css create mode 100644 website/_dogfooding/_blog tests/custom-rss.xsl diff --git a/.cspell.json b/.cspell.json index ed3031458ccf..82ebc2c41a41 100644 --- a/.cspell.json +++ b/.cspell.json @@ -32,6 +32,8 @@ "website/_dogfooding/_pages tests/diagrams.mdx", "*.xyz", "*.docx", + "*.xsl", + "*.xslt", "*.gitignore", "versioned_docs", "*.min.*", diff --git a/packages/create-docusaurus/templates/classic-typescript/docusaurus.config.ts b/packages/create-docusaurus/templates/classic-typescript/docusaurus.config.ts index 4058eac59e23..ae7cbf502e05 100644 --- a/packages/create-docusaurus/templates/classic-typescript/docusaurus.config.ts +++ b/packages/create-docusaurus/templates/classic-typescript/docusaurus.config.ts @@ -42,6 +42,10 @@ const config: Config = { }, blog: { showReadingTime: true, + feedOptions: { + type: ['rss', 'atom'], + xslt: true, + }, // Please change this to your repo. // Remove this to remove the "edit this page" links. editUrl: diff --git a/packages/create-docusaurus/templates/classic/docusaurus.config.js b/packages/create-docusaurus/templates/classic/docusaurus.config.js index 7b8d5b1c7f11..d3b51858f497 100644 --- a/packages/create-docusaurus/templates/classic/docusaurus.config.js +++ b/packages/create-docusaurus/templates/classic/docusaurus.config.js @@ -48,6 +48,10 @@ const config = { }, blog: { showReadingTime: true, + feedOptions: { + type: ['rss', 'atom'], + xslt: true, + }, // Please change this to your repo. // Remove this to remove the "edit this page" links. editUrl: diff --git a/packages/docusaurus-plugin-content-blog/assets/atom.css b/packages/docusaurus-plugin-content-blog/assets/atom.css new file mode 100644 index 000000000000..d2fc20b27416 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/assets/atom.css @@ -0,0 +1,75 @@ +/** + * 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. + */ + +main { + flex: 1 0 auto; + width: 100%; + margin: 2rem auto; + max-width: 800px; + /* stylelint-disable-next-line font-family-name-quotes */ + font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell; +} + +.info { + display: block; + margin: 2rem 0; + padding: 1.6rem 2.4rem; + border: 1px solid dodgerblue; + border-left-width: 0.5rem; + border-radius: 0.4rem; + background-color: #edf5ff; +} + +a { + color: #005aff; + text-decoration: none; +} + +h1 { + text-wrap: balance; + font-size: 3.4rem; + font-weight: 800; + margin-bottom: 2rem; + display: flex; + align-items: center; +} + +h1 .rss-icon { + height: 3.2rem; + width: 3.2rem; + margin-right: 1rem; +} + +h2 { + font-size: 2.2rem; + font-weight: 700; + margin-bottom: 0.2rem; +} + +h3 { + font-size: 1.8rem; + font-weight: 700; + margin-bottom: 0.1rem; +} + +.blog-description { + font-size: 1.4rem; + margin-bottom: 0.6rem; +} + +.blog-post-date { + font-size: 1rem; + line-height: 1.4rem; + font-style: italic; + color: #797b7e; +} + +.blog-post-description { + font-size: 1rem; + line-height: 1.4rem; + color: #434349; +} diff --git a/packages/docusaurus-plugin-content-blog/assets/atom.xsl b/packages/docusaurus-plugin-content-blog/assets/atom.xsl new file mode 100644 index 000000000000..271895cf7775 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/assets/atom.xsl @@ -0,0 +1,92 @@ + + + + + + + + Atom Feed | <xsl:value-of + select="atom:feed/atom:title" + /> + + + +
+
+
+ This is an Atom feed. Subscribe by copying the URL + from the address bar into your newsreader. Visit + About Feeds to learn more + and get started. It’s free. +
+

+
+ + + + + + + + + + +
+ +

+

+ +

+
+

Recent Posts

+
+ +
+

+ +
+ +
+
+
+
+
+ + +
+
diff --git a/packages/docusaurus-plugin-content-blog/assets/rss.css b/packages/docusaurus-plugin-content-blog/assets/rss.css new file mode 100644 index 000000000000..d2fc20b27416 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/assets/rss.css @@ -0,0 +1,75 @@ +/** + * 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. + */ + +main { + flex: 1 0 auto; + width: 100%; + margin: 2rem auto; + max-width: 800px; + /* stylelint-disable-next-line font-family-name-quotes */ + font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell; +} + +.info { + display: block; + margin: 2rem 0; + padding: 1.6rem 2.4rem; + border: 1px solid dodgerblue; + border-left-width: 0.5rem; + border-radius: 0.4rem; + background-color: #edf5ff; +} + +a { + color: #005aff; + text-decoration: none; +} + +h1 { + text-wrap: balance; + font-size: 3.4rem; + font-weight: 800; + margin-bottom: 2rem; + display: flex; + align-items: center; +} + +h1 .rss-icon { + height: 3.2rem; + width: 3.2rem; + margin-right: 1rem; +} + +h2 { + font-size: 2.2rem; + font-weight: 700; + margin-bottom: 0.2rem; +} + +h3 { + font-size: 1.8rem; + font-weight: 700; + margin-bottom: 0.1rem; +} + +.blog-description { + font-size: 1.4rem; + margin-bottom: 0.6rem; +} + +.blog-post-date { + font-size: 1rem; + line-height: 1.4rem; + font-style: italic; + color: #797b7e; +} + +.blog-post-description { + font-size: 1rem; + line-height: 1.4rem; + color: #434349; +} diff --git a/packages/docusaurus-plugin-content-blog/assets/rss.xsl b/packages/docusaurus-plugin-content-blog/assets/rss.xsl new file mode 100644 index 000000000000..e9695b298468 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/assets/rss.xsl @@ -0,0 +1,86 @@ + + + + + + + + RSS Feed | <xsl:value-of select="rss/channel/title" /> + + + +
+
+
+ This is an RSS feed. Subscribe by copying the URL + from the address bar into your newsreader. Visit + About Feeds to learn more + and get started. It’s free. +
+

+
+ + + + + + + + + + +
+ +

+

+ +

+
+

Recent Posts

+
+ +
+

+ +
+ +
+
+
+
+
+ + +
+
diff --git a/packages/docusaurus-plugin-content-blog/package.json b/packages/docusaurus-plugin-content-blog/package.json index 815413ff2cfd..ff88860d2ae4 100644 --- a/packages/docusaurus-plugin-content-blog/package.json +++ b/packages/docusaurus-plugin-content-blog/package.json @@ -59,6 +59,7 @@ "node": ">=18.0" }, "devDependencies": { - "@total-typescript/shoehorn": "^0.1.2" + "@total-typescript/shoehorn": "^0.1.2", + "tree-node-cli": "^1.6.0" } } diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/custom-atom.css b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/custom-atom.css new file mode 100644 index 000000000000..c016178d9007 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/custom-atom.css @@ -0,0 +1,76 @@ +/** + * 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. + */ + +* { + color: #0d1137; +} + +main { + flex: 1 0 auto; + width: 100%; + margin: 4rem auto; + padding: 1.5 rem; + max-width: 800px; + /* stylelint-disable-next-line font-family-name-quotes */ + font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell; +} + +.info { + display: block; + margin: 3rem 0; + padding: 2rem 3rem; + border: 1px solid dodgerblue; + border-left-width: 0.5rem; + border-radius: 0.4rem; + background-color: #e52165; +} + +.rss-icon { + height: 3.8rem; + width: 3.8rem; + margin-right: 1rem; +} + +.flex { + display: flex; +} + +.items-start { + align-items: flex-start; +} + +.pb-7 { + padding-bottom: 3rem; +} + +a { + color: #005aff; + text-decoration: none; +} + +h1 { + text-wrap: balance; + font-size: 3.8rem; + line-height: 1; + font-weight: 800; + margin-bottom: 4rem; +} + +h2 { + font-size: 3rem; + line-height: 1.2; + font-weight: 700; + margin-bottom: 3rem; +} + +h2:not(:first-child) { + margin-top: 5.8rem; +} + +.italic { + font-style: italic; +} diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/custom-atom.xsl b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/custom-atom.xsl new file mode 100644 index 000000000000..80406b153124 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/custom-atom.xsl @@ -0,0 +1,65 @@ + + + + + + + + + Atom Feed | <xsl:value-of select="atom:feed/atom:title" /> + + + +
+
+
+ This is an Atom feed. Subscribe by copying the URL from the address + bar into your newsreader. Visit About Feeds to learn more + and get started. It’s free.
+

+
+ + + + + + + + + + +
+ Custom Atom Feed Preview

+

+ +

+

Description:

+
+

Recent Posts

+
+ +
+ + + +
Published on +
+
+ +
+
+
+
+
+ + +
+ +
diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/custom-rss.css b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/custom-rss.css new file mode 100644 index 000000000000..c016178d9007 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/custom-rss.css @@ -0,0 +1,76 @@ +/** + * 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. + */ + +* { + color: #0d1137; +} + +main { + flex: 1 0 auto; + width: 100%; + margin: 4rem auto; + padding: 1.5 rem; + max-width: 800px; + /* stylelint-disable-next-line font-family-name-quotes */ + font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell; +} + +.info { + display: block; + margin: 3rem 0; + padding: 2rem 3rem; + border: 1px solid dodgerblue; + border-left-width: 0.5rem; + border-radius: 0.4rem; + background-color: #e52165; +} + +.rss-icon { + height: 3.8rem; + width: 3.8rem; + margin-right: 1rem; +} + +.flex { + display: flex; +} + +.items-start { + align-items: flex-start; +} + +.pb-7 { + padding-bottom: 3rem; +} + +a { + color: #005aff; + text-decoration: none; +} + +h1 { + text-wrap: balance; + font-size: 3.8rem; + line-height: 1; + font-weight: 800; + margin-bottom: 4rem; +} + +h2 { + font-size: 3rem; + line-height: 1.2; + font-weight: 700; + margin-bottom: 3rem; +} + +h2:not(:first-child) { + margin-top: 5.8rem; +} + +.italic { + font-style: italic; +} diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/custom-rss.xsl b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/custom-rss.xsl new file mode 100644 index 000000000000..4d793963cba1 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/custom-rss.xsl @@ -0,0 +1,66 @@ + + + + + + + + + RSS Feed | <xsl:value-of select="rss/channel/title" /> + + + +
+
+
+ This is an RSS feed. Subscribe by copying the URL from the address + bar into your newsreader. Visit About Feeds to learn more + and get started. It’s free.
+

+
+ + + + + + + + + + +
+ Custom RSS Feed Preview

+

+ +

+

Description:

+
+

Recent Posts

+
+ +
+ + + +
Published on +
+
+ +
+
+
+
+
+ + +
+ +
diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/atom.css b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/atom.css new file mode 100644 index 000000000000..d2fc20b27416 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/atom.css @@ -0,0 +1,75 @@ +/** + * 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. + */ + +main { + flex: 1 0 auto; + width: 100%; + margin: 2rem auto; + max-width: 800px; + /* stylelint-disable-next-line font-family-name-quotes */ + font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell; +} + +.info { + display: block; + margin: 2rem 0; + padding: 1.6rem 2.4rem; + border: 1px solid dodgerblue; + border-left-width: 0.5rem; + border-radius: 0.4rem; + background-color: #edf5ff; +} + +a { + color: #005aff; + text-decoration: none; +} + +h1 { + text-wrap: balance; + font-size: 3.4rem; + font-weight: 800; + margin-bottom: 2rem; + display: flex; + align-items: center; +} + +h1 .rss-icon { + height: 3.2rem; + width: 3.2rem; + margin-right: 1rem; +} + +h2 { + font-size: 2.2rem; + font-weight: 700; + margin-bottom: 0.2rem; +} + +h3 { + font-size: 1.8rem; + font-weight: 700; + margin-bottom: 0.1rem; +} + +.blog-description { + font-size: 1.4rem; + margin-bottom: 0.6rem; +} + +.blog-post-date { + font-size: 1rem; + line-height: 1.4rem; + font-style: italic; + color: #797b7e; +} + +.blog-post-description { + font-size: 1rem; + line-height: 1.4rem; + color: #434349; +} diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/atom.xsl b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/atom.xsl new file mode 100644 index 000000000000..271895cf7775 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/atom.xsl @@ -0,0 +1,92 @@ + + + + + + + + Atom Feed | <xsl:value-of + select="atom:feed/atom:title" + /> + + + +
+
+
+ This is an Atom feed. Subscribe by copying the URL + from the address bar into your newsreader. Visit + About Feeds to learn more + and get started. It’s free. +
+

+
+ + + + + + + + + + +
+ +

+

+ +

+
+

Recent Posts

+
+ +
+

+ +
+ +
+
+
+
+
+ + +
+
diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/custom-atom.css b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/custom-atom.css new file mode 100644 index 000000000000..c016178d9007 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/custom-atom.css @@ -0,0 +1,76 @@ +/** + * 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. + */ + +* { + color: #0d1137; +} + +main { + flex: 1 0 auto; + width: 100%; + margin: 4rem auto; + padding: 1.5 rem; + max-width: 800px; + /* stylelint-disable-next-line font-family-name-quotes */ + font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell; +} + +.info { + display: block; + margin: 3rem 0; + padding: 2rem 3rem; + border: 1px solid dodgerblue; + border-left-width: 0.5rem; + border-radius: 0.4rem; + background-color: #e52165; +} + +.rss-icon { + height: 3.8rem; + width: 3.8rem; + margin-right: 1rem; +} + +.flex { + display: flex; +} + +.items-start { + align-items: flex-start; +} + +.pb-7 { + padding-bottom: 3rem; +} + +a { + color: #005aff; + text-decoration: none; +} + +h1 { + text-wrap: balance; + font-size: 3.8rem; + line-height: 1; + font-weight: 800; + margin-bottom: 4rem; +} + +h2 { + font-size: 3rem; + line-height: 1.2; + font-weight: 700; + margin-bottom: 3rem; +} + +h2:not(:first-child) { + margin-top: 5.8rem; +} + +.italic { + font-style: italic; +} diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/custom-atom.xsl b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/custom-atom.xsl new file mode 100644 index 000000000000..80406b153124 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/custom-atom.xsl @@ -0,0 +1,65 @@ + + + + + + + + + Atom Feed | <xsl:value-of select="atom:feed/atom:title" /> + + + +
+
+
+ This is an Atom feed. Subscribe by copying the URL from the address + bar into your newsreader. Visit About Feeds to learn more + and get started. It’s free.
+

+
+ + + + + + + + + + +
+ Custom Atom Feed Preview

+

+ +

+

Description:

+
+

Recent Posts

+
+ +
+ + + +
Published on +
+
+ +
+
+
+
+
+ + +
+ +
diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/custom-rss.css b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/custom-rss.css new file mode 100644 index 000000000000..c016178d9007 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/custom-rss.css @@ -0,0 +1,76 @@ +/** + * 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. + */ + +* { + color: #0d1137; +} + +main { + flex: 1 0 auto; + width: 100%; + margin: 4rem auto; + padding: 1.5 rem; + max-width: 800px; + /* stylelint-disable-next-line font-family-name-quotes */ + font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell; +} + +.info { + display: block; + margin: 3rem 0; + padding: 2rem 3rem; + border: 1px solid dodgerblue; + border-left-width: 0.5rem; + border-radius: 0.4rem; + background-color: #e52165; +} + +.rss-icon { + height: 3.8rem; + width: 3.8rem; + margin-right: 1rem; +} + +.flex { + display: flex; +} + +.items-start { + align-items: flex-start; +} + +.pb-7 { + padding-bottom: 3rem; +} + +a { + color: #005aff; + text-decoration: none; +} + +h1 { + text-wrap: balance; + font-size: 3.8rem; + line-height: 1; + font-weight: 800; + margin-bottom: 4rem; +} + +h2 { + font-size: 3rem; + line-height: 1.2; + font-weight: 700; + margin-bottom: 3rem; +} + +h2:not(:first-child) { + margin-top: 5.8rem; +} + +.italic { + font-style: italic; +} diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/custom-rss.xsl b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/custom-rss.xsl new file mode 100644 index 000000000000..4d793963cba1 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/custom-rss.xsl @@ -0,0 +1,66 @@ + + + + + + + + + RSS Feed | <xsl:value-of select="rss/channel/title" /> + + + +
+
+
+ This is an RSS feed. Subscribe by copying the URL from the address + bar into your newsreader. Visit About Feeds to learn more + and get started. It’s free.
+

+
+ + + + + + + + + + +
+ Custom RSS Feed Preview

+

+ +

+

Description:

+
+

Recent Posts

+
+ +
+ + + +
Published on +
+
+ +
+
+
+
+
+ + +
+ +
diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/rss.css b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/rss.css new file mode 100644 index 000000000000..d2fc20b27416 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/rss.css @@ -0,0 +1,75 @@ +/** + * 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. + */ + +main { + flex: 1 0 auto; + width: 100%; + margin: 2rem auto; + max-width: 800px; + /* stylelint-disable-next-line font-family-name-quotes */ + font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell; +} + +.info { + display: block; + margin: 2rem 0; + padding: 1.6rem 2.4rem; + border: 1px solid dodgerblue; + border-left-width: 0.5rem; + border-radius: 0.4rem; + background-color: #edf5ff; +} + +a { + color: #005aff; + text-decoration: none; +} + +h1 { + text-wrap: balance; + font-size: 3.4rem; + font-weight: 800; + margin-bottom: 2rem; + display: flex; + align-items: center; +} + +h1 .rss-icon { + height: 3.2rem; + width: 3.2rem; + margin-right: 1rem; +} + +h2 { + font-size: 2.2rem; + font-weight: 700; + margin-bottom: 0.2rem; +} + +h3 { + font-size: 1.8rem; + font-weight: 700; + margin-bottom: 0.1rem; +} + +.blog-description { + font-size: 1.4rem; + margin-bottom: 0.6rem; +} + +.blog-post-date { + font-size: 1rem; + line-height: 1.4rem; + font-style: italic; + color: #797b7e; +} + +.blog-post-description { + font-size: 1rem; + line-height: 1.4rem; + color: #434349; +} diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/rss.xsl b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/rss.xsl new file mode 100644 index 000000000000..e9695b298468 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/rss.xsl @@ -0,0 +1,86 @@ + + + + + + + + RSS Feed | <xsl:value-of select="rss/channel/title" /> + + + +
+
+
+ This is an RSS feed. Subscribe by copying the URL + from the address bar into your newsreader. Visit + About Feeds to learn more + and get started. It’s free. +
+

+
+ + + + + + + + + + +
+ +

+

+ +

+
+

Recent Posts

+
+ +
+

+ +
+ +
+
+
+
+
+ + +
+
diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/feed.test.ts.snap b/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/feed.test.ts.snap index 6b693d83fb2e..2c90688e2c56 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/feed.test.ts.snap +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/feed.test.ts.snap @@ -92,6 +92,192 @@ exports[`atom filters to the first two entries using limit 1`] = ` ] `; +exports[`atom has custom xslt files for feed 1`] = ` +[ + [ + "/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/atom.xml", + " + + https://docusaurus.io/myBaseUrl/blog + Hello Blog + 2023-07-23T00:00:00.000Z + https://github.com/jpmonette/feed + + Hello Blog + https://docusaurus.io/myBaseUrl/image/favicon.ico + Copyright + + <![CDATA[test links]]> + https://docusaurus.io/myBaseUrl/blog/blog-with-links + + 2023-07-23T00:00:00.000Z + + absolute full url

+

absolute pathname

+

relative pathname

+

md link

+

anchor

+

relative pathname + anchor

+

+

+ +]]>
+
+ + <![CDATA[MDX Blog Sample with require calls]]> + https://docusaurus.io/myBaseUrl/blog/mdx-require-blog-post + + 2021-03-06T00:00:00.000Z + + Test MDX with require calls

+ + + + +]]>
+
+ + <![CDATA[Full Blog Sample]]> + https://docusaurus.io/myBaseUrl/blog/mdx-blog-post + + 2021-03-05T00:00:00.000Z + + HTML Heading 1 +

HTML Heading 2

+

HTML Paragraph

+ + +

Import DOM

+

Heading 1

+

Heading 2

+

Heading 3

+

Heading 4

+
Heading 5
+
    +
  • list1
  • +
  • list2
  • +
  • list3
  • +
+
    +
  • list1
  • +
  • list2
  • +
  • list3
  • +
+

Normal Text Italics Text Bold Text

+

link image

]]>
+
+ + <![CDATA[Complex Slug]]> + https://docusaurus.io/myBaseUrl/blog/hey/my super path/héllô + + 2020-08-16T00:00:00.000Z + + complex url slug

]]>
+ + +
+ + <![CDATA[Simple Slug]]> + https://docusaurus.io/myBaseUrl/blog/simple/slug + + 2020-08-15T00:00:00.000Z + + simple url slug

]]>
+ + Sébastien Lorber + https://sebastienlorber.com + +
+ + <![CDATA[some heading]]> + https://docusaurus.io/myBaseUrl/blog/heading-as-title + + 2019-01-02T00:00:00.000Z + + + <![CDATA[date-matter]]> + https://docusaurus.io/myBaseUrl/blog/date-matter + + 2019-01-01T00:00:00.000Z + + date inside front matter

]]>
+ +
+ + <![CDATA[Happy 1st Birthday Slash! (translated)]]> + https://docusaurus.io/myBaseUrl/blog/2018/12/14/Happy-First-Birthday-Slash + + 2018-12-14T00:00:00.000Z + + Happy birthday! (translated)

]]>
+ + Yangshun Tay (translated) + + + Sébastien Lorber (translated) + lorber.sebastien@gmail.com + + + +
+
", + ], +] +`; + +exports[`atom has custom xslt files for feed: blog tree 1`] = ` +"blog +├── 2018 +│ └── 12 +│ └── 14 +│ └── Happy-First-Birthday-Slash +│ └── index.html +├── archive +│ └── index.html +├── atom.css +├── atom.xml +├── atom.xsl +├── blog-with-links +│ └── index.html +├── custom-atom.css +├── custom-atom.xsl +├── custom-rss.css +├── custom-rss.xsl +├── date-matter +│ └── index.html +├── feed.json +├── heading-as-title +│ └── index.html +├── hey +│ └── my super path +│ └── héllô +│ └── index.html +├── index.html +├── mdx-blog-post +│ └── index.html +├── mdx-require-blog-post +│ └── index.html +├── page +│ ├── 2 +│ │ └── index.html +│ └── 3 +│ └── index.html +├── rss.css +├── rss.xml +├── rss.xsl +├── simple +│ └── slug +│ └── index.html +├── tags +│ ├── complex +│ │ └── index.html +│ ├── date +│ │ └── index.html +│ └── index.html +└── unlisted + └── index.html" +`; + exports[`atom has feed item for each post - with trailing slash 1`] = ` [ " @@ -352,6 +538,192 @@ exports[`atom has feed item for each post 1`] = ` ] `; +exports[`atom has xslt files for feed 1`] = ` +[ + [ + "/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/atom.xml", + " + + https://docusaurus.io/myBaseUrl/blog + Hello Blog + 2023-07-23T00:00:00.000Z + https://github.com/jpmonette/feed + + Hello Blog + https://docusaurus.io/myBaseUrl/image/favicon.ico + Copyright + + <![CDATA[test links]]> + https://docusaurus.io/myBaseUrl/blog/blog-with-links + + 2023-07-23T00:00:00.000Z + + absolute full url

+

absolute pathname

+

relative pathname

+

md link

+

anchor

+

relative pathname + anchor

+

+

+ +]]>
+
+ + <![CDATA[MDX Blog Sample with require calls]]> + https://docusaurus.io/myBaseUrl/blog/mdx-require-blog-post + + 2021-03-06T00:00:00.000Z + + Test MDX with require calls

+ + + + +]]>
+
+ + <![CDATA[Full Blog Sample]]> + https://docusaurus.io/myBaseUrl/blog/mdx-blog-post + + 2021-03-05T00:00:00.000Z + + HTML Heading 1 +

HTML Heading 2

+

HTML Paragraph

+ + +

Import DOM

+

Heading 1

+

Heading 2

+

Heading 3

+

Heading 4

+
Heading 5
+
    +
  • list1
  • +
  • list2
  • +
  • list3
  • +
+
    +
  • list1
  • +
  • list2
  • +
  • list3
  • +
+

Normal Text Italics Text Bold Text

+

link image

]]>
+
+ + <![CDATA[Complex Slug]]> + https://docusaurus.io/myBaseUrl/blog/hey/my super path/héllô + + 2020-08-16T00:00:00.000Z + + complex url slug

]]>
+ + +
+ + <![CDATA[Simple Slug]]> + https://docusaurus.io/myBaseUrl/blog/simple/slug + + 2020-08-15T00:00:00.000Z + + simple url slug

]]>
+ + Sébastien Lorber + https://sebastienlorber.com + +
+ + <![CDATA[some heading]]> + https://docusaurus.io/myBaseUrl/blog/heading-as-title + + 2019-01-02T00:00:00.000Z + + + <![CDATA[date-matter]]> + https://docusaurus.io/myBaseUrl/blog/date-matter + + 2019-01-01T00:00:00.000Z + + date inside front matter

]]>
+ +
+ + <![CDATA[Happy 1st Birthday Slash! (translated)]]> + https://docusaurus.io/myBaseUrl/blog/2018/12/14/Happy-First-Birthday-Slash + + 2018-12-14T00:00:00.000Z + + Happy birthday! (translated)

]]>
+ + Yangshun Tay (translated) + + + Sébastien Lorber (translated) + lorber.sebastien@gmail.com + + + +
+
", + ], +] +`; + +exports[`atom has xslt files for feed: blog tree 1`] = ` +"blog +├── 2018 +│ └── 12 +│ └── 14 +│ └── Happy-First-Birthday-Slash +│ └── index.html +├── archive +│ └── index.html +├── atom.css +├── atom.xml +├── atom.xsl +├── blog-with-links +│ └── index.html +├── custom-atom.css +├── custom-atom.xsl +├── custom-rss.css +├── custom-rss.xsl +├── date-matter +│ └── index.html +├── feed.json +├── heading-as-title +│ └── index.html +├── hey +│ └── my super path +│ └── héllô +│ └── index.html +├── index.html +├── mdx-blog-post +│ └── index.html +├── mdx-require-blog-post +│ └── index.html +├── page +│ ├── 2 +│ │ └── index.html +│ └── 3 +│ └── index.html +├── rss.css +├── rss.xml +├── rss.xsl +├── simple +│ └── slug +│ └── index.html +├── tags +│ ├── complex +│ │ └── index.html +│ ├── date +│ │ └── index.html +│ └── index.html +└── unlisted + └── index.html" +`; + exports[`json filters to the first two entries 1`] = ` [ "{ @@ -414,6 +786,161 @@ exports[`json filters to the first two entries using limit 1`] = ` ] `; +exports[`json has custom xslt files for feed 1`] = ` +[ + [ + "/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/feed.json", + "{ + "version": "https://jsonfeed.org/version/1", + "title": "Hello Blog", + "home_page_url": "https://docusaurus.io/myBaseUrl/blog", + "description": "Hello Blog", + "items": [ + { + "id": "https://docusaurus.io/myBaseUrl/blog/blog-with-links", + "content_html": "

absolute full url

/n

absolute pathname

/n

relative pathname

/n

md link

/n

anchor

/n

relative pathname + anchor

/n

/n

\\"\\"

/n/n", + "url": "https://docusaurus.io/myBaseUrl/blog/blog-with-links", + "title": "test links", + "summary": "absolute full url", + "date_modified": "2023-07-23T00:00:00.000Z", + "tags": [] + }, + { + "id": "https://docusaurus.io/myBaseUrl/blog/mdx-require-blog-post", + "content_html": "

Test MDX with require calls

/n/n/n/n/n", + "url": "https://docusaurus.io/myBaseUrl/blog/mdx-require-blog-post", + "title": "MDX Blog Sample with require calls", + "summary": "Test MDX with require calls", + "date_modified": "2021-03-06T00:00:00.000Z", + "tags": [] + }, + { + "id": "https://docusaurus.io/myBaseUrl/blog/mdx-blog-post", + "content_html": "

HTML Heading 1

/n

HTML Heading 2

/n

HTML Paragraph

/n/n/n

Import DOM

/n

Heading 1

/n

Heading 2

/n

Heading 3

/n

Heading 4

/n
Heading 5
/n
    /n
  • list1
  • /n
  • list2
  • /n
  • list3
  • /n
/n
    /n
  • list1
  • /n
  • list2
  • /n
  • list3
  • /n
/n

Normal Text Italics Text Bold Text

/n

link \\"image\\"

", + "url": "https://docusaurus.io/myBaseUrl/blog/mdx-blog-post", + "title": "Full Blog Sample", + "summary": "HTML Heading 1", + "date_modified": "2021-03-05T00:00:00.000Z", + "tags": [] + }, + { + "id": "https://docusaurus.io/myBaseUrl/blog/hey/my super path/héllô", + "content_html": "

complex url slug

", + "url": "https://docusaurus.io/myBaseUrl/blog/hey/my super path/héllô", + "title": "Complex Slug", + "summary": "complex url slug", + "date_modified": "2020-08-16T00:00:00.000Z", + "tags": [ + "date", + "complex" + ] + }, + { + "id": "https://docusaurus.io/myBaseUrl/blog/simple/slug", + "content_html": "

simple url slug

", + "url": "https://docusaurus.io/myBaseUrl/blog/simple/slug", + "title": "Simple Slug", + "summary": "simple url slug", + "date_modified": "2020-08-15T00:00:00.000Z", + "author": { + "name": "Sébastien Lorber", + "url": "https://sebastienlorber.com" + }, + "tags": [] + }, + { + "id": "https://docusaurus.io/myBaseUrl/blog/heading-as-title", + "content_html": "", + "url": "https://docusaurus.io/myBaseUrl/blog/heading-as-title", + "title": "some heading", + "date_modified": "2019-01-02T00:00:00.000Z", + "tags": [] + }, + { + "id": "https://docusaurus.io/myBaseUrl/blog/date-matter", + "content_html": "

date inside front matter

", + "url": "https://docusaurus.io/myBaseUrl/blog/date-matter", + "title": "date-matter", + "summary": "date inside front matter", + "date_modified": "2019-01-01T00:00:00.000Z", + "tags": [ + "date" + ] + }, + { + "id": "https://docusaurus.io/myBaseUrl/blog/2018/12/14/Happy-First-Birthday-Slash", + "content_html": "

Happy birthday! (translated)

", + "url": "https://docusaurus.io/myBaseUrl/blog/2018/12/14/Happy-First-Birthday-Slash", + "title": "Happy 1st Birthday Slash! (translated)", + "summary": "Happy birthday! (translated)", + "date_modified": "2018-12-14T00:00:00.000Z", + "author": { + "name": "Yangshun Tay (translated)" + }, + "tags": [ + "inlineTag", + "Global Tag label (en)" + ] + } + ] +}", + ], +] +`; + +exports[`json has custom xslt files for feed: blog tree 1`] = ` +"blog +├── 2018 +│ └── 12 +│ └── 14 +│ └── Happy-First-Birthday-Slash +│ └── index.html +├── archive +│ └── index.html +├── atom.css +├── atom.xml +├── atom.xsl +├── blog-with-links +│ └── index.html +├── custom-atom.css +├── custom-atom.xsl +├── custom-rss.css +├── custom-rss.xsl +├── date-matter +│ └── index.html +├── feed.json +├── heading-as-title +│ └── index.html +├── hey +│ └── my super path +│ └── héllô +│ └── index.html +├── index.html +├── mdx-blog-post +│ └── index.html +├── mdx-require-blog-post +│ └── index.html +├── page +│ ├── 2 +│ │ └── index.html +│ └── 3 +│ └── index.html +├── rss.css +├── rss.xml +├── rss.xsl +├── simple +│ └── slug +│ └── index.html +├── tags +│ ├── complex +│ │ └── index.html +│ ├── date +│ │ └── index.html +│ └── index.html +└── unlisted + └── index.html" +`; + exports[`json has feed item for each post - with trailing slash 1`] = ` [ "{ @@ -483,9 +1010,108 @@ exports[`json has feed item for each post - with trailing slash 1`] = ` "tags": [] }, { - "id": "https://docusaurus.io/myBaseUrl/blog/date-matter/", + "id": "https://docusaurus.io/myBaseUrl/blog/date-matter/", + "content_html": "

date inside front matter

", + "url": "https://docusaurus.io/myBaseUrl/blog/date-matter/", + "title": "date-matter", + "summary": "date inside front matter", + "date_modified": "2019-01-01T00:00:00.000Z", + "tags": [ + "date" + ] + }, + { + "id": "https://docusaurus.io/myBaseUrl/blog/2018/12/14/Happy-First-Birthday-Slash/", + "content_html": "

Happy birthday! (translated)

", + "url": "https://docusaurus.io/myBaseUrl/blog/2018/12/14/Happy-First-Birthday-Slash/", + "title": "Happy 1st Birthday Slash! (translated)", + "summary": "Happy birthday! (translated)", + "date_modified": "2018-12-14T00:00:00.000Z", + "author": { + "name": "Yangshun Tay (translated)" + }, + "tags": [ + "inlineTag", + "Global Tag label (en)" + ] + } + ] +}", +] +`; + +exports[`json has feed item for each post 1`] = ` +[ + "{ + "version": "https://jsonfeed.org/version/1", + "title": "Hello Blog", + "home_page_url": "https://docusaurus.io/myBaseUrl/blog", + "description": "Hello Blog", + "items": [ + { + "id": "https://docusaurus.io/myBaseUrl/blog/blog-with-links", + "content_html": "

absolute full url

/n

absolute pathname

/n

relative pathname

/n

md link

/n

anchor

/n

relative pathname + anchor

/n

/n

\\"\\"

/n/n", + "url": "https://docusaurus.io/myBaseUrl/blog/blog-with-links", + "title": "test links", + "summary": "absolute full url", + "date_modified": "2023-07-23T00:00:00.000Z", + "tags": [] + }, + { + "id": "https://docusaurus.io/myBaseUrl/blog/mdx-require-blog-post", + "content_html": "

Test MDX with require calls

/n/n/n/n/n", + "url": "https://docusaurus.io/myBaseUrl/blog/mdx-require-blog-post", + "title": "MDX Blog Sample with require calls", + "summary": "Test MDX with require calls", + "date_modified": "2021-03-06T00:00:00.000Z", + "tags": [] + }, + { + "id": "https://docusaurus.io/myBaseUrl/blog/mdx-blog-post", + "content_html": "

HTML Heading 1

/n

HTML Heading 2

/n

HTML Paragraph

/n/n/n

Import DOM

/n

Heading 1

/n

Heading 2

/n

Heading 3

/n

Heading 4

/n
Heading 5
/n
    /n
  • list1
  • /n
  • list2
  • /n
  • list3
  • /n
/n
    /n
  • list1
  • /n
  • list2
  • /n
  • list3
  • /n
/n

Normal Text Italics Text Bold Text

/n

link \\"image\\"

", + "url": "https://docusaurus.io/myBaseUrl/blog/mdx-blog-post", + "title": "Full Blog Sample", + "summary": "HTML Heading 1", + "date_modified": "2021-03-05T00:00:00.000Z", + "tags": [] + }, + { + "id": "https://docusaurus.io/myBaseUrl/blog/hey/my super path/héllô", + "content_html": "

complex url slug

", + "url": "https://docusaurus.io/myBaseUrl/blog/hey/my super path/héllô", + "title": "Complex Slug", + "summary": "complex url slug", + "date_modified": "2020-08-16T00:00:00.000Z", + "tags": [ + "date", + "complex" + ] + }, + { + "id": "https://docusaurus.io/myBaseUrl/blog/simple/slug", + "content_html": "

simple url slug

", + "url": "https://docusaurus.io/myBaseUrl/blog/simple/slug", + "title": "Simple Slug", + "summary": "simple url slug", + "date_modified": "2020-08-15T00:00:00.000Z", + "author": { + "name": "Sébastien Lorber", + "url": "https://sebastienlorber.com" + }, + "tags": [] + }, + { + "id": "https://docusaurus.io/myBaseUrl/blog/heading-as-title", + "content_html": "", + "url": "https://docusaurus.io/myBaseUrl/blog/heading-as-title", + "title": "some heading", + "date_modified": "2019-01-02T00:00:00.000Z", + "tags": [] + }, + { + "id": "https://docusaurus.io/myBaseUrl/blog/date-matter", "content_html": "

date inside front matter

", - "url": "https://docusaurus.io/myBaseUrl/blog/date-matter/", + "url": "https://docusaurus.io/myBaseUrl/blog/date-matter", "title": "date-matter", "summary": "date inside front matter", "date_modified": "2019-01-01T00:00:00.000Z", @@ -494,9 +1120,9 @@ exports[`json has feed item for each post - with trailing slash 1`] = ` ] }, { - "id": "https://docusaurus.io/myBaseUrl/blog/2018/12/14/Happy-First-Birthday-Slash/", + "id": "https://docusaurus.io/myBaseUrl/blog/2018/12/14/Happy-First-Birthday-Slash", "content_html": "

Happy birthday! (translated)

", - "url": "https://docusaurus.io/myBaseUrl/blog/2018/12/14/Happy-First-Birthday-Slash/", + "url": "https://docusaurus.io/myBaseUrl/blog/2018/12/14/Happy-First-Birthday-Slash", "title": "Happy 1st Birthday Slash! (translated)", "summary": "Happy birthday! (translated)", "date_modified": "2018-12-14T00:00:00.000Z", @@ -513,9 +1139,11 @@ exports[`json has feed item for each post - with trailing slash 1`] = ` ] `; -exports[`json has feed item for each post 1`] = ` +exports[`json has xslt files for feed 1`] = ` [ - "{ + [ + "/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/feed.json", + "{ "version": "https://jsonfeed.org/version/1", "title": "Hello Blog", "home_page_url": "https://docusaurus.io/myBaseUrl/blog", @@ -609,9 +1237,63 @@ exports[`json has feed item for each post 1`] = ` } ] }", + ], ] `; +exports[`json has xslt files for feed: blog tree 1`] = ` +"blog +├── 2018 +│ └── 12 +│ └── 14 +│ └── Happy-First-Birthday-Slash +│ └── index.html +├── archive +│ └── index.html +├── atom.css +├── atom.xml +├── atom.xsl +├── blog-with-links +│ └── index.html +├── custom-atom.css +├── custom-atom.xsl +├── custom-rss.css +├── custom-rss.xsl +├── date-matter +│ └── index.html +├── feed.json +├── heading-as-title +│ └── index.html +├── hey +│ └── my super path +│ └── héllô +│ └── index.html +├── index.html +├── mdx-blog-post +│ └── index.html +├── mdx-require-blog-post +│ └── index.html +├── page +│ ├── 2 +│ │ └── index.html +│ └── 3 +│ └── index.html +├── rss.css +├── rss.xml +├── rss.xsl +├── simple +│ └── slug +│ └── index.html +├── tags +│ ├── complex +│ │ └── index.html +│ ├── date +│ │ └── index.html +│ └── index.html +└── unlisted + └── index.html" +`; + exports[`rss filters to the first two entries 1`] = ` [ " @@ -708,6 +1390,184 @@ exports[`rss filters to the first two entries using limit 1`] = ` ] `; +exports[`rss has custom xslt files for feed 1`] = ` +[ + [ + "/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/rss.xml", + " + + + Hello Blog + https://docusaurus.io/myBaseUrl/blog + Hello Blog + Sun, 23 Jul 2023 00:00:00 GMT + https://validator.w3.org/feed/docs/rss2.html + https://github.com/jpmonette/feed + en + Copyright + + <![CDATA[test links]]> + https://docusaurus.io/myBaseUrl/blog/blog-with-links + https://docusaurus.io/myBaseUrl/blog/blog-with-links + Sun, 23 Jul 2023 00:00:00 GMT + + absolute full url

+

absolute pathname

+

relative pathname

+

md link

+

anchor

+

relative pathname + anchor

+

+

+ +]]>
+
+ + <![CDATA[MDX Blog Sample with require calls]]> + https://docusaurus.io/myBaseUrl/blog/mdx-require-blog-post + https://docusaurus.io/myBaseUrl/blog/mdx-require-blog-post + Sat, 06 Mar 2021 00:00:00 GMT + + Test MDX with require calls

+ + + + +]]>
+
+ + <![CDATA[Full Blog Sample]]> + https://docusaurus.io/myBaseUrl/blog/mdx-blog-post + https://docusaurus.io/myBaseUrl/blog/mdx-blog-post + Fri, 05 Mar 2021 00:00:00 GMT + + HTML Heading 1 +

HTML Heading 2

+

HTML Paragraph

+ + +

Import DOM

+

Heading 1

+

Heading 2

+

Heading 3

+

Heading 4

+
Heading 5
+
    +
  • list1
  • +
  • list2
  • +
  • list3
  • +
+
    +
  • list1
  • +
  • list2
  • +
  • list3
  • +
+

Normal Text Italics Text Bold Text

+

link image

]]>
+
+ + <![CDATA[Complex Slug]]> + https://docusaurus.io/myBaseUrl/blog/hey/my super path/héllô + https://docusaurus.io/myBaseUrl/blog/hey/my super path/héllô + Sun, 16 Aug 2020 00:00:00 GMT + + complex url slug

]]>
+ date + complex +
+ + <![CDATA[Simple Slug]]> + https://docusaurus.io/myBaseUrl/blog/simple/slug + https://docusaurus.io/myBaseUrl/blog/simple/slug + Sat, 15 Aug 2020 00:00:00 GMT + + simple url slug

]]>
+
+ + <![CDATA[some heading]]> + https://docusaurus.io/myBaseUrl/blog/heading-as-title + https://docusaurus.io/myBaseUrl/blog/heading-as-title + Wed, 02 Jan 2019 00:00:00 GMT + + + <![CDATA[date-matter]]> + https://docusaurus.io/myBaseUrl/blog/date-matter + https://docusaurus.io/myBaseUrl/blog/date-matter + Tue, 01 Jan 2019 00:00:00 GMT + + date inside front matter

]]>
+ date +
+ + <![CDATA[Happy 1st Birthday Slash! (translated)]]> + https://docusaurus.io/myBaseUrl/blog/2018/12/14/Happy-First-Birthday-Slash + https://docusaurus.io/myBaseUrl/blog/2018/12/14/Happy-First-Birthday-Slash + Fri, 14 Dec 2018 00:00:00 GMT + + Happy birthday! (translated)

]]>
+ lorber.sebastien@gmail.com (Sébastien Lorber (translated)) + inlineTag + Global Tag label (en) +
+
+
", + ], +] +`; + +exports[`rss has custom xslt files for feed: blog tree 1`] = ` +"blog +├── 2018 +│ └── 12 +│ └── 14 +│ └── Happy-First-Birthday-Slash +│ └── index.html +├── archive +│ └── index.html +├── atom.css +├── atom.xml +├── atom.xsl +├── blog-with-links +│ └── index.html +├── custom-atom.css +├── custom-atom.xsl +├── custom-rss.css +├── custom-rss.xsl +├── date-matter +│ └── index.html +├── feed.json +├── heading-as-title +│ └── index.html +├── hey +│ └── my super path +│ └── héllô +│ └── index.html +├── index.html +├── mdx-blog-post +│ └── index.html +├── mdx-require-blog-post +│ └── index.html +├── page +│ ├── 2 +│ │ └── index.html +│ └── 3 +│ └── index.html +├── rss.css +├── rss.xml +├── rss.xsl +├── simple +│ └── slug +│ └── index.html +├── tags +│ ├── complex +│ │ └── index.html +│ ├── date +│ │ └── index.html +│ └── index.html +└── unlisted + └── index.html" +`; + exports[`rss has feed item for each post - with trailing slash 1`] = ` [ " @@ -951,3 +1811,181 @@ exports[`rss has feed item for each post 1`] = ` ", ] `; + +exports[`rss has xslt files for feed 1`] = ` +[ + [ + "/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/rss.xml", + " + + + Hello Blog + https://docusaurus.io/myBaseUrl/blog + Hello Blog + Sun, 23 Jul 2023 00:00:00 GMT + https://validator.w3.org/feed/docs/rss2.html + https://github.com/jpmonette/feed + en + Copyright + + <![CDATA[test links]]> + https://docusaurus.io/myBaseUrl/blog/blog-with-links + https://docusaurus.io/myBaseUrl/blog/blog-with-links + Sun, 23 Jul 2023 00:00:00 GMT + + absolute full url

+

absolute pathname

+

relative pathname

+

md link

+

anchor

+

relative pathname + anchor

+

+

+ +]]>
+
+ + <![CDATA[MDX Blog Sample with require calls]]> + https://docusaurus.io/myBaseUrl/blog/mdx-require-blog-post + https://docusaurus.io/myBaseUrl/blog/mdx-require-blog-post + Sat, 06 Mar 2021 00:00:00 GMT + + Test MDX with require calls

+ + + + +]]>
+
+ + <![CDATA[Full Blog Sample]]> + https://docusaurus.io/myBaseUrl/blog/mdx-blog-post + https://docusaurus.io/myBaseUrl/blog/mdx-blog-post + Fri, 05 Mar 2021 00:00:00 GMT + + HTML Heading 1 +

HTML Heading 2

+

HTML Paragraph

+ + +

Import DOM

+

Heading 1

+

Heading 2

+

Heading 3

+

Heading 4

+
Heading 5
+
    +
  • list1
  • +
  • list2
  • +
  • list3
  • +
+
    +
  • list1
  • +
  • list2
  • +
  • list3
  • +
+

Normal Text Italics Text Bold Text

+

link image

]]>
+
+ + <![CDATA[Complex Slug]]> + https://docusaurus.io/myBaseUrl/blog/hey/my super path/héllô + https://docusaurus.io/myBaseUrl/blog/hey/my super path/héllô + Sun, 16 Aug 2020 00:00:00 GMT + + complex url slug

]]>
+ date + complex +
+ + <![CDATA[Simple Slug]]> + https://docusaurus.io/myBaseUrl/blog/simple/slug + https://docusaurus.io/myBaseUrl/blog/simple/slug + Sat, 15 Aug 2020 00:00:00 GMT + + simple url slug

]]>
+
+ + <![CDATA[some heading]]> + https://docusaurus.io/myBaseUrl/blog/heading-as-title + https://docusaurus.io/myBaseUrl/blog/heading-as-title + Wed, 02 Jan 2019 00:00:00 GMT + + + <![CDATA[date-matter]]> + https://docusaurus.io/myBaseUrl/blog/date-matter + https://docusaurus.io/myBaseUrl/blog/date-matter + Tue, 01 Jan 2019 00:00:00 GMT + + date inside front matter

]]>
+ date +
+ + <![CDATA[Happy 1st Birthday Slash! (translated)]]> + https://docusaurus.io/myBaseUrl/blog/2018/12/14/Happy-First-Birthday-Slash + https://docusaurus.io/myBaseUrl/blog/2018/12/14/Happy-First-Birthday-Slash + Fri, 14 Dec 2018 00:00:00 GMT + + Happy birthday! (translated)

]]>
+ lorber.sebastien@gmail.com (Sébastien Lorber (translated)) + inlineTag + Global Tag label (en) +
+
+
", + ], +] +`; + +exports[`rss has xslt files for feed: blog tree 1`] = ` +"blog +├── 2018 +│ └── 12 +│ └── 14 +│ └── Happy-First-Birthday-Slash +│ └── index.html +├── archive +│ └── index.html +├── atom.css +├── atom.xml +├── atom.xsl +├── blog-with-links +│ └── index.html +├── custom-atom.css +├── custom-atom.xsl +├── custom-rss.css +├── custom-rss.xsl +├── date-matter +│ └── index.html +├── feed.json +├── heading-as-title +│ └── index.html +├── hey +│ └── my super path +│ └── héllô +│ └── index.html +├── index.html +├── mdx-blog-post +│ └── index.html +├── mdx-require-blog-post +│ └── index.html +├── page +│ ├── 2 +│ │ └── index.html +│ └── 3 +│ └── index.html +├── rss.css +├── rss.xml +├── rss.xsl +├── simple +│ └── slug +│ └── index.html +├── tags +│ ├── complex +│ │ └── index.html +│ ├── date +│ │ └── index.html +│ └── index.html +└── unlisted + └── index.html" +`; diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/options.test.ts.snap b/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/options.test.ts.snap index 4406ef7b72eb..ce4af321ff02 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/options.test.ts.snap +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/options.test.ts.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`validateOptions throws Error in case of invalid feed type 1`] = `""feedOptions.type" does not match any of the allowed types"`; +exports[`validateOptions feed throws Error in case of invalid feed type 1`] = `""feedOptions.type" does not match any of the allowed types"`; exports[`validateOptions throws Error in case of invalid options 1`] = `""postsPerPage" must be greater than or equal to 1"`; diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts index ef57b4c7d63e..3cca6f94aaf9 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts @@ -10,13 +10,15 @@ import path from 'path'; import fs from 'fs-extra'; import {DEFAULT_PARSE_FRONT_MATTER} from '@docusaurus/utils'; import {fromPartial} from '@total-typescript/shoehorn'; -import {DEFAULT_OPTIONS} from '../options'; +import {normalizePluginOptions} from '@docusaurus/utils-validation'; +import tree from 'tree-node-cli'; +import {DEFAULT_OPTIONS, validateOptions} from '../options'; import {generateBlogPosts} from '../blogUtils'; import {createBlogFeedFiles} from '../feed'; import {getAuthorsMap} from '../authorsMap'; -import type {LoadContext, I18n} from '@docusaurus/types'; +import type {LoadContext, I18n, Validate} from '@docusaurus/types'; import type {BlogContentPaths} from '../types'; -import type {PluginOptions} from '@docusaurus/plugin-content-blog'; +import type {Options, PluginOptions} from '@docusaurus/plugin-content-blog'; const DefaultI18N: I18n = { currentLocale: 'en', @@ -50,8 +52,16 @@ function getBlogContentPaths(siteDir: string): BlogContentPaths { async function testGenerateFeeds( context: LoadContext, - options: PluginOptions, + optionsInput: Options, ): Promise { + const options = validateOptions({ + validate: normalizePluginOptions as Validate< + Options | undefined, + PluginOptions + >, + options: optionsInput, + }); + const contentPaths = getBlogContentPaths(context.siteDir); const authorsMap = await getAuthorsMap({ contentPaths, @@ -72,10 +82,11 @@ async function testGenerateFeeds( siteConfig: context.siteConfig, outDir: context.outDir, locale: 'en', + contentPaths, }); } -describe.each(['atom', 'rss', 'json'])('%s', (feedType) => { +describe.each(['atom', 'rss', 'json'] as const)('%s', (feedType) => { const fsMock = jest.spyOn(fs, 'outputFile').mockImplementation(() => {}); it('does not get generated without posts', async () => { @@ -105,13 +116,14 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => { feedOptions: { type: [feedType], copyright: 'Copyright', + xslt: {atom: null, rss: null}, }, readingTime: ({content, defaultReadingTime}) => defaultReadingTime({content}), truncateMarker: //, onInlineTags: 'ignore', onInlineAuthors: 'ignore', - } as PluginOptions, + }, ); expect(fsMock).toHaveBeenCalledTimes(0); @@ -148,13 +160,14 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => { feedOptions: { type: [feedType], copyright: 'Copyright', + xslt: {atom: null, rss: null}, }, readingTime: ({content, defaultReadingTime}) => defaultReadingTime({content}), truncateMarker: //, onInlineTags: 'ignore', onInlineAuthors: 'ignore', - } as PluginOptions, + }, ); expect( @@ -203,13 +216,14 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => { ...rest, }); }, + xslt: {atom: null, rss: null}, }, readingTime: ({content, defaultReadingTime}) => defaultReadingTime({content}), truncateMarker: //, onInlineTags: 'ignore', onInlineAuthors: 'ignore', - } as PluginOptions, + }, ); expect( @@ -249,13 +263,14 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => { type: [feedType], copyright: 'Copyright', limit: 2, + xslt: {atom: null, rss: null}, }, readingTime: ({content, defaultReadingTime}) => defaultReadingTime({content}), truncateMarker: //, onInlineTags: 'ignore', onInlineAuthors: 'ignore', - } as PluginOptions, + }, ); expect( @@ -295,13 +310,14 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => { feedOptions: { type: [feedType], copyright: 'Copyright', + xslt: {atom: null, rss: null}, }, readingTime: ({content, defaultReadingTime}) => defaultReadingTime({content}), truncateMarker: //, onInlineTags: 'ignore', onInlineAuthors: 'ignore', - } as PluginOptions, + }, ); expect( @@ -309,4 +325,99 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => { ).toMatchSnapshot(); fsMock.mockClear(); }); + + it('has xslt files for feed', async () => { + const siteDir = path.join(__dirname, '__fixtures__', 'website'); + const outDir = path.join(siteDir, 'build-snap'); + const siteConfig = { + title: 'Hello', + baseUrl: '/myBaseUrl/', + url: 'https://docusaurus.io', + favicon: 'image/favicon.ico', + markdown, + }; + + // Build is quite difficult to mock, so we built the blog beforehand and + // copied the output to the fixture... + await testGenerateFeeds( + fromPartial({ + siteDir, + siteConfig, + i18n: DefaultI18N, + outDir, + }), + { + path: 'blog', + routeBasePath: 'blog', + tagsBasePath: 'tags', + authorsMapPath: 'authors.yml', + include: DEFAULT_OPTIONS.include, + exclude: DEFAULT_OPTIONS.exclude, + feedOptions: { + type: [feedType], + copyright: 'Copyright', + xslt: true, + }, + readingTime: ({content, defaultReadingTime}) => + defaultReadingTime({content}), + truncateMarker: //, + onInlineTags: 'ignore', + onInlineAuthors: 'ignore', + }, + ); + + expect(tree(path.join(outDir, 'blog'))).toMatchSnapshot('blog tree'); + + expect(fsMock.mock.calls).toMatchSnapshot(); + fsMock.mockClear(); + }); + + it('has custom xslt files for feed', async () => { + const siteDir = path.join(__dirname, '__fixtures__', 'website'); + const outDir = path.join(siteDir, 'build-snap'); + const siteConfig = { + title: 'Hello', + baseUrl: '/myBaseUrl/', + url: 'https://docusaurus.io', + favicon: 'image/favicon.ico', + markdown, + }; + + // Build is quite difficult to mock, so we built the blog beforehand and + // copied the output to the fixture... + await testGenerateFeeds( + fromPartial({ + siteDir, + siteConfig, + i18n: DefaultI18N, + outDir, + }), + { + path: 'blog', + routeBasePath: 'blog', + tagsBasePath: 'tags', + authorsMapPath: 'authors.yml', + include: DEFAULT_OPTIONS.include, + exclude: DEFAULT_OPTIONS.exclude, + feedOptions: { + type: [feedType], + copyright: 'Copyright', + xslt: { + rss: 'custom-rss.xsl', + atom: 'custom-atom.xsl', + }, + }, + readingTime: ({content, defaultReadingTime}) => + defaultReadingTime({content}), + truncateMarker: //, + onInlineTags: 'ignore', + onInlineAuthors: 'ignore', + }, + ); + + expect(tree(path.join(outDir, 'blog'))).toMatchSnapshot('blog tree'); + + expect(fsMock.mock.calls).toMatchSnapshot(); + fsMock.mockClear(); + }); }); diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/options.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/options.test.ts index 050e2a016023..254d56b96b2a 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/options.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/options.test.ts @@ -6,8 +6,12 @@ */ import {normalizePluginOptions} from '@docusaurus/utils-validation'; -import {validateOptions, DEFAULT_OPTIONS} from '../options'; -import type {Options, PluginOptions} from '@docusaurus/plugin-content-blog'; +import {validateOptions, DEFAULT_OPTIONS, XSLTBuiltInPaths} from '../options'; +import type { + Options, + PluginOptions, + UserFeedOptions, +} from '@docusaurus/plugin-content-blog'; import type {Validate} from '@docusaurus/types'; function testValidate(options?: Options) { @@ -38,7 +42,10 @@ describe('validateOptions', () => { it('accepts correctly defined user options', () => { const userOptions: Options = { ...defaultOptions, - feedOptions: {type: 'rss' as const, title: 'myTitle'}, + feedOptions: { + type: 'rss' as const, + title: 'myTitle', + }, path: 'not_blog', routeBasePath: '/myBlog', postsPerPage: 5, @@ -48,7 +55,13 @@ describe('validateOptions', () => { }; expect(testValidate(userOptions)).toEqual({ ...userOptions, - feedOptions: {type: ['rss'], title: 'myTitle', copyright: '', limit: 20}, + feedOptions: { + type: ['rss'], + title: 'myTitle', + copyright: '', + limit: 20, + xslt: {rss: null, atom: null}, + }, }); }); @@ -79,60 +92,182 @@ describe('validateOptions', () => { ).toThrowErrorMatchingSnapshot(); }); - it('throws Error in case of invalid feed type', () => { - expect(() => - testValidate({ + describe('feed', () => { + it('throws Error in case of invalid feed type', () => { + expect(() => + testValidate({ + feedOptions: { + // @ts-expect-error: test + type: 'none', + }, + }), + ).toThrowErrorMatchingSnapshot(); + }); + + it('converts all feed type to array with other feed type', () => { + expect( + testValidate({ + feedOptions: {type: 'all'}, + }), + ).toEqual({ + ...defaultOptions, feedOptions: { - // @ts-expect-error: test - type: 'none', + type: ['rss', 'atom', 'json'], + copyright: '', + limit: 20, + xslt: {rss: null, atom: null}, }, - }), - ).toThrowErrorMatchingSnapshot(); - }); + }); + }); - it('converts all feed type to array with other feed type', () => { - expect( - testValidate({ - feedOptions: {type: 'all'}, - }), - ).toEqual({ - ...defaultOptions, - feedOptions: {type: ['rss', 'atom', 'json'], copyright: '', limit: 20}, + it('accepts null feed type and return same', () => { + expect( + testValidate({ + feedOptions: {type: null}, + }), + ).toEqual({ + ...defaultOptions, + feedOptions: { + type: null, + limit: 20, + xslt: {rss: null, atom: null}, + }, + }); }); - }); - it('accepts null type and return same', () => { - expect( - testValidate({ - feedOptions: {type: null}, - }), - ).toEqual({ - ...defaultOptions, - feedOptions: {type: null, limit: 20}, + it('contains array with rss + atom for missing feed type', () => { + expect( + testValidate({ + feedOptions: {}, + }), + ).toEqual(defaultOptions); }); - }); - it('contains array with rss + atom for missing feed type', () => { - expect( - testValidate({ - feedOptions: {}, - }), - ).toEqual(defaultOptions); - }); + it('has array with rss + atom, title for missing feed type', () => { + expect( + testValidate({ + feedOptions: {title: 'title'}, + }), + ).toEqual({ + ...defaultOptions, + feedOptions: { + type: ['rss', 'atom'], + title: 'title', + copyright: '', + limit: 20, + xslt: {rss: null, atom: null}, + }, + }); + }); - it('has array with rss + atom, title for missing feed type', () => { - expect( - testValidate({ - feedOptions: {title: 'title'}, - }), - ).toEqual({ - ...defaultOptions, - feedOptions: { - type: ['rss', 'atom'], - title: 'title', - copyright: '', - limit: 20, - }, + describe('feed xslt', () => { + function testXSLT(xslt: UserFeedOptions['xslt']) { + return testValidate({feedOptions: {xslt}}).feedOptions.xslt; + } + + it('accepts xslt: true', () => { + expect(testXSLT(true)).toEqual({ + rss: XSLTBuiltInPaths.rss, + atom: XSLTBuiltInPaths.atom, + }); + }); + + it('accepts xslt: false', () => { + expect(testXSLT(false)).toEqual({ + rss: null, + atom: null, + }); + }); + + it('accepts xslt: null', () => { + expect(testXSLT(null)).toEqual({ + rss: null, + atom: null, + }); + }); + + it('accepts xslt: undefined', () => { + expect(testXSLT(undefined)).toEqual({ + rss: null, + atom: null, + }); + }); + + it('accepts xslt: {rss: true}', () => { + expect(testXSLT({rss: true})).toEqual({ + rss: XSLTBuiltInPaths.rss, + atom: null, + }); + }); + + it('accepts xslt: {atom: true}', () => { + expect(testXSLT({atom: true})).toEqual({ + rss: null, + atom: XSLTBuiltInPaths.atom, + }); + }); + + it('accepts xslt: {rss: true, atom: true}', () => { + expect(testXSLT({rss: true, atom: true})).toEqual({ + rss: XSLTBuiltInPaths.rss, + atom: XSLTBuiltInPaths.atom, + }); + }); + + it('accepts xslt: {rss: "custom-path"}', () => { + expect(testXSLT({rss: 'custom-path'})).toEqual({ + rss: 'custom-path', + atom: null, + }); + }); + + it('accepts xslt: {rss: true, atom: "custom-path"}', () => { + expect(testXSLT({rss: true, atom: 'custom-path'})).toEqual({ + rss: XSLTBuiltInPaths.rss, + atom: 'custom-path', + }); + }); + + it('accepts xslt: {rss: null, atom: true}', () => { + expect(testXSLT({rss: null, atom: true})).toEqual({ + rss: null, + atom: XSLTBuiltInPaths.atom, + }); + }); + + it('accepts xslt: {rss: false, atom: null}', () => { + expect(testXSLT({rss: false, atom: null})).toEqual({ + rss: null, + atom: null, + }); + }); + + it('rejects xslt: 42', () => { + // @ts-expect-error: bad type + expect(() => testXSLT(42)).toThrowErrorMatchingInlineSnapshot( + `""feedOptions.xslt" must be one of [object, boolean]"`, + ); + }); + it('rejects xslt: []', () => { + // @ts-expect-error: bad type + expect(() => testXSLT([])).toThrowErrorMatchingInlineSnapshot( + `""feedOptions.xslt" must be one of [object, boolean]"`, + ); + }); + + it('rejects xslt: {rss: 42}', () => { + // @ts-expect-error: bad type + expect(() => testXSLT({rss: 42})).toThrowErrorMatchingInlineSnapshot( + `""feedOptions.xslt.rss" must be one of [string, boolean]"`, + ); + }); + + it('rejects xslt: {rss: []}', () => { + // @ts-expect-error: bad type + expect(() => testXSLT({rss: 42})).toThrowErrorMatchingInlineSnapshot( + `""feedOptions.xslt.rss" must be one of [string, boolean]"`, + ); + }); }); }); diff --git a/packages/docusaurus-plugin-content-blog/src/feed.ts b/packages/docusaurus-plugin-content-blog/src/feed.ts index b8bc8c1481c9..96f252f23070 100644 --- a/packages/docusaurus-plugin-content-blog/src/feed.ts +++ b/packages/docusaurus-plugin-content-blog/src/feed.ts @@ -7,15 +7,20 @@ import path from 'path'; import fs from 'fs-extra'; -import logger from '@docusaurus/logger'; import {Feed, type Author as FeedAuthor} from 'feed'; import * as srcset from 'srcset'; -import {normalizeUrl, readOutputHTMLFile} from '@docusaurus/utils'; +import { + getDataFilePath, + normalizeUrl, + readOutputHTMLFile, +} from '@docusaurus/utils'; import { blogPostContainerID, applyTrailingSlash, } from '@docusaurus/utils-common'; import {load as cheerioLoad} from 'cheerio'; +import logger from '@docusaurus/logger'; +import type {BlogContentPaths} from './types'; import type {DocusaurusConfig, HtmlTags, LoadContext} from '@docusaurus/types'; import type { FeedType, @@ -23,6 +28,8 @@ import type { Author, BlogPost, BlogFeedItem, + FeedOptions, + FeedXSLTOptions, } from '@docusaurus/plugin-content-blog'; async function generateBlogFeed({ @@ -180,32 +187,144 @@ async function defaultCreateFeedItems({ ); } +async function resolveXsltFilePaths({ + xsltFilePath, + contentPaths, +}: { + xsltFilePath: string; + contentPaths: BlogContentPaths; +}) { + const xsltAbsolutePath: string = path.isAbsolute(xsltFilePath) + ? xsltFilePath + : (await getDataFilePath({filePath: xsltFilePath, contentPaths})) ?? + path.resolve(contentPaths.contentPath, xsltFilePath); + + if (!(await fs.pathExists(xsltAbsolutePath))) { + throw new Error( + logger.interpolate`Blog feed XSLT file not found at path=${path.relative( + process.cwd(), + xsltAbsolutePath, + )}`, + ); + } + + const parsedPath = path.parse(xsltAbsolutePath); + const cssAbsolutePath = path.resolve( + parsedPath.dir, + `${parsedPath.name}.css`, + ); + if (!(await fs.pathExists(xsltAbsolutePath))) { + throw new Error( + logger.interpolate`Blog feed XSLT file was found at path=${path.relative( + process.cwd(), + xsltAbsolutePath, + )} +But its expected co-located CSS file could not be found at path=${path.relative( + process.cwd(), + cssAbsolutePath, + )} +If you want to provide a custom XSLT file, you must provide a CSS file with the exact same name.`, + ); + } + + return {xsltAbsolutePath, cssAbsolutePath}; +} + +async function generateXsltFiles({ + xsltFilePath, + generatePath, + contentPaths, +}: { + xsltFilePath: string; + generatePath: string; + contentPaths: BlogContentPaths; +}) { + const {xsltAbsolutePath, cssAbsolutePath} = await resolveXsltFilePaths({ + xsltFilePath, + contentPaths, + }); + const xsltOutputPath = path.join( + generatePath, + path.basename(xsltAbsolutePath), + ); + const cssOutputPath = path.join(generatePath, path.basename(cssAbsolutePath)); + await fs.copy(xsltAbsolutePath, xsltOutputPath); + await fs.copy(cssAbsolutePath, cssOutputPath); +} + +// This modifies the XML feed content to add a relative href to the XSLT file +// Good enough for now: we probably don't need a full XML parser just for that +// See also https://darekkay.com/blog/rss-styling/ +function injectXslt({ + feedContent, + xsltFilePath, +}: { + feedContent: string; + xsltFilePath: string; +}) { + return feedContent.replace( + '', + ``, + ); +} + +const FeedConfigs: Record< + FeedType, + { + outputFileName: string; + getContent: (feed: Feed) => string; + getXsltFilePath: (xslt: FeedXSLTOptions) => string | null; + } +> = { + rss: { + outputFileName: 'rss.xml', + getContent: (feed) => feed.rss2(), + getXsltFilePath: (xslt) => xslt.rss, + }, + atom: { + outputFileName: 'atom.xml', + getContent: (feed) => feed.atom1(), + getXsltFilePath: (xslt) => xslt.atom, + }, + json: { + outputFileName: 'feed.json', + getContent: (feed) => feed.json1(), + getXsltFilePath: () => null, + }, +}; + async function createBlogFeedFile({ feed, feedType, generatePath, + feedOptions, + contentPaths, }: { feed: Feed; feedType: FeedType; generatePath: string; + feedOptions: FeedOptions; + contentPaths: BlogContentPaths; }) { - const [feedContent, feedPath] = (() => { - switch (feedType) { - case 'rss': - return [feed.rss2(), 'rss.xml']; - case 'json': - return [feed.json1(), 'feed.json']; - case 'atom': - return [feed.atom1(), 'atom.xml']; - default: - throw new Error(`Feed type ${feedType} not supported.`); - } - })(); try { - await fs.outputFile(path.join(generatePath, feedPath), feedContent); + const feedConfig = FeedConfigs[feedType]; + + let feedContent = feedConfig.getContent(feed); + + const xsltFilePath = feedConfig.getXsltFilePath(feedOptions.xslt); + if (xsltFilePath) { + await generateXsltFiles({xsltFilePath, contentPaths, generatePath}); + feedContent = injectXslt({feedContent, xsltFilePath}); + } + + const outputPath = path.join(generatePath, feedConfig.outputFileName); + await fs.outputFile(outputPath, feedContent); } catch (err) { - logger.error(`Generating ${feedType} feed failed.`); - throw err; + throw new Error(`Generating ${feedType} feed failed.`, { + cause: err as Error, + }); } } @@ -222,12 +341,14 @@ export async function createBlogFeedFiles({ siteConfig, outDir, locale, + contentPaths, }: { blogPosts: BlogPost[]; options: PluginOptions; siteConfig: DocusaurusConfig; outDir: string; locale: string; + contentPaths: BlogContentPaths; }): Promise { const blogPosts = allBlogPosts.filter(shouldBeInFeed); @@ -250,6 +371,8 @@ export async function createBlogFeedFiles({ feed, feedType, generatePath: path.join(outDir, options.routeBasePath), + feedOptions: options.feedOptions, + contentPaths, }), ), ); diff --git a/packages/docusaurus-plugin-content-blog/src/index.ts b/packages/docusaurus-plugin-content-blog/src/index.ts index 679924729e5f..ea2652c57fe8 100644 --- a/packages/docusaurus-plugin-content-blog/src/index.ts +++ b/packages/docusaurus-plugin-content-blog/src/index.ts @@ -388,6 +388,7 @@ export default async function pluginContentBlog( outDir, siteConfig, locale: currentLocale, + contentPaths, }); }, diff --git a/packages/docusaurus-plugin-content-blog/src/options.ts b/packages/docusaurus-plugin-content-blog/src/options.ts index 20e0c3427948..e9d91d3bf449 100644 --- a/packages/docusaurus-plugin-content-blog/src/options.ts +++ b/packages/docusaurus-plugin-content-blog/src/options.ts @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +import path from 'path'; import { Joi, RemarkPluginsSchema, @@ -19,11 +20,20 @@ import type { PluginOptions, Options, FeedType, + FeedXSLTOptions, } from '@docusaurus/plugin-content-blog'; import type {OptionValidationContext} from '@docusaurus/types'; export const DEFAULT_OPTIONS: PluginOptions = { - feedOptions: {type: ['rss', 'atom'], copyright: '', limit: 20}, + feedOptions: { + type: ['rss', 'atom'], + copyright: '', + limit: 20, + xslt: { + rss: null, + atom: null, + }, + }, beforeDefaultRehypePlugins: [], beforeDefaultRemarkPlugins: [], admonitions: true, @@ -64,6 +74,94 @@ export const DEFAULT_OPTIONS: PluginOptions = { onInlineAuthors: 'warn', }; +export const XSLTBuiltInPaths = { + rss: path.resolve(__dirname, '..', 'assets', 'rss.xsl'), + atom: path.resolve(__dirname, '..', 'assets', 'atom.xsl'), +}; + +function normalizeXsltOption( + option: string | null | boolean, + type: 'rss' | 'atom', +): string | null { + if (typeof option === 'string') { + return option; + } + if (option === true) { + return XSLTBuiltInPaths[type]; + } + return null; +} + +function createXSLTFilePathSchema(type: 'atom' | 'rss') { + return Joi.alternatives() + .try( + Joi.string().required(), + Joi.boolean() + .allow(null, () => undefined) + .custom((val) => normalizeXsltOption(val, type)), + ) + .optional() + .default(null); +} + +const FeedXSLTOptionsSchema = Joi.alternatives() + .try( + Joi.object({ + rss: createXSLTFilePathSchema('rss'), + atom: createXSLTFilePathSchema('atom'), + }).required(), + Joi.boolean() + .allow(null, () => undefined) + .custom((val) => ({ + rss: normalizeXsltOption(val, 'rss'), + atom: normalizeXsltOption(val, 'atom'), + })), + ) + .optional() + .custom((val) => { + if (val === null) { + return { + rss: null, + atom: null, + }; + } + return val; + }) + .default(DEFAULT_OPTIONS.feedOptions.xslt); + +const FeedOptionsSchema = Joi.object({ + type: Joi.alternatives() + .try( + Joi.array().items(Joi.string().equal('rss', 'atom', 'json')), + Joi.alternatives().conditional( + Joi.string().equal('all', 'rss', 'atom', 'json'), + { + then: Joi.custom((val: FeedType | 'all') => + val === 'all' ? ['rss', 'atom', 'json'] : [val], + ), + }, + ), + ) + .allow(null) + .default(DEFAULT_OPTIONS.feedOptions.type), + xslt: FeedXSLTOptionsSchema, + title: Joi.string().allow(''), + description: Joi.string().allow(''), + // Only add default value when user actually wants a feed (type is not null) + copyright: Joi.when('type', { + is: Joi.any().valid(null), + then: Joi.string().optional(), + otherwise: Joi.string() + .allow('') + .default(DEFAULT_OPTIONS.feedOptions.copyright), + }), + language: Joi.string(), + createFeedItems: Joi.function(), + limit: Joi.alternatives() + .try(Joi.number(), Joi.valid(null), Joi.valid(false)) + .default(DEFAULT_OPTIONS.feedOptions.limit), +}).default(DEFAULT_OPTIONS.feedOptions); + const PluginOptionSchema = Joi.object({ path: Joi.string().default(DEFAULT_OPTIONS.path), archiveBasePath: Joi.string() @@ -116,37 +214,7 @@ const PluginOptionSchema = Joi.object({ beforeDefaultRehypePlugins: RehypePluginsSchema.default( DEFAULT_OPTIONS.beforeDefaultRehypePlugins, ), - feedOptions: Joi.object({ - type: Joi.alternatives() - .try( - Joi.array().items(Joi.string().equal('rss', 'atom', 'json')), - Joi.alternatives().conditional( - Joi.string().equal('all', 'rss', 'atom', 'json'), - { - then: Joi.custom((val: FeedType | 'all') => - val === 'all' ? ['rss', 'atom', 'json'] : [val], - ), - }, - ), - ) - .allow(null) - .default(DEFAULT_OPTIONS.feedOptions.type), - title: Joi.string().allow(''), - description: Joi.string().allow(''), - // Only add default value when user actually wants a feed (type is not null) - copyright: Joi.when('type', { - is: Joi.any().valid(null), - then: Joi.string().optional(), - otherwise: Joi.string() - .allow('') - .default(DEFAULT_OPTIONS.feedOptions.copyright), - }), - language: Joi.string(), - createFeedItems: Joi.function(), - limit: Joi.alternatives() - .try(Joi.number(), Joi.valid(null), Joi.valid(false)) - .default(DEFAULT_OPTIONS.feedOptions.limit), - }).default(DEFAULT_OPTIONS.feedOptions), + feedOptions: FeedOptionsSchema, authorsMapPath: Joi.string().default(DEFAULT_OPTIONS.authorsMapPath), readingTime: Joi.function().default(() => DEFAULT_OPTIONS.readingTime), sortPosts: Joi.string() diff --git a/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts b/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts index e0e5617ce792..02e98f0b1e3b 100644 --- a/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts +++ b/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts @@ -315,10 +315,26 @@ declare module '@docusaurus/plugin-content-blog' { }) => string | undefined; export type FeedType = 'rss' | 'atom' | 'json'; + + export type FeedXSLTOptions = { + /** + * RSS XSLT file path, relative to the blog content folder. + * If null, no XSLT file is used and the feed will be displayed as raw XML. + */ + rss: string | null; + /** + * Atom XSLT file path, relative to the blog content folder. + * If null, no XSLT file is used and the feed will be displayed as raw XML. + */ + atom: string | null; + }; + /** * Normalized feed options used within code. */ export type FeedOptions = { + /** Enable feeds xslt stylesheets */ + xslt: FeedXSLTOptions; /** If `null`, no feed is generated. */ type?: FeedType[] | null; /** Title of generated feed. */ @@ -507,6 +523,14 @@ declare module '@docusaurus/plugin-content-blog' { onInlineAuthors: 'ignore' | 'log' | 'warn' | 'throw'; }; + export type UserFeedXSLTOptions = + | boolean + | null + | { + rss?: string | boolean | null; + atom?: string | boolean | null; + }; + /** * Feed options, as provided by user config. `type` accepts `all` as shortcut */ @@ -515,6 +539,8 @@ declare module '@docusaurus/plugin-content-blog' { { /** Type of feed to be generated. Use `null` to disable generation. */ type?: FeedOptions['type'] | 'all' | FeedType; + /** User-provided XSLT config for feeds, un-normalized */ + xslt?: UserFeedXSLTOptions; } >; /** diff --git a/project-words.txt b/project-words.txt index 528d635318ee..8ff86be90d0e 100644 --- a/project-words.txt +++ b/project-words.txt @@ -412,6 +412,8 @@ webpackbar webstorm Wolcott Xplorer +xslt +XSLT XSOAR Yacop yangshun diff --git a/website/_dogfooding/_blog tests/custom-atom.css b/website/_dogfooding/_blog tests/custom-atom.css new file mode 100644 index 000000000000..c016178d9007 --- /dev/null +++ b/website/_dogfooding/_blog tests/custom-atom.css @@ -0,0 +1,76 @@ +/** + * 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. + */ + +* { + color: #0d1137; +} + +main { + flex: 1 0 auto; + width: 100%; + margin: 4rem auto; + padding: 1.5 rem; + max-width: 800px; + /* stylelint-disable-next-line font-family-name-quotes */ + font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell; +} + +.info { + display: block; + margin: 3rem 0; + padding: 2rem 3rem; + border: 1px solid dodgerblue; + border-left-width: 0.5rem; + border-radius: 0.4rem; + background-color: #e52165; +} + +.rss-icon { + height: 3.8rem; + width: 3.8rem; + margin-right: 1rem; +} + +.flex { + display: flex; +} + +.items-start { + align-items: flex-start; +} + +.pb-7 { + padding-bottom: 3rem; +} + +a { + color: #005aff; + text-decoration: none; +} + +h1 { + text-wrap: balance; + font-size: 3.8rem; + line-height: 1; + font-weight: 800; + margin-bottom: 4rem; +} + +h2 { + font-size: 3rem; + line-height: 1.2; + font-weight: 700; + margin-bottom: 3rem; +} + +h2:not(:first-child) { + margin-top: 5.8rem; +} + +.italic { + font-style: italic; +} diff --git a/website/_dogfooding/_blog tests/custom-atom.xsl b/website/_dogfooding/_blog tests/custom-atom.xsl new file mode 100644 index 000000000000..560d535302b5 --- /dev/null +++ b/website/_dogfooding/_blog tests/custom-atom.xsl @@ -0,0 +1,94 @@ + + + + + + + + + Atom Feed | <xsl:value-of + select="atom:feed/atom:title" + /> + + + +
+
+
+ This is an Atom feed. Subscribe by copying the URL from the address + bar into your newsreader. Visit About Feeds to learn more + and get started. It’s free.
+

+
+ + + + + + + + + + +
+ Custom Atom Feed Preview

+

+ +

+

Description:

+
+

Recent Posts

+
+ +
+ + + +
Published on +
+
+ +
+
+
+
+
+ + +
+ +
diff --git a/website/_dogfooding/_blog tests/custom-rss.css b/website/_dogfooding/_blog tests/custom-rss.css new file mode 100644 index 000000000000..c016178d9007 --- /dev/null +++ b/website/_dogfooding/_blog tests/custom-rss.css @@ -0,0 +1,76 @@ +/** + * 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. + */ + +* { + color: #0d1137; +} + +main { + flex: 1 0 auto; + width: 100%; + margin: 4rem auto; + padding: 1.5 rem; + max-width: 800px; + /* stylelint-disable-next-line font-family-name-quotes */ + font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell; +} + +.info { + display: block; + margin: 3rem 0; + padding: 2rem 3rem; + border: 1px solid dodgerblue; + border-left-width: 0.5rem; + border-radius: 0.4rem; + background-color: #e52165; +} + +.rss-icon { + height: 3.8rem; + width: 3.8rem; + margin-right: 1rem; +} + +.flex { + display: flex; +} + +.items-start { + align-items: flex-start; +} + +.pb-7 { + padding-bottom: 3rem; +} + +a { + color: #005aff; + text-decoration: none; +} + +h1 { + text-wrap: balance; + font-size: 3.8rem; + line-height: 1; + font-weight: 800; + margin-bottom: 4rem; +} + +h2 { + font-size: 3rem; + line-height: 1.2; + font-weight: 700; + margin-bottom: 3rem; +} + +h2:not(:first-child) { + margin-top: 5.8rem; +} + +.italic { + font-style: italic; +} diff --git a/website/_dogfooding/_blog tests/custom-rss.xsl b/website/_dogfooding/_blog tests/custom-rss.xsl new file mode 100644 index 000000000000..dd868860c2d6 --- /dev/null +++ b/website/_dogfooding/_blog tests/custom-rss.xsl @@ -0,0 +1,92 @@ + + + + + + + + + RSS Feed | <xsl:value-of select="rss/channel/title" /> + + + +
+
+
+ This is an RSS feed. Subscribe by copying the URL from the address + bar into your newsreader. Visit About Feeds to learn more + and get started. It’s free.
+

+
+ + + + + + + + + + +
+ Custom RSS Feed Preview

+

+ +

+

Description:

+
+

Recent Posts

+
+ +
+ + + +
Published on +
+
+ +
+
+
+
+
+ + +
+ +
diff --git a/website/_dogfooding/dogfooding.config.ts b/website/_dogfooding/dogfooding.config.ts index f68020dca475..d31bce0776d7 100644 --- a/website/_dogfooding/dogfooding.config.ts +++ b/website/_dogfooding/dogfooding.config.ts @@ -88,6 +88,10 @@ export const dogfoodingPluginInstances: PluginConfig[] = [ type: 'all', title: 'Docusaurus Tests Blog', copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc.`, + xslt: { + rss: 'custom-rss.xsl', + atom: 'custom-atom.xsl', + }, }, readingTime: ({content, frontMatter, defaultReadingTime}) => frontMatter.hide_reading_time diff --git a/website/docs/api/plugins/plugin-content-blog.mdx b/website/docs/api/plugins/plugin-content-blog.mdx index 3ad31c590296..9b2d50d96d2e 100644 --- a/website/docs/api/plugins/plugin-content-blog.mdx +++ b/website/docs/api/plugins/plugin-content-blog.mdx @@ -77,6 +77,7 @@ Accepted fields: | `feedOptions.title` | `string` | `siteConfig.title` | Title of the feed. | | `feedOptions.description` | `string` | \`$\{siteConfig.title} Blog\` | Description of the feed. | | `feedOptions.copyright` | `string` | `undefined` | Copyright message. | +| `feedOptions.xslt` | boolean \| [FeedXSLTOptions](#FeedXSLTOptions) | `undefined` | Copyright message. | | `feedOptions.language` | `string` (See [documentation](http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes) for possible values) | `undefined` | Language metadata of the feed. | | `sortPosts` | 'descending' \| 'ascending' | `'descending'` | Governs the direction of blog post sorting. | | `processBlogPosts` | [ProcessBlogPostsFn](#ProcessBlogPostsFn) | `undefined` | An optional function which can be used to transform blog posts (filter, modify, delete, etc...). | @@ -129,6 +130,25 @@ type ReadingTimeFn = (params: { type FeedType = 'rss' | 'atom' | 'json'; ``` +#### `FeedXSLTOptions` {#FeedXSLTOptions} + +Permits to style the blog XML feeds so that browsers render them nicely with [XSLT](https://developer.mozilla.org/en-US/docs/Web/XSLT). + +- Use `true` to let the blog use its built-in `.xsl` and `.css` files to style the blog feed +- Use a falsy value (`undefined | null | false`) to disable the feature +- Use a `string` to provide a file path to a custom `.xsl` file relative to the blog content folder. By convention, you must provide a `.css` file with the exact same name. + +```ts +type FeedXSLTOptions = + | boolean + | undefined + | null + | { + rss?: string | boolean | null | undefined; + atom?: string | boolean | null | undefined; + }; +``` + #### `CreateFeedItemsFn` {#CreateFeedItemsFn} ```ts diff --git a/website/docs/blog.mdx b/website/docs/blog.mdx index e6cf18644e98..468e42f3b929 100644 --- a/website/docs/blog.mdx +++ b/website/docs/blog.mdx @@ -602,9 +602,18 @@ type BlogOptions = { title?: string; description?: string; copyright: string; + language?: string; // possible values: http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes limit?: number | false | null; // defaults to 20 - /** Allow control over the construction of BlogFeedItems */ + // XSLT permits browsers to style and render nicely the feed XML files + xslt?: + | boolean + | { + // + rss?: string | boolean; + atom?: string | boolean; + }; + // Allow control over the construction of BlogFeedItems createFeedItems?: (params: { blogPosts: BlogPost[]; siteConfig: DocusaurusConfig; diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index 70d7fd33b78f..26a3e265f0af 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -487,7 +487,10 @@ export default async function createConfigAsync() { postsPerPage: 5, feedOptions: { type: 'all', + description: + 'Keep up to date with upcoming Docusaurus releases and articles by following our feed!', copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc.`, + xslt: true, }, blogTitle: 'Docusaurus blog', blogDescription: 'Read blog posts about Docusaurus from the team',