diff --git a/packages/create-docusaurus/templates/shared/blog/tags.yml b/packages/create-docusaurus/templates/shared/blog/tags.yml new file mode 100644 index 0000000000..f71dd73931 --- /dev/null +++ b/packages/create-docusaurus/templates/shared/blog/tags.yml @@ -0,0 +1,16 @@ +facebook: + label: Facebook + permalink: /facebook + description: Facebook tag description +hello: + label: Hello + permalink: /hello + description: Hello tag description +docusaurus: + label: Docusaurus + permalink: /docusaurus + description: Docusaurus tag description +hola: + label: Hola + permalink: /hola + description: Hola tag description diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/2018-12-14-Happy-First-Birthday-Slash.md b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/2018-12-14-Happy-First-Birthday-Slash.md index 02350da6d2..10ba9373aa 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/2018-12-14-Happy-First-Birthday-Slash.md +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/2018-12-14-Happy-First-Birthday-Slash.md @@ -3,7 +3,7 @@ title: Happy 1st Birthday Slash! authors: - name: Yangshun Tay - slorber -tags: [birthday] +tags: [birthday,inlineTag,globalTag] --- Happy birthday! diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/tags.yml b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/tags.yml new file mode 100644 index 0000000000..04d2fbef70 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/tags.yml @@ -0,0 +1,5 @@ + +globalTag: + label: 'Global Tag label' + description: 'Global Tag description' + permalink: '/tags/global-tag-permalink' diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/i18n/en/docusaurus-plugin-content-blog/2018-12-14-Happy-First-Birthday-Slash.md b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/i18n/en/docusaurus-plugin-content-blog/2018-12-14-Happy-First-Birthday-Slash.md index b015e77fb5..7b25e16ce1 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/i18n/en/docusaurus-plugin-content-blog/2018-12-14-Happy-First-Birthday-Slash.md +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/i18n/en/docusaurus-plugin-content-blog/2018-12-14-Happy-First-Birthday-Slash.md @@ -3,6 +3,7 @@ title: Happy 1st Birthday Slash! (translated) authors: - name: Yangshun Tay (translated) - slorber +tags: [inlineTag,globalTag] --- Happy birthday! (translated) diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/i18n/en/docusaurus-plugin-content-blog/tags.yml b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/i18n/en/docusaurus-plugin-content-blog/tags.yml new file mode 100644 index 0000000000..6b826f7bf7 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/i18n/en/docusaurus-plugin-content-blog/tags.yml @@ -0,0 +1,5 @@ + +globalTag: + label: 'Global Tag label (en)' + description: 'Global Tag description (en)' + permalink: 'global-tag-permalink (en)' 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 b207a23623..6b693d83fb 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,134 +92,6 @@ exports[`atom filters to the first two entries using limit 1`] = ` ] `; -exports[`atom has feed item for each post 1`] = ` -[ - " - - 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 feed item for each post - with trailing slash 1`] = ` [ " @@ -343,6 +215,138 @@ exports[`atom has feed item for each post - with trailing slash 1`] = ` Sébastien Lorber (translated) lorber.sebastien@gmail.com + + + +", +] +`; + +exports[`atom has feed item for each post 1`] = ` +[ + " + + 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 + + +
", ] @@ -410,102 +414,6 @@ exports[`json filters to the first two entries using limit 1`] = ` ] `; -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/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": [] - } - ] -}", -] -`; - exports[`json has feed item for each post - with trailing slash 1`] = ` [ "{ @@ -595,7 +503,109 @@ exports[`json has feed item for each post - with trailing slash 1`] = ` "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/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)" + ] } ] }", @@ -698,126 +708,6 @@ exports[`rss filters to the first two entries using limit 1`] = ` ] `; -exports[`rss has feed item for each post 1`] = ` -[ - " - - - 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)) -
-
-
", -] -`; - exports[`rss has feed item for each post - with trailing slash 1`] = ` [ " @@ -932,6 +822,130 @@ exports[`rss has feed item for each post - with trailing slash 1`] = ` Happy birthday! (translated)

]]>
lorber.sebastien@gmail.com (Sébastien Lorber (translated)) + inlineTag + Global Tag label (en) + + +", +] +`; + +exports[`rss has feed item for each post 1`] = ` +[ + " + + + 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)
", diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/index.test.ts.snap index 983054187f..a9ff614dba 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/index.test.ts.snap @@ -3,6 +3,8 @@ exports[`blog plugin process blog posts load content 1`] = ` { "/blog/tags/tag-1": { + "description": undefined, + "inline": true, "items": [ "/simple/slug/another", "/another/tags", @@ -63,6 +65,8 @@ exports[`blog plugin process blog posts load content 1`] = ` "unlisted": false, }, "/blog/tags/tag-2": { + "description": undefined, + "inline": true, "items": [ "/another/tags", "/another/tags2", @@ -148,6 +152,8 @@ exports[`blog plugin process blog posts load content 2`] = ` "source": "@site/blog/another-simple-slug-with-tags.md", "tags": [ { + "description": undefined, + "inline": true, "label": "tag1", "permalink": "/blog/tags/tag-1", }, @@ -189,10 +195,14 @@ exports[`blog plugin process blog posts load content 2`] = ` "source": "@site/blog/another-with-tags.md", "tags": [ { + "description": undefined, + "inline": true, "label": "tag1", "permalink": "/blog/tags/tag-1", }, { + "description": undefined, + "inline": true, "label": "tag2", "permalink": "/blog/tags/tag-2", }, @@ -230,10 +240,14 @@ exports[`blog plugin process blog posts load content 2`] = ` "source": "@site/blog/another-with-tags2.md", "tags": [ { + "description": undefined, + "inline": true, "label": "tag1", "permalink": "/blog/tags/tag-1", }, { + "description": undefined, + "inline": true, "label": "tag2", "permalink": "/blog/tags/tag-2", }, @@ -248,6 +262,8 @@ exports[`blog plugin process blog posts load content 2`] = ` exports[`blog plugin works on blog tags without pagination 1`] = ` { "/blog/tags/tag-1": { + "description": undefined, + "inline": true, "items": [ "/simple/slug/another", "/another/tags", @@ -278,6 +294,8 @@ exports[`blog plugin works on blog tags without pagination 1`] = ` "unlisted": false, }, "/blog/tags/tag-2": { + "description": undefined, + "inline": true, "items": [ "/another/tags", "/another/tags2", @@ -306,6 +324,8 @@ exports[`blog plugin works on blog tags without pagination 1`] = ` "unlisted": false, }, "/blog/tags/unlisted": { + "description": undefined, + "inline": true, "items": [ "/another/blog-with-tags-unlisted", ], @@ -337,6 +357,8 @@ exports[`blog plugin works on blog tags without pagination 1`] = ` exports[`blog plugin works with blog tags 1`] = ` { "/blog/tags/tag-1": { + "description": undefined, + "inline": true, "items": [ "/simple/slug/another", "/another/tags", @@ -382,6 +404,8 @@ exports[`blog plugin works with blog tags 1`] = ` "unlisted": false, }, "/blog/tags/tag-2": { + "description": undefined, + "inline": true, "items": [ "/another/tags", "/another/tags2", @@ -410,6 +434,8 @@ exports[`blog plugin works with blog tags 1`] = ` "unlisted": false, }, "/blog/tags/unlisted": { + "description": undefined, + "inline": true, "items": [ "/another/blog-with-tags-unlisted", ], 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 b900129c9b..8c79310890 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts @@ -100,6 +100,7 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => { readingTime: ({content, defaultReadingTime}) => defaultReadingTime({content}), truncateMarker: //, + onInlineTags: 'ignore', } as PluginOptions, ); @@ -141,6 +142,7 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => { readingTime: ({content, defaultReadingTime}) => defaultReadingTime({content}), truncateMarker: //, + onInlineTags: 'ignore', } as PluginOptions, ); @@ -194,6 +196,7 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => { readingTime: ({content, defaultReadingTime}) => defaultReadingTime({content}), truncateMarker: //, + onInlineTags: 'ignore', } as PluginOptions, ); @@ -238,6 +241,7 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => { readingTime: ({content, defaultReadingTime}) => defaultReadingTime({content}), truncateMarker: //, + onInlineTags: 'ignore', } as PluginOptions, ); @@ -282,6 +286,7 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => { readingTime: ({content, defaultReadingTime}) => defaultReadingTime({content}), truncateMarker: //, + onInlineTags: 'ignore', } as PluginOptions, ); diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts index 16802634bc..5280a72c18 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts @@ -160,6 +160,8 @@ describe('blog plugin', () => { ); expect(relativePathsToWatch).toEqual([ 'i18n/en/docusaurus-plugin-content-blog/authors.yml', + 'i18n/en/docusaurus-plugin-content-blog/tags.yml', + 'blog/tags.yml', 'i18n/en/docusaurus-plugin-content-blog/**/*.{md,mdx}', 'blog/**/*.{md,mdx}', ]); @@ -188,6 +190,8 @@ describe('blog plugin', () => { prevItem: undefined, tags: [ { + description: undefined, + inline: true, label: 'date', permalink: '/blog/tags/date', }, @@ -232,9 +236,23 @@ describe('blog plugin', () => { }, 'slorber', ], + tags: ['inlineTag', 'globalTag'], title: 'Happy 1st Birthday Slash! (translated)', }, - tags: [], + tags: [ + { + description: undefined, + inline: true, + label: 'inlineTag', + permalink: '/blog/tags/inline-tag', + }, + { + description: 'Global Tag description (en)', + inline: false, + label: 'Global Tag label (en)', + permalink: '/blog/tags/global-tag-permalink (en)', + }, + ], prevItem: { permalink: '/blog/date-matter', title: 'date-matter', @@ -269,10 +287,14 @@ describe('blog plugin', () => { }, tags: [ { + description: undefined, + inline: true, label: 'date', permalink: '/blog/tags/date', }, { + description: undefined, + inline: true, label: 'complex', permalink: '/blog/tags/complex', }, @@ -516,6 +538,8 @@ describe('blog plugin', () => { postsPerPage: 1, processBlogPosts: async ({blogPosts}) => blogPosts.filter((blog) => blog.metadata.tags[0]?.label === 'tag1'), + onInlineTags: 'ignore', + tags: false, }, DefaultI18N, ); 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 8133d10df1..3e925cf106 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/options.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/options.test.ts @@ -36,13 +36,15 @@ describe('validateOptions', () => { }); it('accepts correctly defined user options', () => { - const userOptions = { + const userOptions: Options = { ...defaultOptions, feedOptions: {type: 'rss' as const, title: 'myTitle'}, path: 'not_blog', routeBasePath: '/myBlog', postsPerPage: 5, include: ['api/*', 'docs/*'], + tags: 'customTags.yml', + onInlineTags: 'warn', }; expect(testValidate(userOptions)).toEqual({ ...userOptions, @@ -172,4 +174,68 @@ describe('validateOptions', () => { `""blogSidebarTitle" must be a string"`, ); }); + + describe('tags', () => { + it('accepts tags - undefined', () => { + expect(testValidate({tags: undefined}).tags).toBeUndefined(); + }); + + it('accepts tags - null', () => { + expect(testValidate({tags: null}).tags).toBeNull(); + }); + + it('accepts tags - false', () => { + expect(testValidate({tags: false}).tags).toBeFalsy(); + }); + + it('accepts tags - customTags.yml', () => { + expect(testValidate({tags: 'customTags.yml'}).tags).toBe( + 'customTags.yml', + ); + }); + + it('rejects tags - 42', () => { + // @ts-expect-error: test + expect(() => testValidate({tags: 42})).toThrowErrorMatchingInlineSnapshot( + `""tags" must be a string"`, + ); + }); + }); + + describe('onInlineTags', () => { + it('accepts onInlineTags - undefined', () => { + expect(testValidate({onInlineTags: undefined}).onInlineTags).toBe('warn'); + }); + + it('accepts onInlineTags - "throw"', () => { + expect(testValidate({onInlineTags: 'throw'}).onInlineTags).toBe('throw'); + }); + + it('rejects onInlineTags - "trace"', () => { + expect(() => + // @ts-expect-error: test + testValidate({onInlineTags: 'trace'}), + ).toThrowErrorMatchingInlineSnapshot( + `""onInlineTags" must be one of [ignore, log, warn, throw]"`, + ); + }); + + it('rejects onInlineTags - null', () => { + expect(() => + // @ts-expect-error: test + testValidate({onInlineTags: 42}), + ).toThrowErrorMatchingInlineSnapshot( + `""onInlineTags" must be one of [ignore, log, warn, throw]"`, + ); + }); + + it('rejects onInlineTags - 42', () => { + expect(() => + // @ts-expect-error: test + testValidate({onInlineTags: 42}), + ).toThrowErrorMatchingInlineSnapshot( + `""onInlineTags" must be one of [ignore, log, warn, throw]"`, + ); + }); + }); }); diff --git a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts index 89f1c8f36d..d4a9f275bc 100644 --- a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts +++ b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts @@ -18,7 +18,6 @@ import { getFolderContainingFile, posixPath, Globby, - normalizeFrontMatterTags, groupTaggedItems, getTagVisibility, getFileCommitDate, @@ -26,9 +25,12 @@ import { isUnlisted, isDraft, readLastUpdateData, + normalizeTags, } from '@docusaurus/utils'; +import {getTagsFile} from '@docusaurus/utils-validation'; import {validateBlogPostFrontMatter} from './frontMatter'; import {type AuthorsMap, getAuthorsMap, getBlogPostAuthors} from './authors'; +import type {TagsFile} from '@docusaurus/utils'; import type {LoadContext, ParseFrontMatter} from '@docusaurus/types'; import type { PluginOptions, @@ -125,9 +127,11 @@ export function getBlogTags({ isUnlisted: (item) => item.metadata.unlisted, }); return { + inline: tag.inline, label: tag.label, - items: tagVisibility.listedItems.map((item) => item.id), permalink: tag.permalink, + description: tag.description, + items: tagVisibility.listedItems.map((item) => item.id), pages: paginateBlogPosts({ blogPosts: tagVisibility.listedItems, basePageUrl: tag.permalink, @@ -197,6 +201,7 @@ async function processBlogSourceFile( contentPaths: BlogContentPaths, context: LoadContext, options: PluginOptions, + tagsFile: TagsFile | null, authorsMap?: AuthorsMap, ): Promise { const { @@ -315,13 +320,21 @@ async function processBlogSourceFile( return undefined; } - const tagsBasePath = normalizeUrl([ + const tagsBaseRoutePath = normalizeUrl([ baseUrl, routeBasePath, tagsRouteBasePath, ]); const authors = getBlogPostAuthors({authorsMap, frontMatter, baseUrl}); + const tags = normalizeTags({ + options, + source: blogSourceRelative, + frontMatterTags: frontMatter.tags, + tagsBaseRoutePath, + tagsFile, + }); + return { id: slug, metadata: { @@ -331,7 +344,7 @@ async function processBlogSourceFile( title, description, date, - tags: normalizeFrontMatterTags(tagsBasePath, frontMatter.tags), + tags, readingTime: showReadingTime ? options.readingTime({ content, @@ -371,6 +384,8 @@ export async function generateBlogPosts( authorsMapPath: options.authorsMapPath, }); + const tagsFile = await getTagsFile({contentPaths, tags: options.tags}); + async function doProcessBlogSourceFile(blogSourceFile: string) { try { return await processBlogSourceFile( @@ -378,6 +393,7 @@ export async function generateBlogPosts( contentPaths, context, options, + tagsFile, authorsMap, ); } catch (err) { diff --git a/packages/docusaurus-plugin-content-blog/src/index.ts b/packages/docusaurus-plugin-content-blog/src/index.ts index dc64b8b451..19dfb1a5d2 100644 --- a/packages/docusaurus-plugin-content-blog/src/index.ts +++ b/packages/docusaurus-plugin-content-blog/src/index.ts @@ -20,6 +20,7 @@ import { DEFAULT_PLUGIN_ID, resolveMarkdownLinkPathname, } from '@docusaurus/utils'; +import {getTagsFilePathsToWatch} from '@docusaurus/utils-validation'; import { getSourceToPermalink, getBlogTags, @@ -104,9 +105,16 @@ export default async function pluginContentBlog( (contentPath) => include.map((pattern) => `${contentPath}/${pattern}`), ); - return [authorsMapFilePath, ...contentMarkdownGlobs].filter( - Boolean, - ) as string[]; + const tagsFilePaths = getTagsFilePathsToWatch({ + contentPaths, + tags: options.tags, + }); + + return [ + authorsMapFilePath, + ...tagsFilePaths, + ...contentMarkdownGlobs, + ].filter(Boolean) as string[]; }, getTranslationFiles() { diff --git a/packages/docusaurus-plugin-content-blog/src/options.ts b/packages/docusaurus-plugin-content-blog/src/options.ts index 86dcbbd4be..5835c32fc1 100644 --- a/packages/docusaurus-plugin-content-blog/src/options.ts +++ b/packages/docusaurus-plugin-content-blog/src/options.ts @@ -54,6 +54,8 @@ export const DEFAULT_OPTIONS: PluginOptions = { showLastUpdateTime: false, showLastUpdateAuthor: false, processBlogPosts: async () => undefined, + onInlineTags: 'warn', + tags: undefined, }; const PluginOptionSchema = Joi.object({ @@ -144,6 +146,13 @@ const PluginOptionSchema = Joi.object({ processBlogPosts: Joi.function() .optional() .default(() => DEFAULT_OPTIONS.processBlogPosts), + onInlineTags: Joi.string() + .equal('ignore', 'log', 'warn', 'throw') + .default(DEFAULT_OPTIONS.onInlineTags), + tags: Joi.string() + .disallow('') + .allow(null, false) + .default(() => DEFAULT_OPTIONS.tags), }).default(DEFAULT_OPTIONS); export function validateOptions({ 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 a1f580466e..f4d4f135c0 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 @@ -4,7 +4,6 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - /// declare module '@docusaurus/plugin-content-blog' { @@ -12,9 +11,10 @@ declare module '@docusaurus/plugin-content-blog' { import type {MDXOptions} from '@docusaurus/mdx-loader'; import type { FrontMatterTag, - Tag, + TagMetadata, LastUpdateData, FrontMatterLastUpdate, + TagsPluginOptions, } from '@docusaurus/utils'; import type {DocusaurusConfig, Plugin, LoadContext} from '@docusaurus/types'; import type {Item as FeedItem} from 'feed'; @@ -236,7 +236,7 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the /** Front matter, as-is. */ readonly frontMatter: BlogPostFrontMatter & {[key: string]: unknown}; /** Tags, normalized. */ - readonly tags: Tag[]; + readonly tags: TagMetadata[]; /** * Marks the post as unlisted and visibly hides it unless directly accessed. */ @@ -345,103 +345,104 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the /** * Plugin options after normalization. */ - export type PluginOptions = MDXOptions & { - /** Plugin ID. */ - id?: string; - /** - * Path to the blog content directory on the file system, relative to site - * directory. - */ - path: string; - /** - * URL route for the blog section of your site. **DO NOT** include a - * trailing slash. Use `/` to put the blog at root path. - */ - routeBasePath: string; - /** - * URL route for the tags section of your blog. Will be appended to - * `routeBasePath`. - */ - tagsBasePath: string; - /** - * URL route for the pages section of your blog. Will be appended to - * `routeBasePath`. - */ - pageBasePath: string; - /** - * URL route for the archive section of your blog. Will be appended to - * `routeBasePath`. **DO NOT** include a trailing slash. Use `null` to - * disable generation of archive. - */ - archiveBasePath: string | null; - /** - * Array of glob patterns matching Markdown files to be built, relative to - * the content path. - */ - include: string[]; - /** - * Array of glob patterns matching Markdown files to be excluded. Serves as - * refinement based on the `include` option. - */ - exclude: string[]; - /** - * Number of posts to show per page in the listing page. Use `'ALL'` to - * display all posts on one listing page. - */ - postsPerPage: number | 'ALL'; - /** Root component of the blog listing page. */ - blogListComponent: string; - /** Root component of each blog post page. */ - blogPostComponent: string; - /** Root component of the tags list page. */ - blogTagsListComponent: string; - /** Root component of the "posts containing tag" page. */ - blogTagsPostsComponent: string; - /** Root component of the blog archive page. */ - blogArchiveComponent: string; - /** Blog page title for better SEO. */ - blogTitle: string; - /** Blog page meta description for better SEO. */ - blogDescription: string; - /** - * Number of blog post elements to show in the blog sidebar. `'ALL'` to show - * all blog posts; `0` to disable. - */ - blogSidebarCount: number | 'ALL'; - /** Title of the blog sidebar. */ - blogSidebarTitle: string; - /** Truncate marker marking where the summary ends. */ - truncateMarker: RegExp; - /** Show estimated reading time for the blog post. */ - showReadingTime: boolean; - /** Blog feed. */ - feedOptions: FeedOptions; - /** - * Base URL to edit your site. The final URL is computed by `editUrl + - * relativePostPath`. Using a function allows more nuanced control for each - * file. Omitting this variable entirely will disable edit links. - */ - editUrl?: string | EditUrlFunction; - /** - * The edit URL will target the localized file, instead of the original - * unlocalized file. Ignored when `editUrl` is a function. - */ - editLocalizedFiles?: boolean; - /** Path to the authors map file, relative to the blog content directory. */ - authorsMapPath: string; - /** A callback to customize the reading time number displayed. */ - readingTime: ReadingTimeFunctionOption; - /** Governs the direction of blog post sorting. */ - sortPosts: 'ascending' | 'descending'; - /** Whether to display the last date the doc was updated. */ - showLastUpdateTime: boolean; - /** Whether to display the author who last updated the doc. */ - showLastUpdateAuthor: boolean; - /** An optional function which can be used to transform blog posts - * (filter, modify, delete, etc...). - */ - processBlogPosts: ProcessBlogPostsFn; - }; + export type PluginOptions = MDXOptions & + TagsPluginOptions & { + /** Plugin ID. */ + id?: string; + /** + * Path to the blog content directory on the file system, relative to site + * directory. + */ + path: string; + /** + * URL route for the blog section of your site. **DO NOT** include a + * trailing slash. Use `/` to put the blog at root path. + */ + routeBasePath: string; + /** + * URL route for the tags section of your blog. Will be appended to + * `routeBasePath`. + */ + tagsBasePath: string; + /** + * URL route for the pages section of your blog. Will be appended to + * `routeBasePath`. + */ + pageBasePath: string; + /** + * URL route for the archive section of your blog. Will be appended to + * `routeBasePath`. **DO NOT** include a trailing slash. Use `null` to + * disable generation of archive. + */ + archiveBasePath: string | null; + /** + * Array of glob patterns matching Markdown files to be built, relative to + * the content path. + */ + include: string[]; + /** + * Array of glob patterns matching Markdown files to be excluded. Serves as + * refinement based on the `include` option. + */ + exclude: string[]; + /** + * Number of posts to show per page in the listing page. Use `'ALL'` to + * display all posts on one listing page. + */ + postsPerPage: number | 'ALL'; + /** Root component of the blog listing page. */ + blogListComponent: string; + /** Root component of each blog post page. */ + blogPostComponent: string; + /** Root component of the tags list page. */ + blogTagsListComponent: string; + /** Root component of the "posts containing tag" page. */ + blogTagsPostsComponent: string; + /** Root component of the blog archive page. */ + blogArchiveComponent: string; + /** Blog page title for better SEO. */ + blogTitle: string; + /** Blog page meta description for better SEO. */ + blogDescription: string; + /** + * Number of blog post elements to show in the blog sidebar. `'ALL'` to show + * all blog posts; `0` to disable. + */ + blogSidebarCount: number | 'ALL'; + /** Title of the blog sidebar. */ + blogSidebarTitle: string; + /** Truncate marker marking where the summary ends. */ + truncateMarker: RegExp; + /** Show estimated reading time for the blog post. */ + showReadingTime: boolean; + /** Blog feed. */ + feedOptions: FeedOptions; + /** + * Base URL to edit your site. The final URL is computed by `editUrl + + * relativePostPath`. Using a function allows more nuanced control for each + * file. Omitting this variable entirely will disable edit links. + */ + editUrl?: string | EditUrlFunction; + /** + * The edit URL will target the localized file, instead of the original + * unlocalized file. Ignored when `editUrl` is a function. + */ + editLocalizedFiles?: boolean; + /** Path to the authors map file, relative to the blog content directory. */ + authorsMapPath: string; + /** A callback to customize the reading time number displayed. */ + readingTime: ReadingTimeFunctionOption; + /** Governs the direction of blog post sorting. */ + sortPosts: 'ascending' | 'descending'; + /** Whether to display the last date the doc was updated. */ + showLastUpdateTime: boolean; + /** Whether to display the author who last updated the doc. */ + showLastUpdateAuthor: boolean; + /** An optional function which can be used to transform blog posts + * (filter, modify, delete, etc...). + */ + processBlogPosts: ProcessBlogPostsFn; + }; /** * Feed options, as provided by user config. `type` accepts `all` as shortcut @@ -494,7 +495,7 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the [permalink: string]: BlogTag; }; - export type BlogTag = Tag & { + export type BlogTag = TagMetadata & { /** Blog post permalinks. */ items: string[]; pages: BlogPaginated[]; diff --git a/packages/docusaurus-plugin-content-blog/src/props.ts b/packages/docusaurus-plugin-content-blog/src/props.ts index 8946bc15d5..517cd4a5f8 100644 --- a/packages/docusaurus-plugin-content-blog/src/props.ts +++ b/packages/docusaurus-plugin-content-blog/src/props.ts @@ -13,6 +13,7 @@ export function toTagsProp({blogTags}: {blogTags: BlogTags}): TagsListItem[] { .map((tag) => ({ label: tag.label, permalink: tag.permalink, + description: tag.description, count: tag.items.length, })); } @@ -27,6 +28,7 @@ export function toTagProp({ return { label: tag.label, permalink: tag.permalink, + description: tag.description, allTagsPath: blogTagsListPath, count: tag.items.length, unlisted: tag.unlisted, diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/simple-site/docs/foo/baz.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/simple-site/docs/foo/baz.md index c3f676e276..de766e7cb1 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/simple-site/docs/foo/baz.md +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/simple-site/docs/foo/baz.md @@ -5,6 +5,7 @@ slug: bazSlug.html pagination_label: baz pagination_label tags: - tag 1 + - globalTag1 - tag-1 - label: tag 2 permalink: tag2-custom-permalink diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/simple-site/docs/tags.yml b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/simple-site/docs/tags.yml new file mode 100644 index 0000000000..1a12f09267 --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/simple-site/docs/tags.yml @@ -0,0 +1,5 @@ + +globalTag1: + label: 'Global Tag 1 label' + description: 'Global Tag 1 description' + permalink: 'global-tag-1-permalink' diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md index 6940424b19..7fed3dbd34 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md @@ -1,4 +1,6 @@ --- slug: / +tags: [inlineTag-v1.0.0, globalTag-v1.0.0] --- + Hello `1.0.0` ! (translated en) diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/tags.yml b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/tags.yml new file mode 100644 index 0000000000..60368a2935 --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/tags.yml @@ -0,0 +1,5 @@ + +globalTag-v1.0.0: + label: 'globalTag-v1.0.0 label (en)' + description: 'globalTag-v1.0.0 description (en)' + permalink: 'globalTag-v1.0.0 permalink (en)' diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/i18n/fr/docusaurus-plugin-content-docs/version-1.0.0/hello.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/i18n/fr/docusaurus-plugin-content-docs/version-1.0.0/hello.md index 5cf148d17a..0e2a3969e4 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/i18n/fr/docusaurus-plugin-content-docs/version-1.0.0/hello.md +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/i18n/fr/docusaurus-plugin-content-docs/version-1.0.0/hello.md @@ -1,4 +1,6 @@ --- slug: / +tags: [inlineTag-v1.0.0, globalTag-v1.0.0] --- + Hello `1.0.0` ! (translated fr) diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/i18n/fr/docusaurus-plugin-content-docs/version-1.0.0/tags.yml b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/i18n/fr/docusaurus-plugin-content-docs/version-1.0.0/tags.yml new file mode 100644 index 0000000000..7b72346f9c --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/i18n/fr/docusaurus-plugin-content-docs/version-1.0.0/tags.yml @@ -0,0 +1,5 @@ + +globalTag-v1.0.0: + label: 'globalTag-v1.0.0 label (fr)' + description: 'globalTag-v1.0.0 description (fr)' + permalink: 'globalTag-v1.0.0 permalink (fr)' diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/versioned_docs/version-1.0.0/hello.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/versioned_docs/version-1.0.0/hello.md index db13f1d75b..b535fbbfbf 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/versioned_docs/version-1.0.0/hello.md +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/versioned_docs/version-1.0.0/hello.md @@ -1,4 +1,6 @@ --- slug: / +tags: [inlineTag-v1.0.0, globalTag-v1.0.0] --- + Hello `1.0.0` ! diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/versioned_docs/version-1.0.0/tags.yml b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/versioned_docs/version-1.0.0/tags.yml new file mode 100644 index 0000000000..a348b603d0 --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/versioned_docs/version-1.0.0/tags.yml @@ -0,0 +1,5 @@ + +globalTag-v1.0.0: + label: 'globalTag-v1.0.0 label' + description: 'globalTag-v1.0.0 description' + permalink: 'globalTag-v1.0.0 permalink' diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/versioned_docs/version-1.0.1/hello.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/versioned_docs/version-1.0.1/hello.md index a9950ade90..60a7a3c869 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/versioned_docs/version-1.0.1/hello.md +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/versioned_docs/version-1.0.1/hello.md @@ -1,4 +1,6 @@ --- slug: / +tags: [inlineTag-v1.0.1, globalTag-v1.0.1] --- + Hello `1.0.1` ! diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/versioned_docs/version-1.0.1/tags.yml b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/versioned_docs/version-1.0.1/tags.yml new file mode 100644 index 0000000000..e483ca75d4 --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/versioned_docs/version-1.0.1/tags.yml @@ -0,0 +1,5 @@ + +globalTag-v1.0.1: + label: 'globalTag-v1.0.1 label' + description: 'globalTag-v1.0.1 description' + permalink: 'globalTag-v1.0.1 permalink' 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 a19421255b..048e97aaee 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 @@ -58,6 +58,7 @@ exports[`simple website content 1`] = ` "slug": "bazSlug.html", "tags": [ "tag 1", + "globalTag1", "tag-1", { "label": "tag 2", @@ -85,10 +86,26 @@ exports[`simple website content 1`] = ` "sourceDirName": "foo", "tags": [ { + "description": undefined, + "inline": true, "label": "tag 1", "permalink": "/docs/tags/tag-1", }, { + "description": "Global Tag 1 description", + "inline": false, + "label": "Global Tag 1 label", + "permalink": "/docs/tags/global-tag-1-permalink", + }, + { + "description": undefined, + "inline": true, + "label": "tag-1", + "permalink": "/docs/tags/tag-1", + }, + { + "description": undefined, + "inline": true, "label": "tag 2", "permalink": "/docs/tags/tag2-custom-permalink", }, @@ -130,10 +147,14 @@ exports[`simple website content 2`] = ` "sourceDirName": ".", "tags": [ { + "description": undefined, + "inline": true, "label": "tag-1", "permalink": "/docs/tags/tag-1", }, { + "description": undefined, + "inline": true, "label": "tag 3", "permalink": "/docs/tags/tag-3", }, @@ -567,6 +588,7 @@ exports[`simple website content: data 1`] = ` "slug": "bazSlug.html", "tags": [ "tag 1", + "globalTag1", "tag-1", { "label": "tag 2", @@ -594,10 +616,26 @@ exports[`simple website content: data 1`] = ` "sourceDirName": "foo", "tags": [ { + "description": undefined, + "inline": true, "label": "tag 1", "permalink": "/docs/tags/tag-1", }, { + "description": "Global Tag 1 description", + "inline": false, + "label": "Global Tag 1 label", + "permalink": "/docs/tags/global-tag-1-permalink", + }, + { + "description": undefined, + "inline": true, + "label": "tag-1", + "permalink": "/docs/tags/tag-1", + }, + { + "description": undefined, + "inline": true, "label": "tag 2", "permalink": "/docs/tags/tag2-custom-permalink", }, @@ -663,10 +701,14 @@ exports[`simple website content: data 1`] = ` "sourceDirName": ".", "tags": [ { + "description": undefined, + "inline": true, "label": "tag-1", "permalink": "/docs/tags/tag-1", }, { + "description": undefined, + "inline": true, "label": "tag 3", "permalink": "/docs/tags/tag-3", }, @@ -1520,22 +1562,54 @@ exports[`simple website content: route config 1`] = ` "tags": [ { "count": 2, + "description": undefined, "label": "tag 1", "permalink": "/docs/tags/tag-1", }, { "count": 1, + "description": "Global Tag 1 description", + "label": "Global Tag 1 label", + "permalink": "/docs/tags/global-tag-1-permalink", + }, + { + "count": 1, + "description": undefined, "label": "tag 2", "permalink": "/docs/tags/tag2-custom-permalink", }, { "count": 1, + "description": undefined, "label": "tag 3", "permalink": "/docs/tags/tag-3", }, ], }, }, + { + "component": "@theme/DocTagDocListPage", + "exact": true, + "path": "/docs/tags/global-tag-1-permalink", + "props": { + "tag": { + "allTagsPath": "/docs/tags", + "count": 1, + "description": "Global Tag 1 description", + "items": [ + { + "description": "Images", + "id": "foo/baz", + "permalink": "/docs/foo/bazSlug.html", + "title": "baz", + }, + ], + "label": "Global Tag 1 label", + "permalink": "/docs/tags/global-tag-1-permalink", + "unlisted": false, + }, + }, + }, { "component": "@theme/DocTagDocListPage", "exact": true, @@ -1544,6 +1618,7 @@ exports[`simple website content: route config 1`] = ` "tag": { "allTagsPath": "/docs/tags", "count": 2, + "description": undefined, "items": [ { "description": "Images", @@ -1572,6 +1647,7 @@ exports[`simple website content: route config 1`] = ` "tag": { "allTagsPath": "/docs/tags", "count": 1, + "description": undefined, "items": [ { "description": "Hi, Endilie here :)", @@ -1594,6 +1670,7 @@ exports[`simple website content: route config 1`] = ` "tag": { "allTagsPath": "/docs/tags", "count": 1, + "description": undefined, "items": [ { "description": "Images", @@ -1929,6 +2006,8 @@ exports[`simple website getPathToWatch 1`] = ` "sidebars.json", "i18n/en/docusaurus-plugin-content-docs/current/**/*.{md,mdx}", "docs/**/*.{md,mdx}", + "i18n/en/docusaurus-plugin-content-docs/current/tags.yml", + "docs/tags.yml", "docs/**/_category_.{json,yml,yaml}", ] `; @@ -3249,10 +3328,14 @@ exports[`versioned website (community) getPathToWatch 1`] = ` "community_sidebars.json", "i18n/en/docusaurus-plugin-content-docs-community/current/**/*.{md,mdx}", "community/**/*.{md,mdx}", + "i18n/en/docusaurus-plugin-content-docs-community/current/tags.yml", + "community/tags.yml", "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}", + "i18n/en/docusaurus-plugin-content-docs-community/version-1.0.0/tags.yml", + "community_versioned_docs/version-1.0.0/tags.yml", "community_versioned_docs/version-1.0.0/**/_category_.{json,yml,yaml}", ] `; @@ -3289,14 +3372,20 @@ exports[`versioned website content 1`] = ` "sourceDirName": "foo", "tags": [ { + "description": undefined, + "inline": true, "label": "barTag 1", "permalink": "/docs/next/tags/bar-tag-1", }, { + "description": undefined, + "inline": true, "label": "barTag-2", "permalink": "/docs/next/tags/bar-tag-2", }, { + "description": undefined, + "inline": true, "label": "barTag 3", "permalink": "/docs/next/tags/barTag-3-permalink", }, @@ -3370,6 +3459,10 @@ exports[`versioned website content 4`] = ` "editUrl": undefined, "frontMatter": { "slug": "/", + "tags": [ + "inlineTag-v1.0.1", + "globalTag-v1.0.1", + ], }, "id": "hello", "lastUpdatedAt": undefined, @@ -3385,7 +3478,20 @@ exports[`versioned website content 4`] = ` "slug": "/", "source": "@site/versioned_docs/version-1.0.1/hello.md", "sourceDirName": ".", - "tags": [], + "tags": [ + { + "description": undefined, + "inline": true, + "label": "inlineTag-v1.0.1", + "permalink": "/docs/tags/inline-tag-v-1-0-1", + }, + { + "description": "globalTag-v1.0.1 description", + "inline": false, + "label": "globalTag-v1.0.1 label", + "permalink": "/docs/tags/globalTag-v1.0.1 permalink", + }, + ], "title": "hello", "unlisted": false, "version": "1.0.1", @@ -3558,14 +3664,20 @@ exports[`versioned website content: data 1`] = ` "sourceDirName": "foo", "tags": [ { + "description": undefined, + "inline": true, "label": "barTag 1", "permalink": "/docs/next/tags/bar-tag-1", }, { + "description": undefined, + "inline": true, "label": "barTag-2", "permalink": "/docs/next/tags/bar-tag-2", }, { + "description": undefined, + "inline": true, "label": "barTag 3", "permalink": "/docs/next/tags/barTag-3-permalink", }, @@ -3698,6 +3810,10 @@ exports[`versioned website content: data 1`] = ` "editUrl": undefined, "frontMatter": { "slug": "/", + "tags": [ + "inlineTag-v1.0.0", + "globalTag-v1.0.0", + ], }, "id": "hello", "lastUpdatedAt": undefined, @@ -3713,7 +3829,20 @@ exports[`versioned website content: data 1`] = ` "slug": "/", "source": "@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md", "sourceDirName": ".", - "tags": [], + "tags": [ + { + "description": undefined, + "inline": true, + "label": "inlineTag-v1.0.0", + "permalink": "/docs/1.0.0/tags/inline-tag-v-1-0-0", + }, + { + "description": "globalTag-v1.0.0 description (en)", + "inline": false, + "label": "globalTag-v1.0.0 label (en)", + "permalink": "/docs/1.0.0/tags/globalTag-v1.0.0 permalink (en)", + }, + ], "title": "hello", "unlisted": false, "version": "1.0.0", @@ -3801,6 +3930,10 @@ exports[`versioned website content: data 1`] = ` "editUrl": undefined, "frontMatter": { "slug": "/", + "tags": [ + "inlineTag-v1.0.1", + "globalTag-v1.0.1", + ], }, "id": "hello", "lastUpdatedAt": undefined, @@ -3816,7 +3949,20 @@ exports[`versioned website content: data 1`] = ` "slug": "/", "source": "@site/versioned_docs/version-1.0.1/hello.md", "sourceDirName": ".", - "tags": [], + "tags": [ + { + "description": undefined, + "inline": true, + "label": "inlineTag-v1.0.1", + "permalink": "/docs/tags/inline-tag-v-1-0-1", + }, + { + "description": "globalTag-v1.0.1 description", + "inline": false, + "label": "globalTag-v1.0.1 label", + "permalink": "/docs/tags/globalTag-v1.0.1 permalink", + }, + ], "title": "hello", "unlisted": false, "version": "1.0.1", @@ -4280,6 +4426,73 @@ exports[`versioned website content: route config 1`] = ` }, }, "routes": [ + { + "component": "@theme/DocTagsListPage", + "exact": true, + "path": "/docs/1.0.0/tags", + "props": { + "tags": [ + { + "count": 1, + "description": undefined, + "label": "inlineTag-v1.0.0", + "permalink": "/docs/1.0.0/tags/inline-tag-v-1-0-0", + }, + { + "count": 1, + "description": "globalTag-v1.0.0 description (en)", + "label": "globalTag-v1.0.0 label (en)", + "permalink": "/docs/1.0.0/tags/globalTag-v1.0.0 permalink (en)", + }, + ], + }, + }, + { + "component": "@theme/DocTagDocListPage", + "exact": true, + "path": "/docs/1.0.0/tags/globalTag-v1.0.0 permalink (en)", + "props": { + "tag": { + "allTagsPath": "/docs/1.0.0/tags", + "count": 1, + "description": "globalTag-v1.0.0 description (en)", + "items": [ + { + "description": "Hello 1.0.0 ! (translated en)", + "id": "hello", + "permalink": "/docs/1.0.0/", + "title": "hello", + }, + ], + "label": "globalTag-v1.0.0 label (en)", + "permalink": "/docs/1.0.0/tags/globalTag-v1.0.0 permalink (en)", + "unlisted": false, + }, + }, + }, + { + "component": "@theme/DocTagDocListPage", + "exact": true, + "path": "/docs/1.0.0/tags/inline-tag-v-1-0-0", + "props": { + "tag": { + "allTagsPath": "/docs/1.0.0/tags", + "count": 1, + "description": undefined, + "items": [ + { + "description": "Hello 1.0.0 ! (translated en)", + "id": "hello", + "permalink": "/docs/1.0.0/", + "title": "hello", + }, + ], + "label": "inlineTag-v1.0.0", + "permalink": "/docs/1.0.0/tags/inline-tag-v-1-0-0", + "unlisted": false, + }, + }, + }, { "component": "@theme/DocRoot", "exact": false, @@ -4430,16 +4643,19 @@ exports[`versioned website content: route config 1`] = ` "tags": [ { "count": 1, + "description": undefined, "label": "barTag 1", "permalink": "/docs/next/tags/bar-tag-1", }, { "count": 1, + "description": undefined, "label": "barTag-2", "permalink": "/docs/next/tags/bar-tag-2", }, { "count": 1, + "description": undefined, "label": "barTag 3", "permalink": "/docs/next/tags/barTag-3-permalink", }, @@ -4454,6 +4670,7 @@ exports[`versioned website content: route config 1`] = ` "tag": { "allTagsPath": "/docs/next/tags", "count": 1, + "description": undefined, "items": [ { "description": "This is next version of bar.", @@ -4476,6 +4693,7 @@ exports[`versioned website content: route config 1`] = ` "tag": { "allTagsPath": "/docs/next/tags", "count": 1, + "description": undefined, "items": [ { "description": "This is next version of bar.", @@ -4498,6 +4716,7 @@ exports[`versioned website content: route config 1`] = ` "tag": { "allTagsPath": "/docs/next/tags", "count": 1, + "description": undefined, "items": [ { "description": "This is next version of bar.", @@ -4860,6 +5079,73 @@ exports[`versioned website content: route config 1`] = ` }, }, "routes": [ + { + "component": "@theme/DocTagsListPage", + "exact": true, + "path": "/docs/tags", + "props": { + "tags": [ + { + "count": 1, + "description": undefined, + "label": "inlineTag-v1.0.1", + "permalink": "/docs/tags/inline-tag-v-1-0-1", + }, + { + "count": 1, + "description": "globalTag-v1.0.1 description", + "label": "globalTag-v1.0.1 label", + "permalink": "/docs/tags/globalTag-v1.0.1 permalink", + }, + ], + }, + }, + { + "component": "@theme/DocTagDocListPage", + "exact": true, + "path": "/docs/tags/globalTag-v1.0.1 permalink", + "props": { + "tag": { + "allTagsPath": "/docs/tags", + "count": 1, + "description": "globalTag-v1.0.1 description", + "items": [ + { + "description": "Hello 1.0.1 !", + "id": "hello", + "permalink": "/docs/", + "title": "hello", + }, + ], + "label": "globalTag-v1.0.1 label", + "permalink": "/docs/tags/globalTag-v1.0.1 permalink", + "unlisted": false, + }, + }, + }, + { + "component": "@theme/DocTagDocListPage", + "exact": true, + "path": "/docs/tags/inline-tag-v-1-0-1", + "props": { + "tag": { + "allTagsPath": "/docs/tags", + "count": 1, + "description": undefined, + "items": [ + { + "description": "Hello 1.0.1 !", + "id": "hello", + "permalink": "/docs/", + "title": "hello", + }, + ], + "label": "inlineTag-v1.0.1", + "permalink": "/docs/tags/inline-tag-v-1-0-1", + "unlisted": false, + }, + }, + }, { "component": "@theme/DocRoot", "exact": false, @@ -4925,18 +5211,26 @@ exports[`versioned website getPathToWatch 1`] = ` "sidebars.json", "i18n/en/docusaurus-plugin-content-docs/current/**/*.{md,mdx}", "docs/**/*.{md,mdx}", + "i18n/en/docusaurus-plugin-content-docs/current/tags.yml", + "docs/tags.yml", "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}", + "i18n/en/docusaurus-plugin-content-docs/version-1.0.1/tags.yml", + "versioned_docs/version-1.0.1/tags.yml", "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}", + "i18n/en/docusaurus-plugin-content-docs/version-1.0.0/tags.yml", + "versioned_docs/version-1.0.0/tags.yml", "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}", + "i18n/en/docusaurus-plugin-content-docs/version-withSlugs/tags.yml", + "versioned_docs/version-withSlugs/tags.yml", "versioned_docs/version-withSlugs/**/_category_.{json,yml,yaml}", ] `; 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 ef2b03de79..2e033b2f1c 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts @@ -6,7 +6,7 @@ */ import {jest} from '@jest/globals'; -import path from 'path'; +import * as path from 'path'; import {loadContext} from '@docusaurus/core/src/server/site'; import { createSlugger, @@ -14,6 +14,7 @@ import { DEFAULT_PLUGIN_ID, LAST_UPDATE_FALLBACK, } from '@docusaurus/utils'; +import {getTagsFile} from '@docusaurus/utils-validation'; import {createSidebarsUtils} from '../sidebars/utils'; import { processDocMetadata, @@ -73,13 +74,18 @@ type TestUtilsArg = { env?: DocEnv; }; -function createTestUtils({ +async function createTestUtils({ siteDir, context, versionMetadata, options, env = 'production', }: TestUtilsArg) { + const tagsFile = await getTagsFile({ + contentPaths: versionMetadata, + tags: options.tags, + }); + async function readDoc(docFileSource: string) { return readDocFile(versionMetadata, docFileSource); } @@ -93,6 +99,7 @@ function createTestUtils({ options, context, env, + tagsFile, }); } @@ -139,6 +146,7 @@ function createTestUtils({ context, options, env, + tagsFile: null, }); expect(metadata.permalink).toEqual(expectedPermalink); } @@ -159,6 +167,7 @@ function createTestUtils({ context, options, env, + tagsFile: null, }), ), ); @@ -181,7 +190,6 @@ function createTestUtils({ pagination: addDocNavigation({ docs: rawDocs, sidebarsUtils, - sidebarFilePath: versionMetadata.sidebarFilePath as string, }).map((doc) => ({prev: doc.previous, next: doc.next, id: doc.id})), sidebars, }; @@ -214,7 +222,7 @@ describe('simple site', () => { expect(versionsMetadata).toHaveLength(1); const currentVersion = versionsMetadata[0]!; - function createTestUtilsPartial(args: Partial) { + async function createTestUtilsPartial(args: Partial) { return createTestUtils({ siteDir, context, @@ -224,7 +232,7 @@ describe('simple site', () => { }); } - const defaultTestUtils = createTestUtilsPartial({}); + const defaultTestUtils = await createTestUtilsPartial({}); return { siteDir, @@ -296,6 +304,7 @@ describe('simple site', () => { slug: '/', title: 'Hello, World !', description: `Hi, Endilie here :)`, + sidebarPosition: undefined, frontMatter: { id: 'hello', title: 'Hello, World !', @@ -306,11 +315,15 @@ describe('simple site', () => { tags: [ { label: 'tag-1', + inline: true, permalink: '/docs/tags/tag-1', + description: undefined, }, { label: 'tag 3', + inline: true, permalink: '/docs/tags/tag-3', + description: undefined, }, ], unlisted: false, @@ -325,7 +338,7 @@ describe('simple site', () => { }, }); - const testUtilsLocal = createTestUtilsPartial({ + const testUtilsLocal = await createTestUtilsPartial({ siteDir, context, options, @@ -339,6 +352,7 @@ describe('simple site', () => { permalink: '/docs/foo/bazSlug.html', slug: '/foo/bazSlug.html', title: 'baz', + sidebarPosition: undefined, editUrl: 'https://github.com/facebook/docusaurus/edit/main/website/docs/foo/baz.md', description: 'Images', @@ -349,17 +363,34 @@ describe('simple site', () => { pagination_label: 'baz pagination_label', tags: [ 'tag 1', + 'globalTag1', 'tag-1', {label: 'tag 2', permalink: 'tag2-custom-permalink'}, ], }, tags: [ { + description: undefined, + inline: true, label: 'tag 1', permalink: '/docs/tags/tag-1', }, + { + description: 'Global Tag 1 description', + inline: false, + label: 'Global Tag 1 label', + permalink: '/docs/tags/global-tag-1-permalink', + }, + { + description: undefined, + inline: true, + label: 'tag-1', + permalink: '/docs/tags/tag-1', + }, { label: 'tag 2', + description: undefined, + inline: true, permalink: '/docs/tags/tag2-custom-permalink', }, ], @@ -400,7 +431,7 @@ describe('simple site', () => { }, }); - const testUtilsLocal = createTestUtilsPartial({ + const testUtilsLocal = await createTestUtilsPartial({ siteDir, context, options, @@ -416,6 +447,7 @@ describe('simple site', () => { title: 'baz', editUrl: hardcodedEditUrl, description: 'Images', + sidebarPosition: undefined, frontMatter: { id: 'baz', slug: 'bazSlug.html', @@ -423,16 +455,33 @@ describe('simple site', () => { pagination_label: 'baz pagination_label', tags: [ 'tag 1', + 'globalTag1', 'tag-1', {label: 'tag 2', permalink: 'tag2-custom-permalink'}, ], }, tags: [ { + description: undefined, + inline: true, label: 'tag 1', permalink: '/docs/tags/tag-1', }, { + description: 'Global Tag 1 description', + inline: false, + label: 'Global Tag 1 label', + permalink: '/docs/tags/global-tag-1-permalink', + }, + { + description: undefined, + inline: true, + label: 'tag-1', + permalink: '/docs/tags/tag-1', + }, + { + description: undefined, + inline: true, label: 'tag 2', permalink: '/docs/tags/tag2-custom-permalink', }, @@ -459,7 +508,7 @@ describe('simple site', () => { }, }); - const testUtilsLocal = createTestUtilsPartial({ + const testUtilsLocal = await createTestUtilsPartial({ siteDir, context, options, @@ -489,7 +538,7 @@ describe('simple site', () => { it('docs with draft frontmatter', async () => { const {createTestUtilsPartial} = await loadSite(); - const testUtilsProd = createTestUtilsPartial({ + const testUtilsProd = await createTestUtilsPartial({ env: 'production', }); await expect( @@ -498,7 +547,7 @@ describe('simple site', () => { draft: true, }); - const testUtilsDev = createTestUtilsPartial({ + const testUtilsDev = await createTestUtilsPartial({ env: 'development', }); await expect( @@ -526,7 +575,7 @@ describe('simple site', () => { tags: [], }; - const testUtilsProd = createTestUtilsPartial({ + const testUtilsProd = await createTestUtilsPartial({ env: 'production', }); @@ -535,7 +584,7 @@ describe('simple site', () => { unlisted: true, }); - const testUtilsDev = createTestUtilsPartial({ + const testUtilsDev = await createTestUtilsPartial({ env: 'development', }); @@ -554,7 +603,7 @@ describe('simple site', () => { }, }); - const testUtilsLocal = createTestUtilsPartial({ + const testUtilsLocal = await createTestUtilsPartial({ siteDir, context, options, @@ -593,7 +642,7 @@ describe('simple site', () => { }, }); - const testUtilsLocal = createTestUtilsPartial({ + const testUtilsLocal = await createTestUtilsPartial({ siteDir, context, options, @@ -631,7 +680,7 @@ describe('simple site', () => { }, }); - const testUtilsLocal = createTestUtilsPartial({ + const testUtilsLocal = await createTestUtilsPartial({ siteDir, context, options, @@ -669,7 +718,7 @@ describe('simple site', () => { }, }); - const testUtilsLocal = createTestUtilsPartial({ + const testUtilsLocal = await createTestUtilsPartial({ siteDir, context, options, @@ -761,7 +810,7 @@ describe('simple site', () => { it('custom pagination - production', async () => { const {createTestUtilsPartial, options, versionsMetadata} = await loadSite(); - const testUtils = createTestUtilsPartial({env: 'production'}); + const testUtils = await createTestUtilsPartial({env: 'production'}); const docs = await readVersionDocs(versionsMetadata[0]!, options); await expect(testUtils.generateNavigation(docs)).resolves.toMatchSnapshot(); }); @@ -769,7 +818,7 @@ describe('simple site', () => { it('custom pagination - development', async () => { const {createTestUtilsPartial, options, versionsMetadata} = await loadSite(); - const testUtils = createTestUtilsPartial({env: 'development'}); + const testUtils = await createTestUtilsPartial({env: 'development'}); const docs = await readVersionDocs(versionsMetadata[0]!, options); await expect(testUtils.generateNavigation(docs)).resolves.toMatchSnapshot(); }); @@ -818,27 +867,27 @@ describe('versioned site', () => { const version100 = versionsMetadata[2]!; const versionWithSlugs = versionsMetadata[3]!; - const currentVersionTestUtils = createTestUtils({ + const currentVersionTestUtils = await createTestUtils({ siteDir, context, options, versionMetadata: currentVersion, }); - const version101TestUtils = createTestUtils({ + const version101TestUtils = await createTestUtils({ siteDir, context, options, versionMetadata: version101, }); - const version100TestUtils = createTestUtils({ + const version100TestUtils = await createTestUtils({ siteDir, context, options, versionMetadata: version100, }); - const versionWithSlugsTestUtils = createTestUtils({ + const versionWithSlugsTestUtils = await createTestUtils({ siteDir, context, options, @@ -869,6 +918,7 @@ describe('versioned site', () => { slug: '/foo/barSlug', title: 'bar', description: 'This is next version of bar.', + sidebarPosition: undefined, frontMatter: { slug: 'barSlug', tags: [ @@ -883,15 +933,21 @@ describe('versioned site', () => { tags: [ { label: 'barTag 1', + inline: true, permalink: '/docs/next/tags/bar-tag-1', + description: undefined, }, { label: 'barTag-2', + inline: true, permalink: '/docs/next/tags/bar-tag-2', + description: undefined, }, { label: 'barTag 3', + inline: true, permalink: '/docs/next/tags/barTag-3-permalink', + description: undefined, }, ], unlisted: false, @@ -936,11 +992,25 @@ describe('versioned site', () => { description: 'Hello 1.0.0 ! (translated en)', frontMatter: { slug: '/', + tags: ['inlineTag-v1.0.0', 'globalTag-v1.0.0'], }, version: '1.0.0', source: '@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md', - tags: [], + tags: [ + { + description: undefined, + inline: true, + label: 'inlineTag-v1.0.0', + permalink: '/docs/1.0.0/tags/inline-tag-v-1-0-0', + }, + { + description: 'globalTag-v1.0.0 description (en)', + inline: false, + label: 'globalTag-v1.0.0 label (en)', + permalink: '/docs/1.0.0/tags/globalTag-v1.0.0 permalink (en)', + }, + ], unlisted: false, }); await version101TestUtils.testMeta(path.join('foo', 'bar.md'), { @@ -965,8 +1035,22 @@ describe('versioned site', () => { version: '1.0.1', frontMatter: { slug: '/', + tags: ['inlineTag-v1.0.1', 'globalTag-v1.0.1'], }, - tags: [], + tags: [ + { + description: undefined, + inline: true, + label: 'inlineTag-v1.0.1', + permalink: '/docs/tags/inline-tag-v-1-0-1', + }, + { + description: 'globalTag-v1.0.1 description', + inline: false, + label: 'globalTag-v1.0.1 label', + permalink: '/docs/tags/globalTag-v1.0.1 permalink', + }, + ], unlisted: false, }); }); @@ -1041,7 +1125,7 @@ describe('versioned site', () => { }, }); - const testUtilsLocal = createTestUtils({ + const testUtilsLocal = await createTestUtils({ siteDir, context, options, @@ -1057,12 +1141,26 @@ describe('versioned site', () => { description: 'Hello 1.0.0 ! (translated en)', frontMatter: { slug: '/', + tags: ['inlineTag-v1.0.0', 'globalTag-v1.0.0'], }, version: '1.0.0', source: '@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md', editUrl: hardcodedEditUrl, - tags: [], + tags: [ + { + description: undefined, + inline: true, + label: 'inlineTag-v1.0.0', + permalink: '/docs/1.0.0/tags/inline-tag-v-1-0-0', + }, + { + description: 'globalTag-v1.0.0 description (en)', + inline: false, + label: 'globalTag-v1.0.0 label (en)', + permalink: '/docs/1.0.0/tags/globalTag-v1.0.0 permalink (en)', + }, + ], unlisted: false, }); @@ -1083,7 +1181,7 @@ describe('versioned site', () => { }, }); - const testUtilsLocal = createTestUtils({ + const testUtilsLocal = await createTestUtils({ siteDir, context, options, @@ -1099,13 +1197,27 @@ describe('versioned site', () => { description: 'Hello 1.0.0 ! (translated en)', frontMatter: { slug: '/', + tags: ['inlineTag-v1.0.0', 'globalTag-v1.0.0'], }, version: '1.0.0', source: '@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md', editUrl: 'https://github.com/facebook/docusaurus/edit/main/website/versioned_docs/version-1.0.0/hello.md', - tags: [], + tags: [ + { + description: undefined, + inline: true, + label: 'inlineTag-v1.0.0', + permalink: '/docs/1.0.0/tags/inline-tag-v-1-0-0', + }, + { + description: 'globalTag-v1.0.0 description (en)', + inline: false, + label: 'globalTag-v1.0.0 label (en)', + permalink: '/docs/1.0.0/tags/globalTag-v1.0.0 permalink (en)', + }, + ], unlisted: false, }); }); @@ -1118,7 +1230,7 @@ describe('versioned site', () => { }, }); - const testUtilsLocal = createTestUtils({ + const testUtilsLocal = await createTestUtils({ siteDir, context, options, @@ -1134,13 +1246,27 @@ describe('versioned site', () => { description: 'Hello 1.0.0 ! (translated en)', frontMatter: { slug: '/', + tags: ['inlineTag-v1.0.0', 'globalTag-v1.0.0'], }, version: '1.0.0', source: '@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md', editUrl: 'https://github.com/facebook/docusaurus/edit/main/website/docs/hello.md', - tags: [], + tags: [ + { + description: undefined, + inline: true, + label: 'inlineTag-v1.0.0', + permalink: '/docs/1.0.0/tags/inline-tag-v-1-0-0', + }, + { + description: 'globalTag-v1.0.0 description (en)', + inline: false, + label: 'globalTag-v1.0.0 label (en)', + permalink: '/docs/1.0.0/tags/globalTag-v1.0.0 permalink (en)', + }, + ], unlisted: false, }); }); @@ -1154,7 +1280,7 @@ describe('versioned site', () => { locale: 'fr', }); - const testUtilsLocal = createTestUtils({ + const testUtilsLocal = await createTestUtils({ siteDir, context, options, @@ -1170,13 +1296,27 @@ describe('versioned site', () => { description: 'Hello 1.0.0 ! (translated fr)', frontMatter: { slug: '/', + tags: ['inlineTag-v1.0.0', 'globalTag-v1.0.0'], }, version: '1.0.0', source: '@site/i18n/fr/docusaurus-plugin-content-docs/version-1.0.0/hello.md', editUrl: 'https://github.com/facebook/docusaurus/edit/main/website/i18n/fr/docusaurus-plugin-content-docs/version-1.0.0/hello.md', - tags: [], + tags: [ + { + description: undefined, + inline: true, + label: 'inlineTag-v1.0.0', + permalink: '/fr/docs/1.0.0/tags/inline-tag-v-1-0-0', + }, + { + description: 'globalTag-v1.0.0 description (fr)', + inline: false, + label: 'globalTag-v1.0.0 label (fr)', + permalink: '/fr/docs/1.0.0/tags/globalTag-v1.0.0 permalink (fr)', + }, + ], unlisted: false, }); }); @@ -1191,7 +1331,7 @@ describe('versioned site', () => { locale: 'fr', }); - const testUtilsLocal = createTestUtils({ + const testUtilsLocal = await createTestUtils({ siteDir, context, options, @@ -1207,13 +1347,27 @@ describe('versioned site', () => { description: 'Hello 1.0.0 ! (translated fr)', frontMatter: { slug: '/', + tags: ['inlineTag-v1.0.0', 'globalTag-v1.0.0'], }, version: '1.0.0', source: '@site/i18n/fr/docusaurus-plugin-content-docs/version-1.0.0/hello.md', editUrl: 'https://github.com/facebook/docusaurus/edit/main/website/i18n/fr/docusaurus-plugin-content-docs/current/hello.md', - tags: [], + tags: [ + { + description: undefined, + inline: true, + label: 'inlineTag-v1.0.0', + permalink: '/fr/docs/1.0.0/tags/inline-tag-v-1-0-0', + }, + { + description: 'globalTag-v1.0.0 description (fr)', + inline: false, + label: 'globalTag-v1.0.0 label (fr)', + permalink: '/fr/docs/1.0.0/tags/globalTag-v1.0.0 permalink (fr)', + }, + ], unlisted: false, }); }); 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 260eaccb22..db43660088 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/options.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/options.test.ts @@ -70,6 +70,8 @@ describe('normalizeDocsPluginOptions', () => { disableVersioning: true, editCurrentVersion: true, editLocalizedFiles: true, + tags: 'docsTags.yml', + onInlineTags: 'throw', versions: { current: { path: 'next', @@ -266,4 +268,68 @@ describe('normalizeDocsPluginOptions', () => { }).sidebarCollapsed, ).toBe(false); }); + + describe('tags', () => { + it('accepts tags - undefined', () => { + expect(testValidate({tags: undefined}).tags).toBeUndefined(); + }); + + it('accepts tags - null', () => { + expect(testValidate({tags: null}).tags).toBeNull(); + }); + + it('accepts tags - false', () => { + expect(testValidate({tags: false}).tags).toBeFalsy(); + }); + + it('accepts tags - customTags.yml', () => { + expect(testValidate({tags: 'customTags.yml'}).tags).toBe( + 'customTags.yml', + ); + }); + + it('rejects tags - 42', () => { + // @ts-expect-error: test + expect(() => testValidate({tags: 42})).toThrowErrorMatchingInlineSnapshot( + `""tags" must be a string"`, + ); + }); + }); + + describe('onInlineTags', () => { + it('accepts onInlineTags - undefined', () => { + expect(testValidate({onInlineTags: undefined}).onInlineTags).toBe('warn'); + }); + + it('accepts onInlineTags - "throw"', () => { + expect(testValidate({onInlineTags: 'throw'}).onInlineTags).toBe('throw'); + }); + + it('rejects onInlineTags - "trace"', () => { + expect(() => + // @ts-expect-error: test + testValidate({onInlineTags: 'trace'}), + ).toThrowErrorMatchingInlineSnapshot( + `""onInlineTags" must be one of [ignore, log, warn, throw]"`, + ); + }); + + it('rejects onInlineTags - null', () => { + expect(() => + // @ts-expect-error: test + testValidate({onInlineTags: 42}), + ).toThrowErrorMatchingInlineSnapshot( + `""onInlineTags" must be one of [ignore, log, warn, throw]"`, + ); + }); + + it('rejects onInlineTags - 42', () => { + expect(() => + // @ts-expect-error: test + testValidate({onInlineTags: 42}), + ).toThrowErrorMatchingInlineSnapshot( + `""onInlineTags" must be one of [ignore, log, warn, throw]"`, + ); + }); + }); }); diff --git a/packages/docusaurus-plugin-content-docs/src/docs.ts b/packages/docusaurus-plugin-content-docs/src/docs.ts index 7c94593fa0..e3eb7cc47f 100644 --- a/packages/docusaurus-plugin-content-docs/src/docs.ts +++ b/packages/docusaurus-plugin-content-docs/src/docs.ts @@ -17,15 +17,16 @@ import { parseMarkdownFile, posixPath, Globby, - normalizeFrontMatterTags, isUnlisted, isDraft, readLastUpdateData, + normalizeTags, } from '@docusaurus/utils'; import {validateDocFrontMatter} from './frontMatter'; import getSlug from './slug'; import {stripPathNumberPrefixes} from './numberPrefix'; import {toDocNavigationLink, toNavigationLink} from './sidebars/utils'; +import type {TagsFile} from '@docusaurus/utils'; import type { MetadataOptions, PluginOptions, @@ -82,12 +83,14 @@ async function doProcessDocMetadata({ context, options, env, + tagsFile, }: { docFile: DocFile; versionMetadata: VersionMetadata; context: LoadContext; options: MetadataOptions; env: DocEnv; + tagsFile: TagsFile | null; }): Promise { const {source, content, contentPath, filePath} = docFile; const { @@ -206,6 +209,14 @@ async function doProcessDocMetadata({ const draft = isDraft({env, frontMatter}); const unlisted = isUnlisted({env, frontMatter}); + const tags = normalizeTags({ + options, + source, + frontMatterTags: frontMatter.tags, + tagsBaseRoutePath: versionMetadata.tagsPath, + tagsFile, + }); + // Assign all of object properties during instantiation (if possible) for // NodeJS optimization. // Adding properties to object after instantiation will cause hidden @@ -221,7 +232,7 @@ async function doProcessDocMetadata({ draft, unlisted, editUrl: customEditURL !== undefined ? customEditURL : getDocEditUrl(), - tags: normalizeFrontMatterTags(versionMetadata.tagsPath, frontMatter.tags), + tags, version: versionMetadata.versionName, lastUpdatedBy: lastUpdate.lastUpdatedBy, lastUpdatedAt: lastUpdate.lastUpdatedAt, @@ -236,6 +247,7 @@ export async function processDocMetadata(args: { context: LoadContext; options: MetadataOptions; env: DocEnv; + tagsFile: TagsFile | null; }): Promise { try { return await doProcessDocMetadata(args); diff --git a/packages/docusaurus-plugin-content-docs/src/index.ts b/packages/docusaurus-plugin-content-docs/src/index.ts index baf8b8a4f0..0f0ae32458 100644 --- a/packages/docusaurus-plugin-content-docs/src/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/index.ts @@ -20,6 +20,10 @@ import { resolveMarkdownLinkPathname, DEFAULT_PLUGIN_ID, } from '@docusaurus/utils'; +import { + getTagsFile, + getTagsFilePathsToWatch, +} from '@docusaurus/utils-validation'; import {loadSidebars, resolveSidebarPathOption} from './sidebars'; import {CategoryMetadataFilenamePattern} from './sidebars/generator'; import { @@ -43,6 +47,7 @@ import { } from './translations'; import {createAllRoutes} from './routes'; import {createSidebarsUtils} from './sidebars/utils'; +import type {TagsFile} from '@docusaurus/utils'; import type {Options as MDXLoaderOptions} from '@docusaurus/mdx-loader'; import type { @@ -119,6 +124,10 @@ export default async function pluginContentDocs( (docsDirPath) => `${docsDirPath}/${pattern}`, ), ), + ...getTagsFilePathsToWatch({ + contentPaths: version, + tags: options.tags, + }), `${version.contentPath}/**/${CategoryMetadataFilenamePattern}`, ]; if (typeof version.sidebarFilePath === 'string') { @@ -133,6 +142,7 @@ export default async function pluginContentDocs( async loadContent() { async function loadVersionDocsBase( versionMetadata: VersionMetadata, + tagsFile: TagsFile | null, ): Promise { const docFiles = await readVersionDocs(versionMetadata, options); if (docFiles.length === 0) { @@ -152,6 +162,7 @@ export default async function pluginContentDocs( context, options, env, + tagsFile, }); } return Promise.all(docFiles.map(processVersionDoc)); @@ -160,8 +171,14 @@ export default async function pluginContentDocs( async function doLoadVersion( versionMetadata: VersionMetadata, ): Promise { + const tagsFile = await getTagsFile({ + contentPaths: versionMetadata, + tags: options.tags, + }); + const docsBase: DocMetadataBase[] = await loadVersionDocsBase( versionMetadata, + tagsFile, ); // TODO we only ever need draftIds in further code, not full draft items diff --git a/packages/docusaurus-plugin-content-docs/src/options.ts b/packages/docusaurus-plugin-content-docs/src/options.ts index a7c2e1d1e1..329341b5e5 100644 --- a/packages/docusaurus-plugin-content-docs/src/options.ts +++ b/packages/docusaurus-plugin-content-docs/src/options.ts @@ -54,6 +54,8 @@ export const DEFAULT_OPTIONS: Omit = { sidebarCollapsible: true, sidebarCollapsed: true, breadcrumbs: true, + onInlineTags: 'warn', + tags: undefined, }; const VersionOptionsSchema = Joi.object({ @@ -140,6 +142,13 @@ const OptionsSchema = Joi.object({ lastVersion: Joi.string().optional(), versions: VersionsOptionsSchema, breadcrumbs: Joi.bool().default(DEFAULT_OPTIONS.breadcrumbs), + onInlineTags: Joi.string() + .equal('ignore', 'log', 'warn', 'throw') + .default(DEFAULT_OPTIONS.onInlineTags), + tags: Joi.string() + .disallow('') + .allow(null, false) + .default(() => DEFAULT_OPTIONS.tags), }); export function validateOptions({ diff --git a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts index bae47646fb..806cf280ce 100644 --- a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts +++ b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts @@ -15,9 +15,10 @@ declare module '@docusaurus/plugin-content-docs' { FrontMatterTag, TagsListItem, TagModule, - Tag, FrontMatterLastUpdate, LastUpdateData, + TagMetadata, + TagsPluginOptions, } from '@docusaurus/utils'; import type {Plugin, LoadContext} from '@docusaurus/types'; import type {Overwrite, Required} from 'utility-types'; @@ -64,7 +65,7 @@ declare module '@docusaurus/plugin-content-docs' { locale: string; }) => string | undefined; - export type MetadataOptions = { + export type MetadataOptions = TagsPluginOptions & { /** * URL route for the docs section of your site. **DO NOT** include a * trailing slash. Use `/` for shipping docs without base path. @@ -446,7 +447,7 @@ declare module '@docusaurus/plugin-content-docs' { */ editUrl?: string | null; /** Tags, normalized. */ - tags: Tag[]; + tags: TagMetadata[]; /** Front matter, as-is. */ frontMatter: DocFrontMatter & {[key: string]: unknown}; }; diff --git a/packages/docusaurus-plugin-content-docs/src/props.ts b/packages/docusaurus-plugin-content-docs/src/props.ts index 464ebc5e90..c34211fad1 100644 --- a/packages/docusaurus-plugin-content-docs/src/props.ts +++ b/packages/docusaurus-plugin-content-docs/src/props.ts @@ -206,6 +206,7 @@ export function toTagDocListProp({ return { label: tag.label, permalink: tag.permalink, + description: tag.description, allTagsPath, count: tag.docIds.length, items: toDocListProp(), @@ -221,6 +222,7 @@ export function toTagsListTagsProp( .map((tagValue) => ({ label: tagValue.label, permalink: tagValue.permalink, + description: tagValue.description, count: tagValue.docIds.length, })); } diff --git a/packages/docusaurus-plugin-content-docs/src/routes.ts b/packages/docusaurus-plugin-content-docs/src/routes.ts index 14c55f1540..cde7d88312 100644 --- a/packages/docusaurus-plugin-content-docs/src/routes.ts +++ b/packages/docusaurus-plugin-content-docs/src/routes.ts @@ -11,19 +11,20 @@ import { docuHash, normalizeUrl, aliasedSitePathToRelativePath, + groupTaggedItems, + getTagVisibility, } from '@docusaurus/utils'; import { toTagDocListProp, toTagsListTagsProp, toVersionMetadataProp, } from './props'; -import {getVersionTags} from './tags'; import type { PluginContentLoadedActions, RouteConfig, RouteMetadata, } from '@docusaurus/types'; -import type {FullVersion, VersionTag} from './types'; +import type {FullVersion, VersionTag, VersionTags} from './types'; import type { CategoryGeneratedIndexMetadata, DocMetadata, @@ -112,6 +113,23 @@ async function buildVersionSidebarRoute(param: BuildVersionRoutesParam) { routes: subRoutes, }; } +function getVersionTags(docs: DocMetadata[]): VersionTags { + const groups = groupTaggedItems(docs, (doc) => doc.tags); + return _.mapValues(groups, ({tag, items: tagDocs}) => { + const tagVisibility = getTagVisibility({ + items: tagDocs, + isUnlisted: (item) => item.unlisted, + }); + return { + inline: tag.inline, + label: tag.label, + permalink: tag.permalink, + description: tag.description, + docIds: tagVisibility.listedItems.map((item) => item.id), + unlisted: tagVisibility.unlisted, + }; + }); +} async function buildVersionTagsRoutes( param: BuildVersionRoutesParam, @@ -120,8 +138,9 @@ async function buildVersionTagsRoutes( const versionTags = getVersionTags(version.docs); async function buildTagsListRoute(): Promise { + const tags = toTagsListTagsProp(versionTags); // Don't create a tags list page if there's no tag - if (Object.keys(versionTags).length === 0) { + if (tags.length === 0) { return null; } return { @@ -129,7 +148,7 @@ async function buildVersionTagsRoutes( exact: true, component: options.docTagsListComponent, props: { - tags: toTagsListTagsProp(versionTags), + tags, }, }; } diff --git a/packages/docusaurus-plugin-content-docs/src/tags.ts b/packages/docusaurus-plugin-content-docs/src/tags.ts deleted file mode 100644 index 1fd784c213..0000000000 --- a/packages/docusaurus-plugin-content-docs/src/tags.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * 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 _ from 'lodash'; -import {getTagVisibility, groupTaggedItems} from '@docusaurus/utils'; -import type {VersionTags} from './types'; -import type {DocMetadata} from '@docusaurus/plugin-content-docs'; - -export function getVersionTags(docs: DocMetadata[]): VersionTags { - const groups = groupTaggedItems(docs, (doc) => doc.tags); - return _.mapValues(groups, ({tag, items: tagDocs}) => { - const tagVisibility = getTagVisibility({ - items: tagDocs, - isUnlisted: (item) => item.unlisted, - }); - return { - label: tag.label, - docIds: tagVisibility.listedItems.map((item) => item.id), - permalink: tag.permalink, - unlisted: tagVisibility.unlisted, - }; - }); -} diff --git a/packages/docusaurus-plugin-content-docs/src/types.ts b/packages/docusaurus-plugin-content-docs/src/types.ts index c44c60896a..208675c72a 100644 --- a/packages/docusaurus-plugin-content-docs/src/types.ts +++ b/packages/docusaurus-plugin-content-docs/src/types.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import type {Tag} from '@docusaurus/utils'; +import type {TagMetadata} from '@docusaurus/utils'; import type { LoadedVersion, CategoryGeneratedIndexMetadata, @@ -23,7 +23,7 @@ export type SourceToPermalink = { [source: string]: string; }; -export type VersionTag = Tag & { +export type VersionTag = TagMetadata & { /** All doc ids having this tag. */ docIds: string[]; unlisted: boolean; diff --git a/packages/docusaurus-theme-classic/src/theme/BlogTagsPostsPage/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogTagsPostsPage/index.tsx index c93074a3a3..45f71008db 100644 --- a/packages/docusaurus-theme-classic/src/theme/BlogTagsPostsPage/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/BlogTagsPostsPage/index.tsx @@ -57,7 +57,7 @@ function BlogTagsPostsPageMetadata({tag}: Props): JSX.Element { const title = useBlogTagsPostsPageTitle(tag); return ( <> - + ); @@ -75,6 +75,7 @@ function BlogTagsPostsPageContent({ {tag.unlisted && }
{title} + {tag.description &&

{tag.description}

} - + ); @@ -85,6 +86,7 @@ function DocTagDocListPageContent({ {tag.unlisted && }
{title} + {tag.description &&

{tag.description}

}
    - {tags.map(({label, permalink: tagPermalink}) => ( -
  • - + {tags.map((tag) => ( +
  • +
  • ))}
diff --git a/packages/docusaurus-utils-validation/package.json b/packages/docusaurus-utils-validation/package.json index 96279a3b30..9813db6cd6 100644 --- a/packages/docusaurus-utils-validation/package.json +++ b/packages/docusaurus-utils-validation/package.json @@ -21,10 +21,15 @@ "@docusaurus/logger": "3.3.2", "@docusaurus/utils": "3.3.2", "@docusaurus/utils-common": "3.3.2", + "fs-extra": "^11.2.0", "joi": "^17.9.2", "js-yaml": "^4.1.0", + "lodash": "^4.17.21", "tslib": "^2.6.0" }, + "devDependencies": { + "tmp-promise": "^3.0.3" + }, "engines": { "node": ">=18.0" } diff --git a/packages/docusaurus-utils-validation/src/__tests__/tagsFile.test.ts b/packages/docusaurus-utils-validation/src/__tests__/tagsFile.test.ts new file mode 100644 index 0000000000..149c56a142 --- /dev/null +++ b/packages/docusaurus-utils-validation/src/__tests__/tagsFile.test.ts @@ -0,0 +1,538 @@ +/** + * 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 * as path from 'path'; +import * as fs from 'fs-extra'; +import * as tmp from 'tmp-promise'; +import * as YAML from 'js-yaml'; +import { + ensureUniquePermalinks, + getTagsFile, + getTagsFilePathsToWatch, + normalizeTagsFile, +} from '../tagsFile'; +import type {TagsFile, TagsFileInput} from '@docusaurus/utils'; + +describe('ensureUniquePermalinks', () => { + it('throw when one duplicate permalink found', () => { + const definedTags: TagsFile = { + open: { + label: 'Open Source', + permalink: '/custom-open-source', + description: 'Learn about the open source', + }, + closed: { + label: 'Closed Source', + permalink: '/custom-open-source', + description: 'Learn about the closed source', + }, + }; + + expect(() => ensureUniquePermalinks(definedTags)) + .toThrowErrorMatchingInlineSnapshot(` + "Duplicate permalinks found in tags file: + - /custom-open-source" + `); + }); + + it('throw when multiple duplicate permalink found', () => { + const definedTags: TagsFile = { + open: { + label: 'Open Source', + permalink: '/custom-open-source', + description: 'Learn about the open source', + }, + closed: { + label: 'Closed Source', + permalink: '/custom-open-source', + description: 'Learn about the closed source', + }, + hello: { + label: 'Hello', + permalink: '/hello', + description: 'Learn about the hello', + }, + world: { + label: 'Hello', + permalink: '/hello', + description: 'Learn about the world', + }, + }; + + expect(() => ensureUniquePermalinks(definedTags)) + .toThrowErrorMatchingInlineSnapshot(` + "Duplicate permalinks found in tags file: + - /custom-open-source + - /hello" + `); + }); + + it('do not throw when no duplicate permalink found', () => { + const definedTags: TagsFile = { + open: { + label: 'Open Source', + permalink: '/open-source', + description: 'Learn about the open source', + }, + closed: { + label: 'Closed Source', + permalink: '/closed-source', + description: 'Learn about the closed source', + }, + }; + + expect(() => ensureUniquePermalinks(definedTags)).not.toThrow(); + }); +}); + +describe('normalizeTagsFile', () => { + it('normalize null tag', () => { + const input: TagsFileInput = { + 'kebab case test': null, + }; + + const expectedOutput: TagsFile = { + 'kebab case test': { + description: undefined, + label: 'Kebab case test', + permalink: '/kebab-case-test', + }, + }; + + expect(normalizeTagsFile(input)).toEqual(expectedOutput); + }); + + it('normalize partial tag with label', () => { + const input: TagsFileInput = { + world: {label: 'WORLD'}, + }; + + const expectedOutput: TagsFile = { + world: { + description: undefined, + label: 'WORLD', + permalink: '/world', + }, + }; + + expect(normalizeTagsFile(input)).toEqual(expectedOutput); + }); + + it('normalize partial tag with description', () => { + const input: TagsFileInput = { + world: {description: 'World description test'}, + }; + + const expectedOutput: TagsFile = { + world: { + description: 'World description test', + label: 'World', + permalink: '/world', + }, + }; + + expect(normalizeTagsFile(input)).toEqual(expectedOutput); + }); + + it('normalize partial tag with permalink', () => { + const input: TagsFileInput = { + world: {permalink: 'world'}, + }; + + const expectedOutput: TagsFile = { + world: { + description: undefined, + label: 'World', + permalink: 'world', + }, + }; + + expect(normalizeTagsFile(input)).toEqual(expectedOutput); + }); + + it('does not modify fully defined tags', () => { + const input: TagsFileInput = { + tag1: { + label: 'Custom Label', + description: 'Custom Description', + permalink: 'custom-permalink', + }, + }; + + expect(normalizeTagsFile(input)).toEqual(input); + }); + + it('handle special characters in keys', () => { + const input: TagsFileInput = { + 'special@char$!key': null, + }; + + const expectedOutput: TagsFile = { + 'special@char$!key': { + description: undefined, + label: 'Special@char$!key', + permalink: '/special-char-key', + }, + }; + + expect(normalizeTagsFile(input)).toEqual(expectedOutput); + }); + + it('handle special characters in keys with chinese characters', () => { + const input: TagsFileInput = { + 特殊字符测试: null, + }; + + const expectedOutput: TagsFile = { + 特殊字符测试: { + description: undefined, + label: '特殊字符测试', + permalink: '/特殊字符测试', + }, + }; + + expect(normalizeTagsFile(input)).toEqual(expectedOutput); + }); + + it('normalize test', () => { + const input: TagsFileInput = { + world: {permalink: 'aze'}, + hello: {permalink: 'h e l l o'}, + }; + + const expectedOutput = { + world: { + description: undefined, + label: 'World', + permalink: 'aze', + }, + hello: { + description: undefined, + label: 'Hello', + permalink: 'h e l l o', + }, + }; + + expect(normalizeTagsFile(input)).toEqual(expectedOutput); + }); +}); + +describe('getTagsFilePathsToWatch', () => { + it('returns tags file paths - tags undefined', () => { + expect( + getTagsFilePathsToWatch({ + tags: undefined, + contentPaths: { + contentPath: '/user/blog', + contentPathLocalized: '/i18n/blog', + }, + }), + ).toEqual(['/i18n/blog/tags.yml', '/user/blog/tags.yml']); + }); + + it('returns tags file paths - tags.yml', () => { + expect( + getTagsFilePathsToWatch({ + tags: 'tags.yml', + contentPaths: { + contentPath: '/user/blog', + contentPathLocalized: '/i18n/blog', + }, + }), + ).toEqual(['/i18n/blog/tags.yml', '/user/blog/tags.yml']); + }); + + it('returns tags file paths - customTags.yml', () => { + expect( + getTagsFilePathsToWatch({ + tags: 'customTags.yml', + contentPaths: { + contentPath: '/user/blog', + contentPathLocalized: '/i18n/blog', + }, + }), + ).toEqual(['/i18n/blog/customTags.yml', '/user/blog/customTags.yml']); + }); + + it('returns [] - tags: null', () => { + expect( + getTagsFilePathsToWatch({ + tags: null, + contentPaths: { + contentPath: '/user/blog', + contentPathLocalized: '/i18n/blog', + }, + }), + ).toEqual([]); + }); + + it('returns [] - tags: false', () => { + expect( + getTagsFilePathsToWatch({ + tags: false, + contentPaths: { + contentPath: '/user/blog', + contentPathLocalized: '/i18n/blog', + }, + }), + ).toEqual([]); + }); +}); + +describe('getTagsFile', () => { + async function createTestTagsFile({ + filePath, + tagsFileInput, + }: { + filePath: string; + tagsFileInput: TagsFileInput; + }): Promise<{dir: string}> { + async function createTmpDir() { + return ( + await tmp.dir({ + prefix: 'jest-createTmpSiteDir', + }) + ).path; + } + const contentPath = await createTmpDir(); + const finalFilePath = path.join(contentPath, filePath); + const fileContent = YAML.dump(tagsFileInput); + await fs.writeFile(finalFilePath, fileContent); + return {dir: contentPath}; + } + + type Params = Parameters[0]; + + it('reads tags file - regular', async () => { + const {dir} = await createTestTagsFile({ + filePath: 'tags.yml', + tagsFileInput: { + tag1: {label: 'Tag1 Label'}, + tag2: {description: 'Tag2 Description'}, + tag3: { + label: 'Tag3 Label', + permalink: '/tag-3', + description: 'Tag3 Description', + }, + }, + }); + + const params: Params = { + contentPaths: {contentPath: dir, contentPathLocalized: dir}, + tags: 'tags.yml', + }; + + await expect(getTagsFile(params)).resolves.toMatchInlineSnapshot(` + { + "tag1": { + "description": undefined, + "label": "Tag1 Label", + "permalink": "/tag-1", + }, + "tag2": { + "description": "Tag2 Description", + "label": "Tag2", + "permalink": "/tag-2", + }, + "tag3": { + "description": "Tag3 Description", + "label": "Tag3 Label", + "permalink": "/tag-3", + }, + } + `); + }); + + it('reads tags file - only keys', async () => { + const {dir} = await createTestTagsFile({ + filePath: 'tags.yml', + tagsFileInput: { + tagKey: null, + }, + }); + + const params: Params = { + contentPaths: {contentPath: dir, contentPathLocalized: dir}, + tags: 'tags.yml', + }; + + await expect(getTagsFile(params)).resolves.toMatchInlineSnapshot(` + { + "tagKey": { + "description": undefined, + "label": "Tagkey", + "permalink": "/tag-key", + }, + } + `); + }); + + it('reads tags file - tags option undefined', async () => { + const {dir} = await createTestTagsFile({ + filePath: 'tags.yml', + tagsFileInput: { + tag: {label: 'tag label'}, + }, + }); + + const params: Params = { + contentPaths: {contentPath: dir, contentPathLocalized: dir}, + tags: undefined, + }; + + await expect(getTagsFile(params)).resolves.toMatchInlineSnapshot(` + { + "tag": { + "description": undefined, + "label": "tag label", + "permalink": "/tag", + }, + } + `); + }); + + it('reads tags file - empty file', async () => { + const {dir} = await createTestTagsFile({ + filePath: 'tags.yml', + tagsFileInput: {}, + }); + + const params: Params = { + contentPaths: {contentPath: dir, contentPathLocalized: dir}, + tags: undefined, + }; + + await expect(getTagsFile(params)).resolves.toEqual({}); + }); + + it('reads tags file - prioritizes reading from localized content path', async () => { + const {dir} = await createTestTagsFile({ + filePath: 'tags.yml', + tagsFileInput: { + tag: {label: 'tag label'}, + }, + }); + + const {dir: dirLocalized} = await createTestTagsFile({ + filePath: 'tags.yml', + tagsFileInput: { + tag: {label: 'tag label (localized)'}, + }, + }); + + const params: Params = { + contentPaths: {contentPath: dir, contentPathLocalized: dirLocalized}, + tags: undefined, + }; + + await expect(getTagsFile(params)).resolves.toMatchInlineSnapshot(` + { + "tag": { + "description": undefined, + "label": "tag label (localized)", + "permalink": "/tag", + }, + } + `); + }); + + it('reads tags file - custom tags file path', async () => { + const {dir} = await createTestTagsFile({ + filePath: 'custom-tags-path.yml', + tagsFileInput: { + tag: {label: 'tag label'}, + }, + }); + + const params: Params = { + contentPaths: {contentPath: dir, contentPathLocalized: dir}, + tags: 'custom-tags-path.yml', + }; + + await expect(getTagsFile(params)).resolves.toMatchInlineSnapshot(` + { + "tag": { + "description": undefined, + "label": "tag label", + "permalink": "/tag", + }, + } + `); + }); + + it('throws if duplicate permalink', async () => { + const {dir} = await createTestTagsFile({ + filePath: 'tags.yml', + tagsFileInput: { + tag1: {permalink: '/duplicate'}, + tag2: {permalink: '/duplicate'}, + }, + }); + + const params: Params = { + contentPaths: {contentPath: dir, contentPathLocalized: dir}, + tags: undefined, + }; + + await expect(getTagsFile(params)).rejects.toMatchInlineSnapshot(` + [Error: Duplicate permalinks found in tags file: + - /duplicate] + `); + }); + + it('throws if custom tags file path does not exist', async () => { + const params: Params = { + contentPaths: {contentPath: 'any', contentPathLocalized: 'localizedAny'}, + tags: 'custom-tags-path.yml', + }; + + await expect(getTagsFile(params)).rejects + .toThrowErrorMatchingInlineSnapshot(` + "No tags file 'custom-tags-path.yml' could be found in any of those directories: + - localizedAny + - any" + `); + }); + + it('does not read tags file - tags option null/false', async () => { + const {dir} = await createTestTagsFile({ + filePath: 'tags.yml', + tagsFileInput: { + tag: {label: 'tag label'}, + }, + }); + + await expect( + getTagsFile({ + contentPaths: {contentPath: dir, contentPathLocalized: dir}, + tags: null, + }), + ).resolves.toBeNull(); + await expect( + getTagsFile({ + contentPaths: {contentPath: dir, contentPathLocalized: dir}, + tags: false, + }), + ).resolves.toBeNull(); + }); + + it('does not read tags file - tags files has non-default name', async () => { + const {dir} = await createTestTagsFile({ + filePath: 'bad-tags-file-name.yml', + tagsFileInput: { + tag: {label: 'tag label'}, + }, + }); + + const params: Params = { + contentPaths: {contentPath: dir, contentPathLocalized: dir}, + tags: undefined, + }; + + await expect(getTagsFile(params)).resolves.toBeNull(); + }); +}); diff --git a/packages/docusaurus-utils-validation/src/index.ts b/packages/docusaurus-utils-validation/src/index.ts index eff1cb28d2..426e78db8c 100644 --- a/packages/docusaurus-utils-validation/src/index.ts +++ b/packages/docusaurus-utils-validation/src/index.ts @@ -29,3 +29,4 @@ export { FrontMatterLastUpdateErrorMessage, FrontMatterLastUpdateSchema, } from './validationSchemas'; +export {getTagsFilePathsToWatch, getTagsFile} from './tagsFile'; diff --git a/packages/docusaurus-utils-validation/src/tagsFile.ts b/packages/docusaurus-utils-validation/src/tagsFile.ts new file mode 100644 index 0000000000..55f9550102 --- /dev/null +++ b/packages/docusaurus-utils-validation/src/tagsFile.ts @@ -0,0 +1,131 @@ +/** + * 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 fs from 'fs-extra'; +import path from 'node:path'; +import _ from 'lodash'; +import Joi from 'joi'; +import YAML from 'js-yaml'; +import {getContentPathList, getDataFilePath} from '@docusaurus/utils'; +import type { + ContentPaths, + TagsFile, + TagsFileInput, + TagsPluginOptions, +} from '@docusaurus/utils'; + +const TagsFileInputSchema = Joi.object().pattern( + Joi.string(), + Joi.object({ + label: Joi.string(), + description: Joi.string(), + permalink: Joi.string(), + }).allow(null), +); + +export function ensureUniquePermalinks(tags: TagsFile): void { + const permalinks = new Set(); + const duplicates = new Set(); + + for (const [, tag] of Object.entries(tags)) { + const {permalink} = tag; + if (permalinks.has(permalink)) { + duplicates.add(permalink); + } else { + permalinks.add(permalink); + } + } + + if (duplicates.size > 0) { + const duplicateList = Array.from(duplicates) + .map((permalink) => ` - ${permalink}`) + .join('\n'); + throw new Error( + `Duplicate permalinks found in tags file:\n${duplicateList}`, + ); + } +} + +export function normalizeTagsFile(data: TagsFileInput): TagsFile { + return _.mapValues(data, (tag, key) => { + return { + label: tag?.label || _.capitalize(key), + description: tag?.description, + permalink: tag?.permalink || `/${_.kebabCase(key)}`, + }; + }); +} + +type GetTagsFileParams = { + tags: TagsPluginOptions['tags']; + contentPaths: ContentPaths; +}; + +const DefaultTagsFileName = 'tags.yml'; + +export function getTagsFilePathsToWatch({ + tags, + contentPaths, +}: GetTagsFileParams): string[] { + if (tags === false || tags === null) { + return []; + } + const relativeFilePath = tags ?? DefaultTagsFileName; + + return getContentPathList(contentPaths).map((contentPath) => + path.posix.join(contentPath, relativeFilePath), + ); +} + +export async function getTagsFile({ + tags, + contentPaths, +}: GetTagsFileParams): Promise { + if (tags === false || tags === null) { + return null; + } + + const relativeFilePath = tags ?? DefaultTagsFileName; + + // if returned path is defined, the file exists (localized or not) + const yamlFilePath = await getDataFilePath({ + contentPaths, + filePath: relativeFilePath, + }); + + // If the tags option is undefined, don't throw when the file does not exist + // Retro-compatible behavior: existing sites do not yet have tags.yml + if (tags === undefined && !yamlFilePath) { + return null; + } + if (!yamlFilePath) { + throw new Error( + `No tags file '${relativeFilePath}' could be found in any of those directories:\n- ${getContentPathList( + contentPaths, + ).join('\n- ')}`, + ); + } + + const tagDefinitionContent = await fs.readFile(yamlFilePath, 'utf-8'); + if (!tagDefinitionContent.trim()) { + return {}; + } + + const yamlContent = YAML.load(tagDefinitionContent); + const tagsFileInputResult = TagsFileInputSchema.validate(yamlContent); + if (tagsFileInputResult.error) { + throw new Error( + `There was an error extracting tags from file: ${tagsFileInputResult.error.message}`, + {cause: tagsFileInputResult}, + ); + } + + const tagsFile = normalizeTagsFile(tagsFileInputResult.value); + ensureUniquePermalinks(tagsFile); + + return tagsFile; +} diff --git a/packages/docusaurus-utils-validation/src/validationSchemas.ts b/packages/docusaurus-utils-validation/src/validationSchemas.ts index 69b4ff5c99..706eaf676f 100644 --- a/packages/docusaurus-utils-validation/src/validationSchemas.ts +++ b/packages/docusaurus-utils-validation/src/validationSchemas.ts @@ -5,7 +5,11 @@ * LICENSE file in the root directory of this source tree. */ -import {isValidPathname, DEFAULT_PLUGIN_ID, type Tag} from '@docusaurus/utils'; +import { + isValidPathname, + DEFAULT_PLUGIN_ID, + type FrontMatterTag, +} from '@docusaurus/utils'; import {addLeadingSlash} from '@docusaurus/utils-common'; import Joi from './Joi'; import {JoiFrontMatter} from './JoiFrontMatter'; @@ -113,7 +117,9 @@ export const RouteBasePathSchema = Joi const FrontMatterTagSchema = JoiFrontMatter.alternatives() .try( JoiFrontMatter.string().required(), - JoiFrontMatter.object({ + // TODO Docusaurus v4 remove this legacy front matter tag object form + // users should use tags.yml instead + JoiFrontMatter.object({ label: JoiFrontMatter.string().required(), permalink: JoiFrontMatter.string().required(), }).required(), diff --git a/packages/docusaurus-utils/package.json b/packages/docusaurus-utils/package.json index b249b42691..bc983e669f 100644 --- a/packages/docusaurus-utils/package.json +++ b/packages/docusaurus-utils/package.json @@ -36,6 +36,7 @@ "shelljs": "^0.8.5", "tslib": "^2.6.0", "url-loader": "^4.1.1", + "utility-types": "^3.10.0", "webpack": "^5.88.1" }, "engines": { diff --git a/packages/docusaurus-utils/src/__tests__/tags.test.ts b/packages/docusaurus-utils/src/__tests__/tags.test.ts index cf8c52ec53..a238c63816 100644 --- a/packages/docusaurus-utils/src/__tests__/tags.test.ts +++ b/packages/docusaurus-utils/src/__tests__/tags.test.ts @@ -6,117 +6,284 @@ */ import { - normalizeFrontMatterTags, + reportInlineTags, groupTaggedItems, - type Tag, getTagVisibility, -} from '../tags'; +} from '@docusaurus/utils'; +import {normalizeTag} from '../tags'; +import type {Tag, TagMetadata, FrontMatterTag, TagsFile} from '../tags'; -describe('normalizeFrontMatterTags', () => { - it('normalizes simple string tag', () => { - const tagsPath = '/all/tags'; - const input = 'tag'; - const expectedOutput = { - label: 'tag', - permalink: `${tagsPath}/tag`, +describe('normalizeTag', () => { + const tagsBaseRoutePath = '/all/tags'; + + describe('inline', () => { + it('normalizes simple string tag', () => { + const input: FrontMatterTag = 'tag'; + const expectedOutput: TagMetadata = { + inline: true, + label: 'tag', + permalink: `${tagsBaseRoutePath}/tag`, + description: undefined, + }; + expect( + normalizeTag({tagsBaseRoutePath, tagsFile: null, tag: input}), + ).toEqual(expectedOutput); + }); + + it('normalizes complex string tag', () => { + const input: FrontMatterTag = 'some more Complex_tag'; + const expectedOutput: TagMetadata = { + inline: true, + label: 'some more Complex_tag', + permalink: `${tagsBaseRoutePath}/some-more-complex-tag`, + description: undefined, + }; + expect( + normalizeTag({tagsBaseRoutePath, tagsFile: null, tag: input}), + ).toEqual(expectedOutput); + }); + + it('normalizes simple object tag', () => { + const input: FrontMatterTag = { + label: 'tag', + permalink: 'tagPermalink', + }; + const expectedOutput: TagMetadata = { + inline: true, + label: 'tag', + permalink: `${tagsBaseRoutePath}/tagPermalink`, + description: undefined, + }; + expect( + normalizeTag({tagsBaseRoutePath, tagsFile: null, tag: input}), + ).toEqual(expectedOutput); + }); + + it('normalizes complex string tag with object tag', () => { + const input: FrontMatterTag = { + label: 'tag complex Label', + permalink: '/MoreComplex/Permalink', + }; + const expectedOutput: TagMetadata = { + inline: true, + label: 'tag complex Label', + permalink: `${tagsBaseRoutePath}/MoreComplex/Permalink`, + description: undefined, + }; + expect( + normalizeTag({tagsBaseRoutePath, tagsFile: null, tag: input}), + ).toEqual(expectedOutput); + }); + }); + + describe('with tags file', () => { + const tagsFile: TagsFile = { + tag1: { + label: 'Tag 1 label', + permalink: 'tag-1-permalink', + description: 'Tag 1 description', + }, + tag2: { + label: 'Tag 2 label', + permalink: '/tag-2-permalink', + description: undefined, + }, }; - expect(normalizeFrontMatterTags(tagsPath, [input])).toEqual([ - expectedOutput, - ]); + + it('normalizes tag1 ref', () => { + const input: FrontMatterTag = 'tag1'; + const expectedOutput: TagMetadata = { + inline: false, + label: tagsFile.tag1.label, + description: tagsFile.tag1.description, + permalink: `${tagsBaseRoutePath}/tag-1-permalink`, + }; + expect(normalizeTag({tagsBaseRoutePath, tagsFile, tag: input})).toEqual( + expectedOutput, + ); + }); + + it('normalizes tag2 ref', () => { + const input: FrontMatterTag = 'tag2'; + const expectedOutput: TagMetadata = { + inline: false, + label: tagsFile.tag2.label, + description: tagsFile.tag2.description, + permalink: `${tagsBaseRoutePath}/tag-2-permalink`, + }; + expect(normalizeTag({tagsBaseRoutePath, tagsFile, tag: input})).toEqual( + expectedOutput, + ); + }); + + it('normalizes inline tag not declared in tags file', () => { + const input: FrontMatterTag = 'inlineTag'; + const expectedOutput: TagMetadata = { + inline: true, + label: 'inlineTag', + description: undefined, + permalink: `${tagsBaseRoutePath}/inline-tag`, + }; + expect(normalizeTag({tagsBaseRoutePath, tagsFile, tag: input})).toEqual( + expectedOutput, + ); + }); + }); +}); + +describe('reportInlineTags', () => { + const tagsFile: TagsFile = { + hello: { + label: 'Hello', + permalink: '/hello', + description: undefined, + }, + test: { + label: 'Test', + permalink: '/test', + description: undefined, + }, + open: { + label: 'Open Source', + permalink: '/open', + description: undefined, + }, + }; + + it('throw when inline tags found', () => { + const testFn = () => + reportInlineTags({ + tags: [ + { + label: 'hello', + permalink: 'hello', + inline: true, + description: undefined, + }, + { + label: 'world', + permalink: 'world', + inline: true, + description: undefined, + }, + ], + source: 'wrong.md', + options: {onInlineTags: 'throw', tags: 'tags.yml'}, + }); + + expect(testFn).toThrowErrorMatchingInlineSnapshot( + `"Tags [hello, world] used in wrong.md are not defined in tags.yml"`, + ); }); - it('normalizes complex string tag', () => { - const tagsPath = '/all/tags'; - const input = 'some more Complex_tag'; - const expectedOutput = { - label: 'some more Complex_tag', - permalink: `${tagsPath}/some-more-complex-tag`, - }; - expect(normalizeFrontMatterTags(tagsPath, [input])).toEqual([ - expectedOutput, - ]); + it('warn when docs has invalid tags', () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + reportInlineTags({ + tags: [ + { + label: 'hello', + permalink: 'hello', + inline: false, + description: undefined, + }, + { + label: 'world', + permalink: 'world', + inline: true, + description: undefined, + }, + ], + source: 'wrong.md', + options: {onInlineTags: 'warn', tags: 'tags.yml'}, + }); + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy.mock.calls).toMatchInlineSnapshot(` + [ + [ + "[WARNING] Tags [world] used in wrong.md are not defined in tags.yml", + ], + ] + `); + + warnSpy.mockRestore(); }); - it('normalizes simple object tag', () => { - const tagsPath = '/all/tags'; - const input = {label: 'tag', permalink: 'tagPermalink'}; - const expectedOutput = { - label: 'tag', - permalink: `${tagsPath}/tagPermalink`, - }; - expect(normalizeFrontMatterTags(tagsPath, [input])).toEqual([ - expectedOutput, - ]); + it('ignore when docs has invalid tags', () => { + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + + reportInlineTags({ + tags: [ + { + label: 'hello', + permalink: 'hello', + inline: false, + description: undefined, + }, + { + label: 'world', + permalink: 'world', + inline: true, + description: undefined, + }, + ], + source: 'wrong.md', + options: {onInlineTags: 'ignore', tags: 'tags.yml'}, + }); + expect(errorSpy).not.toHaveBeenCalled(); + expect(warnSpy).not.toHaveBeenCalled(); + expect(logSpy).not.toHaveBeenCalled(); + + errorSpy.mockRestore(); + warnSpy.mockRestore(); + logSpy.mockRestore(); }); - it('normalizes complex string tag with object tag', () => { - const tagsPath = '/all/tags'; - const input = { - label: 'tag complex Label', - permalink: '/MoreComplex/Permalink', - }; - const expectedOutput = { - label: 'tag complex Label', - permalink: `${tagsPath}/MoreComplex/Permalink`, - }; - expect(normalizeFrontMatterTags(tagsPath, [input])).toEqual([ - expectedOutput, - ]); + it('throw for unknown string and object tag', () => { + const frontmatter = ['open', 'world']; + const tags = frontmatter.map((tag) => + normalizeTag({ + tagsBaseRoutePath: '/tags', + tagsFile, + tag, + }), + ); + + const testFn = () => + reportInlineTags({ + tags, + source: 'default.md', + options: { + onInlineTags: 'throw', + tags: 'tags.yml', + }, + }); + expect(testFn).toThrowErrorMatchingInlineSnapshot( + `"Tags [world] used in default.md are not defined in tags.yml"`, + ); }); - type Input = Parameters[1]; - type Output = ReturnType; - - it('normalizes string list', () => { - const tagsPath = '/all/tags'; - const input: Input = ['tag 1', 'tag-1', 'tag 3', 'tag1', 'tag-2']; - // Keep user input order but remove tags that lead to same permalink - const expectedOutput: Output = [ - { - label: 'tag 1', - permalink: `${tagsPath}/tag-1`, - }, - { - label: 'tag 3', - permalink: `${tagsPath}/tag-3`, - }, - { - label: 'tag-2', - permalink: `${tagsPath}/tag-2`, - }, - ]; - expect(normalizeFrontMatterTags(tagsPath, input)).toEqual(expectedOutput); - }); - - it('succeeds for empty list', () => { - expect(normalizeFrontMatterTags('/foo')).toEqual([]); - }); - - it('normalizes complex mixed list', () => { - const tagsPath = '/all/tags'; - const input: Input = [ - 'tag 1', - {label: 'tag-1', permalink: '/tag-1'}, - 'tag 3', - 'tag1', - {label: 'tag 4', permalink: '/tag4Permalink'}, - ]; - // Keep user input order but remove tags that lead to same permalink - const expectedOutput: Output = [ - { - label: 'tag 1', - permalink: `${tagsPath}/tag-1`, - }, - { - label: 'tag 3', - permalink: `${tagsPath}/tag-3`, - }, - { - label: 'tag 4', - permalink: `${tagsPath}/tag4Permalink`, - }, - ]; - expect(normalizeFrontMatterTags(tagsPath, input)).toEqual(expectedOutput); + it('does not throw when docs has valid tags', () => { + const frontmatter = ['open']; + const tags = frontmatter.map((tag) => + normalizeTag({ + tagsBaseRoutePath: '/tags', + tagsFile, + tag, + }), + ); + const testFn = () => + reportInlineTags({ + tags, + source: 'wrong.md', + options: { + onInlineTags: 'throw', + tags: 'tags.yml', + }, + }); + expect(testFn).not.toThrow(); }); }); @@ -135,14 +302,23 @@ describe('groupTaggedItems', () => { type Output = ReturnType; it('groups items by tag permalink', () => { - const tagGuide = {label: 'Guide', permalink: '/guide'}; - const tagTutorial = {label: 'Tutorial', permalink: '/tutorial'}; - const tagAPI = {label: 'API', permalink: '/api'}; + const tagGuide = { + label: 'Guide', + permalink: '/guide', + description: undefined, + }; + const tagTutorial = { + label: 'Tutorial', + permalink: '/tutorial', + description: undefined, + }; + const tagAPI = {label: 'API', permalink: '/api', description: undefined}; // This one will be grouped under same permalink and label is ignored const tagTutorialOtherLabel = { label: 'TutorialOtherLabel', permalink: '/tutorial', + description: undefined, }; const item1: SomeTaggedItem = { diff --git a/packages/docusaurus-utils/src/index.ts b/packages/docusaurus-utils/src/index.ts index 6e6c3c6718..3dd23137f5 100644 --- a/packages/docusaurus-utils/src/index.ts +++ b/packages/docusaurus-utils/src/index.ts @@ -55,10 +55,13 @@ export { export type {URLPath} from './urlUtils'; export { type Tag, + type TagsFile, + type TagsFileInput, + type TagMetadata, type TagsListItem, type TagModule, type FrontMatterTag, - normalizeFrontMatterTags, + type TagsPluginOptions, groupTaggedItems, getTagVisibility, } from './tags'; @@ -120,3 +123,5 @@ export { type LastUpdateData, type FrontMatterLastUpdate, } from './lastUpdateUtils'; + +export {normalizeTags, reportInlineTags} from './tags'; diff --git a/packages/docusaurus-utils/src/tags.ts b/packages/docusaurus-utils/src/tags.ts index 3560bde25e..1da758f9cd 100644 --- a/packages/docusaurus-utils/src/tags.ts +++ b/packages/docusaurus-utils/src/tags.ts @@ -6,13 +6,34 @@ */ import _ from 'lodash'; +import logger from '@docusaurus/logger'; import {normalizeUrl} from './urlUtils'; +import type {Optional} from 'utility-types'; -/** What the user configures. */ export type Tag = { + /** The display label of a tag */ label: string; /** Permalink to this tag's page, without the `/tags/` base path. */ permalink: string; + /** An optional description of the tag */ + description: string | undefined; +}; + +export type TagsFileInput = Record | null>; + +export type TagsFile = Record; + +// Tags plugins options shared between docs/blog +export type TagsPluginOptions = { + // TODO allow option tags later? | TagsFile; + /** Path to the tags file. */ + tags: string | false | null | undefined; + /** The behavior of Docusaurus when it found inline tags. */ + onInlineTags: 'ignore' | 'log' | 'warn' | 'throw'; +}; + +export type TagMetadata = Tag & { + inline: boolean; }; /** What the tags list page should know about each tag. */ @@ -29,62 +50,126 @@ export type TagModule = TagsListItem & { unlisted: boolean; }; -export type FrontMatterTag = string | Tag; +export type FrontMatterTag = string | Optional; -function normalizeFrontMatterTag( - tagsPath: string, +// We always apply tagsBaseRoutePath on purpose. For versioned docs, v1/doc.md +// and v2/doc.md tags with custom permalinks don't lead to the same created +// page. tagsBaseRoutePath is different for each doc version +function normalizeTagPermalink({ + tagsBaseRoutePath, + permalink, +}: { + tagsBaseRoutePath: string; + permalink: string; +}): string { + return normalizeUrl([tagsBaseRoutePath, permalink]); +} + +function normalizeInlineTag( + tagsBaseRoutePath: string, frontMatterTag: FrontMatterTag, -): Tag { - function toTagObject(tagString: string): Tag { +): TagMetadata { + function toTagObject(tagString: string): TagMetadata { return { + inline: true, label: tagString, permalink: _.kebabCase(tagString), + description: undefined, }; } - // TODO maybe make ensure the permalink is valid url path? - function normalizeTagPermalink(permalink: string): string { - // Note: we always apply tagsPath on purpose. For versioned docs, v1/doc.md - // and v2/doc.md tags with custom permalinks don't lead to the same created - // page. tagsPath is different for each doc version - return normalizeUrl([tagsPath, permalink]); - } - const tag: Tag = typeof frontMatterTag === 'string' ? toTagObject(frontMatterTag) - : frontMatterTag; + : {...frontMatterTag, description: frontMatterTag.description}; return { + inline: true, label: tag.label, - permalink: normalizeTagPermalink(tag.permalink), + permalink: normalizeTagPermalink({ + permalink: tag.permalink, + tagsBaseRoutePath, + }), + description: tag.description, }; } -/** - * Takes tag objects as they are defined in front matter, and normalizes each - * into a standard tag object. The permalink is created by appending the - * sluggified label to `tagsPath`. Front matter tags already containing - * permalinks would still have `tagsPath` prepended. - * - * The result will always be unique by permalinks. The behavior with colliding - * permalinks is undetermined. - */ -export function normalizeFrontMatterTags( - /** Base path to append the tag permalinks to. */ - tagsPath: string, - /** Can be `undefined`, so that we can directly pipe in `frontMatter.tags`. */ - frontMatterTags: FrontMatterTag[] | undefined = [], -): Tag[] { - const tags = frontMatterTags.map((tag) => - normalizeFrontMatterTag(tagsPath, tag), - ); +export function normalizeTag({ + tag, + tagsFile, + tagsBaseRoutePath, +}: { + tag: FrontMatterTag; + tagsBaseRoutePath: string; + tagsFile: TagsFile | null; +}): TagMetadata { + if (typeof tag === 'string') { + const tagDescription = tagsFile?.[tag]; + if (tagDescription) { + // pre-defined tag from tags.yml + return { + inline: false, + label: tagDescription.label, + permalink: normalizeTagPermalink({ + permalink: tagDescription.permalink, + tagsBaseRoutePath, + }), + description: tagDescription.description, + }; + } + } + // legacy inline tag object, always inline, unknown because isn't a string + return normalizeInlineTag(tagsBaseRoutePath, tag); +} - return _.uniqBy(tags, (tag) => tag.permalink); +export function normalizeTags({ + options, + source, + frontMatterTags, + tagsBaseRoutePath, + tagsFile, +}: { + options: TagsPluginOptions; + source: string; + frontMatterTags: FrontMatterTag[] | undefined; + tagsBaseRoutePath: string; + tagsFile: TagsFile | null; +}): TagMetadata[] { + const tags = (frontMatterTags ?? []).map((tag) => + normalizeTag({tag, tagsBaseRoutePath, tagsFile}), + ); + if (tagsFile !== null) { + reportInlineTags({tags, source, options}); + } + return tags; +} + +export function reportInlineTags({ + tags, + source, + options, +}: { + tags: TagMetadata[]; + source: string; + options: TagsPluginOptions; +}): void { + if (options.onInlineTags === 'ignore') { + return; + } + const inlineTags = tags.filter((tag) => tag.inline); + if (inlineTags.length > 0) { + const uniqueUnknownTags = [...new Set(inlineTags.map((tag) => tag.label))]; + const tagListString = uniqueUnknownTags.join(', '); + logger.report(options.onInlineTags)( + `Tags [${tagListString}] used in ${source} are not defined in ${ + options.tags ?? 'tags.yml' + }`, + ); + } } type TaggedItemGroup = { - tag: Tag; + tag: TagMetadata; items: Item[]; }; @@ -102,7 +187,7 @@ export function groupTaggedItems( * A callback telling me how to get the tags list of the current item. Usually * simply getting it from some metadata of the current item. */ - getItemTags: (item: Item) => readonly Tag[], + getItemTags: (item: Item) => readonly TagMetadata[], ): {[permalink: string]: TaggedItemGroup} { const result: {[permalink: string]: TaggedItemGroup} = {}; diff --git a/project-words.txt b/project-words.txt index cc4e06e18c..0125953ec9 100644 --- a/project-words.txt +++ b/project-words.txt @@ -123,6 +123,7 @@ héllô hideable Hideable hola +Hola Hostman hoverable Husain @@ -320,7 +321,6 @@ showinfo Sida Simen slorber -sluggified sluggifies sluggify solana @@ -353,6 +353,7 @@ Supabase svgr SVGR swizzlable +Tagkey Teik templating Thanos diff --git a/website/_dogfooding/_blog tests/tags.yml b/website/_dogfooding/_blog tests/tags.yml new file mode 100644 index 0000000000..34bcbdc0f9 --- /dev/null +++ b/website/_dogfooding/_blog tests/tags.yml @@ -0,0 +1,12 @@ +paginated-tag: +blog: +docusaurus: +long: +long-long: +long-long-long: +long-long-long-long: +long-long-long-long-long: +visibility: +unlisted: +draft: +new: diff --git a/website/_dogfooding/_docs tests/index.mdx b/website/_dogfooding/_docs tests/index.mdx index 64fcfb7edd..a97ed434ef 100644 --- a/website/_dogfooding/_docs tests/index.mdx +++ b/website/_dogfooding/_docs tests/index.mdx @@ -1,6 +1,6 @@ --- slug: / -tags: [a, b, c, some tag] +tags: [a, b, c] unlisted: true # Makes the navbar link disappear in prod id: index sidebar_label: Docs tests # TODO why is this required? diff --git a/website/_dogfooding/_docs tests/more-test.mdx b/website/_dogfooding/_docs tests/more-test.mdx index 478082818a..b46d4e52b9 100644 --- a/website/_dogfooding/_docs tests/more-test.mdx +++ b/website/_dogfooding/_docs tests/more-test.mdx @@ -1,5 +1,5 @@ --- -tags: [a, e, some-tag, some_tag] +tags: [a, e, some-tag] --- # Another test page diff --git a/website/_dogfooding/_docs tests/tags.yml b/website/_dogfooding/_docs tests/tags.yml new file mode 100644 index 0000000000..548ba5e7d8 --- /dev/null +++ b/website/_dogfooding/_docs tests/tags.yml @@ -0,0 +1,16 @@ +a: + description: 'Description for tag a' +b: + label: 'Label for tag b' +c: + permalink: '/permalink-for-tag-c' +e: + label: 'Label for tag e' + description: 'Description for tag e' + permalink: '/permalink-for-tag-e' +some-tag: +visibility: +draft: +listed: +unlisted: +d-custom-permalink: diff --git a/website/_dogfooding/dogfooding.config.ts b/website/_dogfooding/dogfooding.config.ts index 7bcc7babea..d751ced7ec 100644 --- a/website/_dogfooding/dogfooding.config.ts +++ b/website/_dogfooding/dogfooding.config.ts @@ -40,6 +40,8 @@ export const dogfoodingPluginInstances: PluginConfig[] = [ noIndex: true, }, }, + onInlineTags: 'warn', + tags: 'tags.yml', // Using a _ prefix to test against an edge case regarding MDX partials: https://github.com/facebook/docusaurus/discussions/5181#discussioncomment-1018079 path: '_dogfooding/_docs tests', @@ -81,6 +83,8 @@ export const dogfoodingPluginInstances: PluginConfig[] = [ frontMatter.hide_reading_time ? undefined : defaultReadingTime({content, options: {wordsPerMinute: 5}}), + onInlineTags: 'warn', + tags: 'tags.yml', } satisfies BlogOptions, ], diff --git a/website/blog/tags.yml b/website/blog/tags.yml new file mode 100644 index 0000000000..2cc6c501f0 --- /dev/null +++ b/website/blog/tags.yml @@ -0,0 +1,18 @@ +blog: +release: + description: "Blog posts about Docusaurus' new releases" +recap: + description: "Blog posts about Docusaurus' year recaps" +birth: +endi: +tribute: +i18n: +beta: +search: +maintenance: +documentation: +docusaurus: +profilo: +adoption: +unlisted: +new: diff --git a/website/docs/api/plugins/_partial-tags-file-api-ref-section.mdx b/website/docs/api/plugins/_partial-tags-file-api-ref-section.mdx new file mode 100644 index 0000000000..f6d247c70f --- /dev/null +++ b/website/docs/api/plugins/_partial-tags-file-api-ref-section.mdx @@ -0,0 +1,54 @@ +## Tags File {#tags-file} + +Use the [`tags` plugin option](#tags) to configure the path of a YAML tags file. + +By convention, the plugin will look for a `tags.yml` file at the root of your content folder(s). + +This file can contain a list of predefined tags. You can reference these tags by their keys in Markdown files thanks to the [`tags` front matter](#markdown-front-matter). + +:::tip Keeping tags consistent + +Using a tags file, you can ensure that your tags usage is consistent across your plugin content set. Use the [`onInlineTags: 'throw'`](#onInlineTags) plugin option to enforce this consistency and prevent usage of inline tags declared on the fly. + +::: + +### Types {#tags-file-types} + +The YAML content of the provided tags file should respect the following shape: + +```tsx +type Tag = { + label?: string; // Tag display label + permalink?: string; // Tag URL pathname segment + description?: string; // Tag description displayed in the tag page +}; + +type TagsFileInput = Record | null>; +``` + +### Example {#tags-file-example} + +```yml title="tags.yml" +releases: + label: 'Product releases' + permalink: '/product-releases' + description: 'Content related to product releases.' + +# A partial tag definition is also valid +announcements: + label: 'Announcements' + +# An empty tag definition is also valid +# Other attributes will be inferred from the key +emptyTag: +``` + +```md title="content.md" +--- +tags: [releases, announcements, emptyTag] +--- + +# Title + +Content +``` diff --git a/website/docs/api/plugins/plugin-content-blog.mdx b/website/docs/api/plugins/plugin-content-blog.mdx index d97b834aa2..1521411dad 100644 --- a/website/docs/api/plugins/plugin-content-blog.mdx +++ b/website/docs/api/plugins/plugin-content-blog.mdx @@ -78,6 +78,8 @@ Accepted fields: | `processBlogPosts` | [ProcessBlogPostsFn](#ProcessBlogPostsFn) | `undefined` | An optional function which can be used to transform blog posts (filter, modify, delete, etc...). | | `showLastUpdateAuthor` | `boolean` | `false` | Whether to display the author who last updated the blog post. | | `showLastUpdateTime` | `boolean` | `false` | Whether to display the last date the blog post was updated. This requires access to git history during the build, so will not work correctly with shallow clones (a common default for CI systems). With GitHub `actions/checkout`, use`fetch-depth: 0`. | +| `tags` | `string \| false \| null \| undefined` | `tags.yml` | Path to the YAML tags file listing pre-defined tags. Relative to the blog content directory. | +| `onInlineTags` | `'ignore' \| 'log' \| 'warn' \| 'throw'` | `warn` | The plugin behavior when blog posts contain inline tags (not appearing in the list of pre-defined tags, usually `tags.yml`). | ```mdx-code-block @@ -224,7 +226,7 @@ Accepted fields: | `author_title` | `string` | `undefined` | ⚠️ Prefer using `authors`. A description of the author. | | `title` | `string` | Markdown title | The blog post title. | | `date` | `string` | File name or file creation time | The blog post creation date. If not specified, this can be extracted from the file or folder name, e.g, `2021-04-15-blog-post.mdx`, `2021-04-15-blog-post/index.mdx`, `2021/04/15/blog-post.mdx`. Otherwise, it is the Markdown file creation time. | -| `tags` | `Tag[]` | `undefined` | A list of strings or objects of two string fields `label` and `permalink` to tag to your post. | +| `tags` | `Tag[]` | `undefined` | A list of strings or objects of two string fields `label` and `permalink` to tag to your post. Strings can be a reference to keys of a [tags file](#tags-file) (usually `tags.yml`) | | `draft` | `boolean` | `false` | Draft blog posts will only be available during development. | | `unlisted` | `boolean` | `false` | Unlisted blog posts will be available in both development and production. They will be "hidden" in production, not indexed, excluded from sitemaps, and can only be accessed by users having a direct link. | | `hide_table_of_contents` | `boolean` | `false` | Whether to hide the table of contents to the right. | @@ -272,7 +274,7 @@ authors: title: Co-creator of Docusaurus 1 url: https://github.com/JoelMarcey image_url: https://github.com/JoelMarcey.png -tags: [hello, docusaurus-v2] +tags: [docusaurus] description: This is my first post on Docusaurus. image: https://i.imgur.com/mErPwqL.png hide_table_of_contents: false @@ -281,6 +283,10 @@ hide_table_of_contents: false A Markdown blog post ``` +import TagsFileApiRefSection from './_partial-tags-file-api-ref-section.mdx'; + + + ## i18n {#i18n} Read the [i18n introduction](../../i18n/i18n-introduction.mdx) first. diff --git a/website/docs/api/plugins/plugin-content-docs.mdx b/website/docs/api/plugins/plugin-content-docs.mdx index 09ce59a60e..1a9dcc3707 100644 --- a/website/docs/api/plugins/plugin-content-docs.mdx +++ b/website/docs/api/plugins/plugin-content-docs.mdx @@ -65,6 +65,8 @@ Accepted fields: | `lastVersion` | `string` | First version in `versions.json` | The version navigated to in priority and displayed by default for docs navbar items. | | `onlyIncludeVersions` | `string[]` | All versions available | Only include a subset of all available versions. | | `versions` | [VersionsConfig](#VersionsConfig) | `{}` | Independent customization of each version's properties. | +| `tags` | `string \| false \| null \| undefined` | `tags.yml` | Path to a YAML file listing pre-defined tags. Relative to the docs version content directories. | +| `onInlineTags` | `'ignore' \| 'log' \| 'warn' \| 'throw'` | `warn` | The plugin behavior when docs contain inline tags (not appearing in the list of pre-defined tags, usually `docs/tags.yml`). | ```mdx-code-block @@ -293,7 +295,7 @@ Accepted fields: | `description` | `string` | The first line of Markdown content | The description of your document, which will become the `` and `` in ``, used by search engines. | | `image` | `string` | `undefined` | Cover or thumbnail image that will be used as the `` in the ``, enhancing link previews on social media and messaging platforms. | | `slug` | `string` | File path | Allows to customize the document URL (`//`). Support multiple patterns: `slug: my-doc`, `slug: /my/path/myDoc`, `slug: /`. | -| `tags` | `Tag[]` | `undefined` | A list of strings or objects of two string fields `label` and `permalink` to tag to your docs. | +| `tags` | `Tag[]` | `undefined` | A list of strings or objects of two string fields `label` and `permalink` to tag to your docs. Strings can be a reference to keys of a [tags file](#tags-file) (usually `tags.yml`) | | `draft` | `boolean` | `false` | Draft documents will only be available during development. | | `unlisted` | `boolean` | `false` | Unlisted documents will be available in both development and production. They will be "hidden" in production, not indexed, excluded from sitemaps, and can only be accessed by users having a direct link. | | `last_update` | `FrontMatterLastUpdate` | `undefined` | Allows overriding the last update author/date. Date can be any [parsable date string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse). | @@ -324,6 +326,7 @@ description: How do I find you when I cannot solve this problem keywords: - docs - docusaurus +tags: [docusaurus] image: https://i.imgur.com/mErPwqL.png slug: /myDoc last_update: @@ -336,6 +339,10 @@ last_update: My Document Markdown content ``` +import TagsFileApiRefSection from './_partial-tags-file-api-ref-section.mdx'; + + + ## i18n {#i18n} Read the [i18n introduction](../../i18n/i18n-introduction.mdx) first. diff --git a/website/docs/blog.mdx b/website/docs/blog.mdx index 1fb3ace11c..75e4fa09a9 100644 --- a/website/docs/blog.mdx +++ b/website/docs/blog.mdx @@ -380,6 +380,35 @@ An author, either declared through front matter or through the authors map, need ::: +## Blog post tags {#blog-post-tags} + +Tags are declared in the front matter and introduce another dimension of categorization. + +It is possible to define tags inline, or to reference predefined tags declared in a [`tags file`](api/plugins/plugin-content-blog.mdx#tags-file) (optional, usually `blog/tags.yml`). + +In the following example: + +- `docusaurus` references a predefined tag key declared in `blog/tags.yml` +- `Releases` is an inline tag, because it does not exist in `blog/tags.yml` + +```md title="blog/my-post.md" +--- +title: 'My blog post' +tags: + - Releases + - docusaurus +--- + +Content +``` + +```yml title="blog/tags.yml" +docusaurus: + label: 'Docusaurus' + permalink: '/docusaurus' + description: 'Blog posts related to the Docusaurus framework' +``` + ## Reading time {#reading-time} Docusaurus generates a reading time estimation for each blog post based on word count. We provide an option to customize this. diff --git a/website/docs/guides/docs/docs-create-doc.mdx b/website/docs/guides/docs/docs-create-doc.mdx index 86fc7c2a8e..caf8e2ea77 100644 --- a/website/docs/guides/docs/docs-create-doc.mdx +++ b/website/docs/guides/docs/docs-create-doc.mdx @@ -60,16 +60,32 @@ The [front matter](../markdown-features/markdown-features-intro.mdx#front-matter ## Doc tags {#doc-tags} -Optionally, you can add tags to your doc pages, which introduces another dimension of categorization in addition to the [docs sidebar](./sidebar/index.mdx). Tags are passed in the front matter as a list of labels: +Tags are declared in the front matter and introduce another dimension of categorization in addition to the [docs sidebar](./sidebar/index.mdx). -```md "your-doc-page.md" +It is possible to define tags inline, or to reference predefined tags declared in a [`tags file`](../../api/plugins/plugin-content-docs.mdx#tags-file) (optional, usually `docs/tags.yml`). + +In the following example: + +- `docusaurus` references a predefined tag key declared in `docs/tags.yml` +- `Releases` is an inline tag, because it does not exist in `docs/tags.yml` + +```md title="docs/my-doc.md" --- -id: doc-with-tags -title: A doc with tags tags: - - Demo - - Getting started + - Releases + - docusaurus --- + +# Title + +Content +``` + +```yml title="docs/tags.yml" +docusaurus: + label: 'Docusaurus' + permalink: '/docusaurus' + description: 'Docs related to the Docusaurus framework' ``` :::tip diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index f42da17542..f84786d10e 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -492,6 +492,7 @@ export default async function createConfigAsync() { blogDescription: 'Read blog posts about Docusaurus from the team', blogSidebarCount: 'ALL', blogSidebarTitle: 'All our posts', + onInlineTags: 'throw', } satisfies BlogOptions, pages: { remarkPlugins: [npm2yarn], diff --git a/yarn.lock b/yarn.lock index 5b26ba4c43..f20f2f15ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7847,10 +7847,10 @@ fs-extra@9.1.0, fs-extra@^9.0.0, fs-extra@^9.0.1, fs-extra@^9.1.0: jsonfile "^6.0.1" universalify "^2.0.0" -fs-extra@^11.1.0, fs-extra@^11.1.1: - version "11.1.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d" - integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ== +fs-extra@^11.1.0, fs-extra@^11.1.1, fs-extra@^11.2.0: + version "11.2.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" + integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== dependencies: graceful-fs "^4.2.0" jsonfile "^6.0.1" @@ -15205,16 +15205,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -15313,14 +15304,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -16990,7 +16974,7 @@ workbox-window@7.0.0, workbox-window@^7.0.0: "@types/trusted-types" "^2.0.2" workbox-core "7.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -17008,15 +16992,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"