Compare commits

...

91 Commits

Author SHA1 Message Date
Sébastien Lorber 26e0bd928c
docs(website): Announce v3.2 on website/homepage (#10004) 2024-03-29 19:43:45 +01:00
Sébastien Lorber 70415a4cef
chore: fix codesandbox default privacy (#9993) 2024-03-29 18:30:47 +01:00
Sébastien Lorber a34e3f8f91
chore: update examples to v3.2.0 (#10003) 2024-03-29 18:27:36 +01:00
Sébastien Lorber 7edfe0e2d1
docs: fix 3.2 blog post headings (#10002) 2024-03-29 18:06:33 +01:00
Sébastien Lorber debfc87d34
chore: release Docusaurus v3.2.0 (#10000) 2024-03-29 17:51:27 +01:00
Sébastien Lorber 1a5fe5c412
fix(mdx-loader): Ignore contentTitle coming after Markdown thematicBreak (#9999) 2024-03-29 15:07:53 +01:00
ozaki 821247142e
refactor(utils): remove duplicated function (#9972)
Co-authored-by: sebastien <lorber.sebastien@gmail.com>
2024-03-29 14:45:30 +01:00
Sébastien Lorber efbe474e9c
refactor(core): improve dev perf, fine-grained site reloads - part 3 (#9975) 2024-03-28 12:39:07 +01:00
dependabot[bot] 06e70a4f9a
chore(deps): bump actions/dependency-review-action from 4.1.3 to 4.2.4 (#9981)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-28 11:37:03 +01:00
dependabot[bot] 1430c85a82
chore(deps): bump katex from 0.16.8 to 0.16.10 (#9982)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-28 11:36:54 +01:00
dependabot[bot] 8024d9b858
chore(deps): bump express from 4.18.2 to 4.19.2 (#9983)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-28 11:36:44 +01:00
dependabot[bot] 3877c1254f
chore(deps): bump webpack-dev-middleware from 5.3.3 to 5.3.4 (#9977)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <dependabot[bot]@users.noreply.github.com>
2024-03-24 17:26:35 +01:00
Vlad Nestorov 6f17d54938
docs: use official GitHub Action to deploy to GitHub Pages (#9937)
Co-authored-by: ozakione <29860391+OzakIOne@users.noreply.github.com>
2024-03-21 16:01:10 +01:00
Sébastien Lorber 93a09ea086
refactor(core): improve dev perf, fine-grained site reloads - part2 (#9968) 2024-03-21 13:05:19 +01:00
Fafowora Sunkanmi 91f93656d8
docs: replace VuePress by VitePress on tool comparison section (#9971)
Co-authored-by: Sébastien Lorber <slorber@users.noreply.github.com>
2024-03-21 13:05:05 +01:00
Zed Spencer-Milnes 9a47ec0581
refactor(website): organise blog posts by year (#9965) 2024-03-20 18:08:05 +01:00
Sébastien Lorber 9017fb9b1d
feat(sitemap): add support for "lastmod" (#9954) 2024-03-20 11:47:44 +01:00
Sébastien Lorber 465cf4d82c
refactor(docs,blog): last update timestamp should be in milliseconds instead of seconds (#9963) 2024-03-19 09:51:27 +01:00
dependabot[bot] 6c724ed857
chore(deps): bump follow-redirects from 1.15.4 to 1.15.6 (#9958)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-18 16:28:58 +01:00
ozaki c745021b01
feat(blog): add LastUpdateAuthor & LastUpdateTime (#9912)
Co-authored-by: OzakIOne <OzakIOne@users.noreply.github.com>
Co-authored-by: sebastien <lorber.sebastien@gmail.com>
2024-03-15 12:50:06 +01:00
Max Schmitt 7938803747
fix(a11y): move focus algolia-search focus back to search input on Escape (#9945) 2024-03-14 17:26:26 +01:00
ozaki 14bec09670
fix(blog): apply trailing slash to blog feed (#9920)
Co-authored-by: sebastien <lorber.sebastien@gmail.com>
2024-03-14 16:45:00 +01:00
Max Schmitt 032e3b8f4d
fix(theme): improve a11y of DocSidebarItemCategory expand/collapsed button (#9944) 2024-03-14 14:59:36 +01:00
ozaki 6151a41428
docs(website): update wrong translate id (#9946) 2024-03-14 12:54:41 +01:00
Sébastien Lorber 8d115a9e0d
feat(core): add new plugin allContentLoaded lifecycle (#9931) 2024-03-08 19:13:59 +01:00
Sébastien Lorber d02b96f7f5
refactor(core): improve dev perf, fine-grained site reloads - part1 (#9903) 2024-03-08 12:59:11 +01:00
ozaki eb6424cc27
docs(showcase): update link (#9929) 2024-03-07 23:47:28 +01:00
Hallbjörn Magnússon ee53208f9d
feat(theme-translations) Icelandic (is) (#9928)
Co-authored-by: Sébastien Lorber <slorber@users.noreply.github.com>
2024-03-07 19:18:40 +01:00
suenryu 9d89607d4d
fix(theme-translations): complete and modify Japanese translations (#9915) 2024-03-07 18:59:09 +01:00
ozaki 319d73d54f
docs(team): update active docusaurus team member (#9893)
Co-authored-by: Sébastien Lorber <slorber@users.noreply.github.com>
Co-authored-by: OzakIOne <OzakIOne@users.noreply.github.com>
2024-03-06 11:17:28 +01:00
suenryu 2b6d8bd646
fix(theme-translations): add Japanese translations (#9910) 2024-03-06 00:00:57 +01:00
ozaki 6d8108b653
docs: update legacy MDX v1 links to markdown links (#9914) 2024-03-05 23:46:52 +01:00
ozaki 2851c93d0d
feat(blog): allow processing blog posts through a processBlogPosts function (#9886)
Co-authored-by: OzakIOne <OzakIOne@users.noreply.github.com>
Co-authored-by: sebastien <lorber.sebastien@gmail.com>
Co-authored-by: Sébastien Lorber <slorber@users.noreply.github.com>
2024-03-05 22:58:16 +01:00
ozaki 05279dc3d8
docs: update legacy MDX v1 links to markdown links (#9913) 2024-03-05 20:04:12 +01:00
Joshua Chen 324c853da0
docs: emphasize "index slug" convention (#9906) 2024-03-04 12:33:17 +01:00
Sébastien Lorber 2152572a3b
fix(mdx-loader): mdx-code-block should support CRLF (#9897) 2024-02-29 13:20:03 +01:00
dependabot[bot] 0589b1475d
chore(deps): bump actions/dependency-review-action from 4.1.2 to 4.1.3 (#9892)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-27 10:05:12 +01:00
Sébastien Lorber f159bb2472
perf: optimize getFileCommitDate, make it async (#9890) 2024-02-24 23:32:18 +01:00
ozaki 0279c329ad
refactor(theme): dates should be formatted on the client-side instead of in nodejs code (#9868)
Co-authored-by: OzakIOne <OzakIOne@users.noreply.github.com>
Co-authored-by: sebastien <lorber.sebastien@gmail.com>
2024-02-23 18:30:05 +01:00
Sébastien Lorber 6bf21d215c
fix(core): fix default i18n calendar used, infer it from locale if possible (#9878) 2024-02-22 17:06:12 +01:00
axel7083 85d74245f8
fix(argos): waiting for images decoding (#9875)
Co-authored-by: Sébastien Lorber <slorber@users.noreply.github.com>
Co-authored-by: sebastien <lorber.sebastien@gmail.com>
2024-02-22 15:02:39 +01:00
Josue [4tro] A 76c0f5c2d9
fix(theme-translations): complete and improve Spanish theme translations (#9872)
Co-authored-by: Sébastien Lorber <slorber@users.noreply.github.com>
2024-02-22 14:35:57 +01:00
dependabot[bot] 7a4826baa8
chore(deps): bump actions/dependency-review-action from 4.0.0 to 4.1.2 (#9869)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-22 14:02:19 +01:00
Aolin 06dabc69bd
docs: fix typos in deployment.mdx (#9877) 2024-02-22 10:39:24 +01:00
dependabot[bot] 4ca9b8e508
chore(deps): bump ip from 2.0.0 to 2.0.1 (#9874)
Bumps [ip](https://github.com/indutny/node-ip) from 2.0.0 to 2.0.1.
- [Commits](https://github.com/indutny/node-ip/compare/v2.0.0...v2.0.1)

---
updated-dependencies:
- dependency-name: ip
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-20 14:43:21 -05:00
Chris Bateman 6b0f9703a3
chore(website): update @crowdin/crowdin-api-client (#9865) 2024-02-20 19:50:39 +01:00
krinza.eth 0d3ac877d5
docs(showcase): remove supabase (#9856)
Co-authored-by: Sébastien Lorber <slorber@users.noreply.github.com>
2024-02-20 17:54:45 +01:00
ozaki 1bbc68bc30
chore(deps): update npm to yarn from 2.0.0 to 2.2.1 (#9861)
* chore(deps): update npm to yarn from 2.0.0 to 2.2.1

* test: update tests
2024-02-17 16:36:57 -05:00
Нуриль Барадусов 3426f848a3
fix(theme-classic): should use plurals for category items description (#9851)
Co-authored-by: Нуриль Барадусов <nuril.b@ati.su>
Co-authored-by: sebastien <lorber.sebastien@gmail.com>
2024-02-15 20:07:48 +01:00
Sébastien Lorber b26e262981
fix(core): ensure core error boundary is able to render theme layout (#9852) 2024-02-15 17:25:17 +01:00
John Reilly 60d9346965
refactor(theme): use JSON-LD instead of microdata for blog structured data (#9669)
Co-authored-by: Joshua Chen <sidachen2003@gmail.com>
Co-authored-by: sebastien <lorber.sebastien@gmail.com>
2024-02-15 16:25:21 +01:00
翊小久 8abd1899a6
feat(swizzle): ask user preferred language if no language CLI option provided (#9681)
Co-authored-by: sebastien <lorber.sebastien@gmail.com>
2024-02-15 15:08:01 +01:00
Rafael Martins 628752d92a
feat(create-docusaurus): ask user for preferred language when no language CLI option provided (#9442)
Co-authored-by: sebastien <lorber.sebastien@gmail.com>
2024-02-15 13:05:05 +01:00
ozaki 6fd8408a2c
docs: typo (#9845) 2024-02-14 14:39:03 +01:00
ozaki 77723a1121
feat(plugin-vercel-analytics): add new vercel analytics plugin (#9687)
Co-authored-by: sebastien <lorber.sebastien@gmail.com>
2024-02-14 13:25:39 +01:00
Liviu Ionescu 70ba9d2d01
feat(blog): add blog pageBasePath plugin option (#9838)
Co-authored-by: sebastien <lorber.sebastien@gmail.com>
2024-02-13 13:27:23 +01:00
dependabot[bot] cc7f43580c
chore(deps): bump actions/setup-node from 4.0.1 to 4.0.2 (#9843)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-13 11:21:17 +01:00
ozaki a296d727b6
docs(blog-only): add warning and improve error log (#9839) 2024-02-09 19:34:32 +01:00
Sébastien Lorber 34297bc56d
refactor(core): internalize, simplify and optimize the SSG logic (#9798) 2024-02-08 18:44:45 +01:00
dependabot[bot] d740be0e9c
chore(deps): bump treosh/lighthouse-ci-action from 10.1.0 to 11.4.0 (#9824)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-06 10:28:33 +01:00
dependabot[bot] 506e763c07
chore(deps): bump marocchino/sticky-pull-request-comment from 2.8.0 to 2.9.0 (#9823)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-06 10:28:25 +01:00
n4n5 9e5679ff1a
docs: Add docs for Mermaid Component (#9816)
* add docs

* fix: missplace

* Update markdown-features-diagrams.mdx

* yarn format

* fix: correct language

* fix: typo
2024-02-02 17:26:07 +01:00
Vahid Naderi c1ac06768b
fix(i18n): add missing theme translations for fa locale (#9812) 2024-02-01 14:21:36 +01:00
Alberto Vilches 436e20a42a
docs: add "TypeScript support" section in the "Modules using Sass/SCSS" section (#9807)
Co-authored-by: sebastien <lorber.sebastien@gmail.com>
2024-02-01 12:03:53 +01:00
Tatsunori Uchino 09b2005759
docs: backport #9471, #9692, and #9695 changes in v3.1.1 docs (#9804) 2024-02-01 11:46:18 +01:00
Sébastien Lorber 48cab8e3fb
chore: release Docusaurus 3.1.1 examples (#9797) 2024-01-26 14:33:03 +01:00
Sébastien Lorber ed88097f63
chore: release Docusaurus 3.1.1 docs (#9794) 2024-01-26 14:10:57 +01:00
Shamoil Arsiwala 3d8b70e0b3
docs: fix documented path of swizzled component (#9786) 2024-01-25 19:57:42 +01:00
Sébastien Lorber d3142c5ed5
fix(core): broken links optimization behaves differently than non-optimized logic (#9791) 2024-01-25 19:49:45 +01:00
Sébastien Lorber 2f2ed41829
fix(core): links with target "_blank" should no be checked by the broken link checker (#9788) 2024-01-25 18:18:11 +01:00
Emjek Cordover cce1698c60
docs: Note `showLastUpdateTime` requires complete checkout (#9784) 2024-01-25 13:19:55 +01:00
eitsupi 0266549a51
fix(mdx-loader): allow spaces before `mdx-code-block` info string (#9776) 2024-01-24 14:12:54 +01:00
Sébastien Lorber c827b6de2d
perf(core): optimize broken links checker (#9778) 2024-01-24 12:14:26 +01:00
Cole Cecil 8597e5dcdb
docs: update troubleshooting details for v3 migration (#9782) 2024-01-24 12:12:06 +01:00
dawei-wang 4b444d55e5
fix(create-docusaurus): fix typo in init template sample docs (#9783) 2024-01-24 11:57:55 +01:00
dependabot[bot] 333889d38d
chore(deps): bump actions/dependency-review-action from 3.1.5 to 4.0.0 (#9779)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-23 10:17:31 +01:00
Anatoly Kopyl 3c982127d7
fix(mdx-loader): the table-of-contents should display toc/headings of imported MDX partials (#9684)
Co-authored-by: Titus <tituswormer@gmail.com>
Co-authored-by: sebastienlorber <lorber.sebastien@gmail.com>
2024-01-19 18:58:11 +01:00
Jack Robson bed11f62bc
fix(core): conditionally include `hostname` parameter when using… (#9407) 2024-01-18 14:03:45 +01:00
Tatsunori Uchino f794559962
docs: add admonition for incompatibility with `*` and `**` in Chinese and Japanese in MDX v2+ (#9692) 2024-01-18 13:50:15 +01:00
Clément Vannicatte 604ccaa89c
docs: update Algolia DocSearch support links (#9755) 2024-01-18 13:31:24 +01:00
Sébastien Lorber 0469590cc4
docs: GitHub actions deployment should use fetch-depth: 0 + actions upgrades (#9759) 2024-01-18 12:01:02 +01:00
Sanjaiyan Parthipan e1899786a4
polish(theme): MDX images should use async decoding (#9470) 2024-01-12 21:25:18 +01:00
Tatsunori Uchino d75f99f9a6
fix(theme-common): fix missing code block MagicComments style in Visual Basic (.NET) 16 (#9727) 2024-01-12 21:13:24 +01:00
Sébastien Lorber 0a2e6e67d3
fix: remove old useless mdx typedefs (#9733) 2024-01-12 19:12:09 +01:00
Sébastien Lorber 4388267c26
fix(core): various broken anchor link fixes (#9732) 2024-01-12 16:09:45 +01:00
dependabot[bot] d94adf6a6c
chore(deps): bump follow-redirects from 1.15.2 to 1.15.4 (#9723)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-09 13:24:00 +01:00
dependabot[bot] 21599e22d0
chore(deps): bump actions/dependency-review-action from 3.1.4 to 3.1.5 (#9718)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-09 12:58:44 +01:00
Tatsunori Uchino 849608bcab
chore: check MDX even by VS Code cspell extension (#9713) 2024-01-08 15:51:20 +01:00
Sébastien Lorber e19b681e51
chore: Docusaurus v3.1 examples (#9707) 2024-01-05 19:46:02 +01:00
Sébastien Lorber c7e090f13a
chore: Docusaurus v3.1 docs + changelog + blog post (#9706) 2024-01-05 19:31:58 +01:00
Sébastien Lorber 16500436f7
fix(theme): allow empty code blocks and live playgrounds (#9704) 2024-01-05 13:37:48 +01:00
683 changed files with 55824 additions and 6963 deletions

View File

@ -37,5 +37,6 @@
"*.min.*",
"jest/vendor"
],
"ignoreRegExpList": ["Email", "Urls", "#[\\w-]*"]
"ignoreRegExpList": ["Email", "Urls", "#[\\w-]*"],
"enableFiletypes": ["mdx"]
}

13
.eslintrc.js vendored
View File

@ -85,13 +85,14 @@ module.exports = {
ignorePattern: '(eslint-disable|@)',
},
],
'arrow-body-style': OFF,
'no-await-in-loop': OFF,
'no-case-declarations': WARNING,
'no-console': OFF,
'no-constant-binary-expression': ERROR,
'no-continue': OFF,
'no-control-regex': WARNING,
'no-else-return': [WARNING, {allowElseIf: true}],
'no-else-return': OFF,
'no-empty': [WARNING, {allowEmptyCatch: true}],
'no-lonely-if': WARNING,
'no-nested-ternary': WARNING,
@ -203,7 +204,10 @@ module.exports = {
})),
],
'no-template-curly-in-string': WARNING,
'no-unused-expressions': [WARNING, {allowTaggedTemplates: true}],
'no-unused-expressions': [
WARNING,
{allowTaggedTemplates: true, allowShortCircuit: true},
],
'no-useless-escape': WARNING,
'no-void': [ERROR, {allowAsStatement: true}],
'prefer-destructuring': WARNING,
@ -344,10 +348,7 @@ module.exports = {
ERROR,
{'ts-expect-error': 'allow-with-description'},
],
'@typescript-eslint/consistent-indexed-object-style': [
WARNING,
'index-signature',
],
'@typescript-eslint/consistent-indexed-object-style': OFF,
'@typescript-eslint/consistent-type-imports': [
WARNING,
{disallowTypeAnnotations: false},

View File

@ -30,7 +30,7 @@ jobs:
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: 18

View File

@ -24,7 +24,7 @@ jobs:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up Node
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: '18'
cache: yarn

View File

@ -38,7 +38,7 @@ jobs:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up Node
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: '18'
cache: yarn
@ -60,7 +60,7 @@ jobs:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up Node
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
cache: yarn
- name: Installation

View File

@ -21,7 +21,7 @@ jobs:
with:
fetch-depth: 0 # Needed to get the commit number with "git rev-list --count HEAD"
- name: Set up Node
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: '18'
cache: yarn

View File

@ -15,4 +15,4 @@ jobs:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Dependency Review
uses: actions/dependency-review-action@01bc87099ba56df1e897b6874784491ea6309bc4 # 3.1.4
uses: actions/dependency-review-action@733dd5d4a5203f238c33806593ec0f5fc5343d8c # 4.2.4

View File

@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: 18
@ -36,7 +36,7 @@ jobs:
- name: Audit URLs using Lighthouse
id: lighthouse_audit
uses: treosh/lighthouse-ci-action@03becbfc543944dd6e7534f7ff768abb8a296826 # 10.1.0
uses: treosh/lighthouse-ci-action@1b0e7c33270fbba31a18a0fbb1de7cc5256b6d39 # 11.4.0
with:
urls: |
http://localhost:3000
@ -64,7 +64,7 @@ jobs:
- name: Add Lighthouse stats as comment
id: comment_to_pr
uses: marocchino/sticky-pull-request-comment@efaaab3fd41a9c3de579aba759d2552635e590fd # 2.8.0
uses: marocchino/sticky-pull-request-comment@331f8f5b4215f0445d3c07b4967662a32a2d3e31 # 2.9.0
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
number: ${{ github.event.pull_request.number }}

View File

@ -22,7 +22,7 @@ jobs:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up Node
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: '18'
cache: yarn

View File

@ -24,9 +24,9 @@ jobs:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up Node
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: '16'
node-version: '20'
cache: yarn
- name: Installation
run: yarn

View File

@ -43,7 +43,7 @@ jobs:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js ${{ matrix.node }}
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: ${{ matrix.node }}
cache: yarn
@ -77,7 +77,7 @@ jobs:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js 18
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: '18'
cache: yarn
@ -131,7 +131,7 @@ jobs:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js 18
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: '18'
cache: yarn
@ -161,7 +161,7 @@ jobs:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js 18
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: '18'
cache: yarn

View File

@ -28,7 +28,7 @@ jobs:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up Node
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: '18'
cache: yarn

View File

@ -33,7 +33,7 @@ jobs:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js ${{ matrix.node }}
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: ${{ matrix.node }}
- name: Installation

View File

@ -31,7 +31,7 @@ jobs:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js ${{ matrix.node }}
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: ${{ matrix.node }}
cache: yarn

View File

@ -1,5 +1,232 @@
# Docusaurus 2 Changelog
## 3.2.0 (2024-03-29)
#### :rocket: New Feature
- `docusaurus-plugin-content-blog`, `docusaurus-plugin-content-docs`, `docusaurus-plugin-content-pages`, `docusaurus-plugin-sitemap`, `docusaurus-types`, `docusaurus-utils`, `docusaurus`
- [#9954](https://github.com/facebook/docusaurus/pull/9954) feat(sitemap): add support for "lastmod" ([@slorber](https://github.com/slorber))
- `docusaurus-plugin-content-blog`, `docusaurus-plugin-content-docs`, `docusaurus-theme-classic`, `docusaurus-theme-common`, `docusaurus-utils-validation`, `docusaurus-utils`
- [#9912](https://github.com/facebook/docusaurus/pull/9912) feat(blog): add LastUpdateAuthor & LastUpdateTime ([@OzakIOne](https://github.com/OzakIOne))
- `docusaurus-plugin-debug`, `docusaurus-types`, `docusaurus`
- [#9931](https://github.com/facebook/docusaurus/pull/9931) feat(core): add new plugin allContentLoaded lifecycle ([@slorber](https://github.com/slorber))
- `docusaurus-theme-translations`
- [#9928](https://github.com/facebook/docusaurus/pull/9928) feat(theme-translations) Icelandic (is) ([@Hallinn](https://github.com/Hallinn))
- `docusaurus-plugin-content-blog`
- [#9886](https://github.com/facebook/docusaurus/pull/9886) feat(blog): allow processing blog posts through a processBlogPosts function ([@OzakIOne](https://github.com/OzakIOne))
- [#9838](https://github.com/facebook/docusaurus/pull/9838) feat(blog): add blog pageBasePath plugin option ([@ilg-ul](https://github.com/ilg-ul))
- `docusaurus`
- [#9681](https://github.com/facebook/docusaurus/pull/9681) feat(swizzle): ask user preferred language if no language CLI option provided ([@yixiaojiu](https://github.com/yixiaojiu))
- `create-docusaurus`, `docusaurus-utils`
- [#9442](https://github.com/facebook/docusaurus/pull/9442) feat(create-docusaurus): ask user for preferred language when no language CLI option provided ([@Rafael-Martins](https://github.com/Rafael-Martins))
- `docusaurus-plugin-vercel-analytics`
- [#9687](https://github.com/facebook/docusaurus/pull/9687) feat(plugin-vercel-analytics): add new vercel analytics plugin ([@OzakIOne](https://github.com/OzakIOne))
- `docusaurus-mdx-loader`
- [#9684](https://github.com/facebook/docusaurus/pull/9684) feat(mdx-loader): the table-of-contents should display toc/headings of imported MDX partials ([@anatolykopyl](https://github.com/anatolykopyl))
#### :bug: Bug Fix
- `docusaurus-mdx-loader`
- [#9999](https://github.com/facebook/docusaurus/pull/9999) fix(mdx-loader): Ignore contentTitle coming after Markdown thematicBreak ([@slorber](https://github.com/slorber))
- `docusaurus-theme-search-algolia`
- [#9945](https://github.com/facebook/docusaurus/pull/9945) fix(a11y): move focus algolia-search focus back to search input on Escape ([@mxschmitt](https://github.com/mxschmitt))
- `docusaurus-plugin-content-blog`
- [#9920](https://github.com/facebook/docusaurus/pull/9920) fix(blog): apply trailing slash to blog feed ([@OzakIOne](https://github.com/OzakIOne))
- `docusaurus-theme-classic`
- [#9944](https://github.com/facebook/docusaurus/pull/9944) fix(theme): improve a11y of DocSidebarItemCategory expand/collapsed button ([@mxschmitt](https://github.com/mxschmitt))
- `docusaurus-theme-translations`
- [#9915](https://github.com/facebook/docusaurus/pull/9915) fix(theme-translations): complete and modify Japanese translations ([@Suenaga-Ryuya](https://github.com/Suenaga-Ryuya))
- [#9910](https://github.com/facebook/docusaurus/pull/9910) fix(theme-translations): add Japanese translations ([@Suenaga-Ryuya](https://github.com/Suenaga-Ryuya))
- [#9872](https://github.com/facebook/docusaurus/pull/9872) fix(theme-translations): complete and improve Spanish theme translations ([@4troDev](https://github.com/4troDev))
- [#9812](https://github.com/facebook/docusaurus/pull/9812) fix(i18n): add missing theme translations for fa locale ([@VahidNaderi](https://github.com/VahidNaderi))
- `docusaurus-utils`
- [#9897](https://github.com/facebook/docusaurus/pull/9897) fix(mdx-loader): mdx-code-block should support CRLF ([@slorber](https://github.com/slorber))
- `docusaurus`
- [#9878](https://github.com/facebook/docusaurus/pull/9878) fix(core): fix default i18n calendar used, infer it from locale if possible ([@slorber](https://github.com/slorber))
- [#9852](https://github.com/facebook/docusaurus/pull/9852) fix(core): ensure core error boundary is able to render theme layout ([@slorber](https://github.com/slorber))
- `docusaurus-remark-plugin-npm2yarn`
- [#9861](https://github.com/facebook/docusaurus/pull/9861) fix(remark-npm2yarn): update npm-to-yarn from 2.0.0 to 2.2.1, fix pnpm extra args syntax ([@OzakIOne](https://github.com/OzakIOne))
- `docusaurus-theme-classic`, `docusaurus-theme-translations`
- [#9851](https://github.com/facebook/docusaurus/pull/9851) fix(theme-classic): should use plurals for category items description ([@baradusov](https://github.com/baradusov))
#### :running_woman: Performance
- `docusaurus-types`, `docusaurus-utils`, `docusaurus`
- [#9975](https://github.com/facebook/docusaurus/pull/9975) refactor(core): improve dev perf, fine-grained site reloads - part 3 ([@slorber](https://github.com/slorber))
- `docusaurus-types`, `docusaurus`
- [#9968](https://github.com/facebook/docusaurus/pull/9968) refactor(core): improve dev perf, fine-grained site reloads - part2 ([@slorber](https://github.com/slorber))
- `docusaurus-plugin-content-docs`, `docusaurus-plugin-content-pages`, `docusaurus-types`, `docusaurus`
- [#9903](https://github.com/facebook/docusaurus/pull/9903) refactor(core): improve dev perf, fine-grained site reloads - part1 ([@slorber](https://github.com/slorber))
- `docusaurus-plugin-content-blog`, `docusaurus-plugin-content-docs`, `docusaurus-utils`
- [#9890](https://github.com/facebook/docusaurus/pull/9890) perf: optimize getFileCommitDate, make it async ([@slorber](https://github.com/slorber))
- `docusaurus`
- [#9798](https://github.com/facebook/docusaurus/pull/9798) refactor(core): internalize, simplify and optimize the SSG logic ([@slorber](https://github.com/slorber))
#### :nail_care: Polish
- `docusaurus-plugin-content-blog`, `docusaurus-plugin-content-docs`, `docusaurus-theme-classic`, `docusaurus-theme-common`
- [#9868](https://github.com/facebook/docusaurus/pull/9868) refactor(theme): dates should be formatted on the client-side instead of in nodejs code ([@OzakIOne](https://github.com/OzakIOne))
- `docusaurus-plugin-content-blog`, `docusaurus-theme-classic`, `docusaurus-theme-common`, `docusaurus-types`
- [#9669](https://github.com/facebook/docusaurus/pull/9669) refactor(theme): use JSON-LD instead of microdata for blog structured data ([@johnnyreilly](https://github.com/johnnyreilly))
- `docusaurus-plugin-content-docs`
- [#9839](https://github.com/facebook/docusaurus/pull/9839) refactor(blog): improve doc global data hook error message + add doc warning to blogOnly mode ([@OzakIOne](https://github.com/OzakIOne))
#### :memo: Documentation
- [#9937](https://github.com/facebook/docusaurus/pull/9937) docs: use official GitHub Action to deploy to GitHub Pages ([@vlad-nestorov](https://github.com/vlad-nestorov))
- [#9971](https://github.com/facebook/docusaurus/pull/9971) docs: replace VuePress by VitePress on tool comparison section ([@sunkanmii](https://github.com/sunkanmii))
- [#9914](https://github.com/facebook/docusaurus/pull/9914) docs: update legacy MDX v1 links to markdown links ([@OzakIOne](https://github.com/OzakIOne))
- [#9913](https://github.com/facebook/docusaurus/pull/9913) docs: update legacy MDX v1 links to markdown links ([@OzakIOne](https://github.com/OzakIOne))
- [#9906](https://github.com/facebook/docusaurus/pull/9906) docs: emphasize "index slug" convention ([@Josh-Cena](https://github.com/Josh-Cena))
- [#9877](https://github.com/facebook/docusaurus/pull/9877) docs: fix typos in deployment.mdx ([@Oreoxmt](https://github.com/Oreoxmt))
- [#9845](https://github.com/facebook/docusaurus/pull/9845) docs: typo ([@OzakIOne](https://github.com/OzakIOne))
- [#9816](https://github.com/facebook/docusaurus/pull/9816) docs: Add docs for Mermaid Component ([@Its-Just-Nans](https://github.com/Its-Just-Nans))
#### :robot: Dependencies
- [#9981](https://github.com/facebook/docusaurus/pull/9981) chore(deps): bump actions/dependency-review-action from 4.1.3 to 4.2.4 ([@dependabot[bot]](https://github.com/apps/dependabot))
- [#9982](https://github.com/facebook/docusaurus/pull/9982) chore(deps): bump katex from 0.16.8 to 0.16.10 ([@dependabot[bot]](https://github.com/apps/dependabot))
- [#9983](https://github.com/facebook/docusaurus/pull/9983) chore(deps): bump express from 4.18.2 to 4.19.2 ([@dependabot[bot]](https://github.com/apps/dependabot))
- [#9977](https://github.com/facebook/docusaurus/pull/9977) chore(deps): bump webpack-dev-middleware from 5.3.3 to 5.3.4 ([@dependabot[bot]](https://github.com/apps/dependabot))
- [#9958](https://github.com/facebook/docusaurus/pull/9958) chore(deps): bump follow-redirects from 1.15.4 to 1.15.6 ([@dependabot[bot]](https://github.com/apps/dependabot))
- [#9892](https://github.com/facebook/docusaurus/pull/9892) chore(deps): bump actions/dependency-review-action from 4.1.2 to 4.1.3 ([@dependabot[bot]](https://github.com/apps/dependabot))
- [#9869](https://github.com/facebook/docusaurus/pull/9869) chore(deps): bump actions/dependency-review-action from 4.0.0 to 4.1.2 ([@dependabot[bot]](https://github.com/apps/dependabot))
- [#9874](https://github.com/facebook/docusaurus/pull/9874) chore(deps): bump ip from 2.0.0 to 2.0.1 ([@dependabot[bot]](https://github.com/apps/dependabot))
- [#9843](https://github.com/facebook/docusaurus/pull/9843) chore(deps): bump actions/setup-node from 4.0.1 to 4.0.2 ([@dependabot[bot]](https://github.com/apps/dependabot))
- [#9824](https://github.com/facebook/docusaurus/pull/9824) chore(deps): bump treosh/lighthouse-ci-action from 10.1.0 to 11.4.0 ([@dependabot[bot]](https://github.com/apps/dependabot))
- [#9823](https://github.com/facebook/docusaurus/pull/9823) chore(deps): bump marocchino/sticky-pull-request-comment from 2.8.0 to 2.9.0 ([@dependabot[bot]](https://github.com/apps/dependabot))
#### :wrench: Maintenance
- `docusaurus-plugin-client-redirects`, `docusaurus-plugin-content-docs`, `docusaurus-utils-common`, `docusaurus-utils-validation`, `docusaurus-utils`, `docusaurus`
- [#9972](https://github.com/facebook/docusaurus/pull/9972) refactor(utils): remove duplicated function ([@OzakIOne](https://github.com/OzakIOne))
- Other
- [#9965](https://github.com/facebook/docusaurus/pull/9965) refactor(website): organise blog posts by year ([@GingerGeek](https://github.com/GingerGeek))
- [#9865](https://github.com/facebook/docusaurus/pull/9865) chore(website): update @crowdin/crowdin-api-client ([@chris-bateman](https://github.com/chris-bateman))
- `docusaurus-plugin-content-blog`, `docusaurus-plugin-content-docs`, `docusaurus-theme-classic`, `docusaurus-theme-common`, `docusaurus-utils`
- [#9963](https://github.com/facebook/docusaurus/pull/9963) refactor(docs,blog): last update timestamp should be in milliseconds instead of seconds ([@slorber](https://github.com/slorber))
#### Committers: 22
- Aolin ([@Oreoxmt](https://github.com/Oreoxmt))
- Anatoly Kopyl ([@anatolykopyl](https://github.com/anatolykopyl))
- Chris Bateman ([@chris-bateman](https://github.com/chris-bateman))
- Fafowora Sunkanmi ([@sunkanmii](https://github.com/sunkanmii))
- Hallbjörn Magnússon ([@Hallinn](https://github.com/Hallinn))
- John Reilly ([@johnnyreilly](https://github.com/johnnyreilly))
- Joshua Chen ([@Josh-Cena](https://github.com/Josh-Cena))
- Josue [4tro] A ([@4troDev](https://github.com/4troDev))
- Liviu Ionescu ([@ilg-ul](https://github.com/ilg-ul))
- Max Schmitt ([@mxschmitt](https://github.com/mxschmitt))
- Rafael Martins ([@Rafael-Martins](https://github.com/Rafael-Martins))
- Sébastien Lorber ([@slorber](https://github.com/slorber))
- Vahid Naderi ([@VahidNaderi](https://github.com/VahidNaderi))
- Vlad Nestorov ([@vlad-nestorov](https://github.com/vlad-nestorov))
- Zed Spencer-Milnes ([@GingerGeek](https://github.com/GingerGeek))
- axel7083 ([@axel7083](https://github.com/axel7083))
- krinza.eth ([@kaymomin](https://github.com/kaymomin))
- n4n5 ([@Its-Just-Nans](https://github.com/Its-Just-Nans))
- ozaki ([@OzakIOne](https://github.com/OzakIOne))
- suenryu ([@Suenaga-Ryuya](https://github.com/Suenaga-Ryuya))
- Нуриль Барадусов ([@baradusov](https://github.com/baradusov))
- 翊小久 ([@yixiaojiu](https://github.com/yixiaojiu))
## 3.1.1 (2024-01-26)
#### :bug: Bug Fix
- `docusaurus-types`, `docusaurus`
- [#9791](https://github.com/facebook/docusaurus/pull/9791) fix(core): broken links optimization behaves differently than non-optimized logic ([@slorber](https://github.com/slorber))
- `docusaurus`
- [#9788](https://github.com/facebook/docusaurus/pull/9788) fix(core): links with target "\_blank" should no be checked by the broken link checker ([@slorber](https://github.com/slorber))
- [#9407](https://github.com/facebook/docusaurus/pull/9407) fix(core): conditionally include `hostname` parameter when using… ([@jack-robson](https://github.com/jack-robson))
- `docusaurus-utils`
- [#9776](https://github.com/facebook/docusaurus/pull/9776) fix(mdx-loader): allow spaces before `mdx-code-block` info string ([@eitsupi](https://github.com/eitsupi))
- `create-docusaurus`
- [#9783](https://github.com/facebook/docusaurus/pull/9783) fix(create-docusaurus): fix typo in init template sample docs ([@dawei-wang](https://github.com/dawei-wang))
- `docusaurus-theme-common`
- [#9727](https://github.com/facebook/docusaurus/pull/9727) fix(theme-common): fix missing code block MagicComments style in Visual Basic (.NET) 16 ([@tats-u](https://github.com/tats-u))
- `docusaurus-theme-classic`, `docusaurus-theme-mermaid`
- [#9733](https://github.com/facebook/docusaurus/pull/9733) fix: remove old useless mdx typedefs ([@slorber](https://github.com/slorber))
- `docusaurus-module-type-aliases`, `docusaurus-theme-classic`, `docusaurus-theme-common`, `docusaurus-utils`, `docusaurus`
- [#9732](https://github.com/facebook/docusaurus/pull/9732) fix(core): various broken anchor link fixes ([@slorber](https://github.com/slorber))
#### :running_woman: Performance
- `docusaurus`
- [#9778](https://github.com/facebook/docusaurus/pull/9778) perf(core): optimize broken links checker ([@slorber](https://github.com/slorber))
#### :nail_care: Polish
- `docusaurus-theme-classic`
- [#9470](https://github.com/facebook/docusaurus/pull/9470) polish(theme): MDX images should use async decoding ([@sanjaiyan-dev](https://github.com/sanjaiyan-dev))
#### Committers: 6
- Jack Robson ([@jack-robson](https://github.com/jack-robson))
- Sanjaiyan Parthipan ([@sanjaiyan-dev](https://github.com/sanjaiyan-dev))
- Sébastien Lorber ([@slorber](https://github.com/slorber))
- Tatsunori Uchino ([@tats-u](https://github.com/tats-u))
- [@dawei-wang](https://github.com/dawei-wang)
- [@eitsupi](https://github.com/eitsupi)
## 3.1.0 (2024-01-05)
#### :rocket: New Feature
- `docusaurus-mdx-loader`, `docusaurus-module-type-aliases`, `docusaurus-theme-classic`, `docusaurus-types`, `docusaurus-utils`, `docusaurus`
- [#9528](https://github.com/facebook/docusaurus/pull/9528) feat(core): make broken link checker detect broken anchors - add `onBrokenAnchors` config ([@OzakIOne](https://github.com/OzakIOne))
- `docusaurus-mdx-loader`, `docusaurus-types`, `docusaurus`
- [#9674](https://github.com/facebook/docusaurus/pull/9674) feat(mdx-loader): add support for siteConfig.markdown.remarkRehypeOptions ([@slorber](https://github.com/slorber))
- `docusaurus-theme-common`
- [#9671](https://github.com/facebook/docusaurus/pull/9671) feat(theme-common): code block MagicComments support for (Visual) Basic/Batch/Fortran/COBOL/ML ([@tats-u](https://github.com/tats-u))
- `docusaurus-mdx-loader`, `docusaurus-plugin-content-blog`, `docusaurus-plugin-content-docs`, `docusaurus-plugin-content-pages`, `docusaurus-types`, `docusaurus-utils`, `docusaurus`
- [#9624](https://github.com/facebook/docusaurus/pull/9624) feat: siteConfig.markdown.parseFrontMatter hook ([@slorber](https://github.com/slorber))
- `docusaurus-utils`
- [#9610](https://github.com/facebook/docusaurus/pull/9610) feat(core): enable port configuration via environment variable ([@OzakIOne](https://github.com/OzakIOne))
#### :bug: Bug Fix
- `docusaurus-theme-classic`, `docusaurus-theme-live-codeblock`
- [#9704](https://github.com/facebook/docusaurus/pull/9704) fix(theme): allow empty code blocks and live playgrounds ([@slorber](https://github.com/slorber))
- `create-docusaurus`
- [#9696](https://github.com/facebook/docusaurus/pull/9696) fix(create-docusaurus): fix init template code blocks, and little improvements ([@slorber](https://github.com/slorber))
- `docusaurus-plugin-pwa`
- [#9668](https://github.com/facebook/docusaurus/pull/9668) fix(pwa-plugin): upgrade workbox ([@SimenB](https://github.com/SimenB))
- `docusaurus`
- [#9648](https://github.com/facebook/docusaurus/pull/9648) fix(cli): output help when no conventional config + no subcommand ([@Josh-Cena](https://github.com/Josh-Cena))
- `docusaurus-theme-live-codeblock`
- [#9631](https://github.com/facebook/docusaurus/pull/9631) fix(live-codeblock): stabilize react-live transformCode callback, fix editor/preview desync ([@slorber](https://github.com/slorber))
- `docusaurus-utils`
- [#9617](https://github.com/facebook/docusaurus/pull/9617) fix(utils): Markdown link replacement with <> but no spaces ([@Josh-Cena](https://github.com/Josh-Cena))
- `docusaurus-module-type-aliases`
- [#9612](https://github.com/facebook/docusaurus/pull/9612) fix(type-aliases): add `title` prop for imported inline SVG React components ([@axmmisaka](https://github.com/axmmisaka))
- `docusaurus-plugin-content-blog`
- [#9581](https://github.com/facebook/docusaurus/pull/9581) fix(content-blog): add baseUrl for author.image_url ([@OzakIOne](https://github.com/OzakIOne))
- `docusaurus-theme-translations`
- [#9477](https://github.com/facebook/docusaurus/pull/9477) fix(i18n): complete translations for theme-common.json Brazilian Portuguese (pt-BR) ([@c0h1b4](https://github.com/c0h1b4))
#### :nail_care: Polish
- `docusaurus-theme-common`
- [#9335](https://github.com/facebook/docusaurus/pull/9335) refactor(theme-common): allow optional desktopBreakpoint param in useWindowSize ([@jgarrow](https://github.com/jgarrow))
#### :wrench: Maintenance
- `docusaurus-theme-search-algolia`
- [#9604](https://github.com/facebook/docusaurus/pull/9604) chore: add lint autofix CI job ([@slorber](https://github.com/slorber))
#### Committers: 8
- Janessa Garrow ([@jgarrow](https://github.com/jgarrow))
- Joshua Chen ([@Josh-Cena](https://github.com/Josh-Cena))
- Simen Bekkhus ([@SimenB](https://github.com/SimenB))
- Sébastien Lorber ([@slorber](https://github.com/slorber))
- Tatsunori Uchino ([@tats-u](https://github.com/tats-u))
- [@c0h1b4](https://github.com/c0h1b4)
- axmmisaka ([@axmmisaka](https://github.com/axmmisaka))
- ozaki ([@OzakIOne](https://github.com/OzakIOne))
## 3.0.1 (2023-11-30)
#### :bug: Bug Fix

View File

@ -11,9 +11,9 @@ const CookieName = 'DocusaurusPlaygroundName';
const PlaygroundConfigs = {
codesandbox:
'https://codesandbox.io/p/sandbox/github/facebook/docusaurus/tree/main/examples/classic?file=%2FREADME.md',
'https://codesandbox.io/p/sandbox/github/facebook/docusaurus/tree/main/examples/classic?file=%2FREADME.md&privacy=public',
'codesandbox-ts':
'https://codesandbox.io/p/sandbox/github/facebook/docusaurus/tree/main/examples/classic-typescript?file=%2FREADME.md',
'https://codesandbox.io/p/sandbox/github/facebook/docusaurus/tree/main/examples/classic-typescript?file=%2FREADME.md&privacy=public',
// Slow to load
// stackblitz: 'https://stackblitz.com/github/facebook/docusaurus/tree/main/examples/classic',

View File

@ -1,6 +1,6 @@
{
"name": "new.docusaurus.io",
"version": "3.0.0",
"version": "3.2.0",
"private": true,
"scripts": {
"start": "npx --package netlify-cli netlify dev"

View File

@ -25,7 +25,8 @@ async function generateTemplateExample(template) {
// Run the docusaurus script to create the template in the examples folder
const command = template.endsWith('-typescript')
? template.replace('-typescript', ' -- --typescript')
: template;
: `${template} -- --javascript`;
shell.exec(
// We use the published init script on purpose, because the local init is
// too new and could generate upcoming/unavailable config options.

View File

@ -52,7 +52,7 @@ git diff --name-only -- '*.json' | sed 's, ,\\&,g' | xargs git checkout --
cd ..
# Build skeleton website with new version
npm_config_registry="$CUSTOM_REGISTRY_URL" npx create-docusaurus@"$NEW_VERSION" test-website classic $EXTRA_OPTS
npm_config_registry="$CUSTOM_REGISTRY_URL" npx create-docusaurus@"$NEW_VERSION" test-website classic --javascript $EXTRA_OPTS
# Stop Docker container
if [[ -z "${KEEP_CONTAINER:-true}" ]] && ( $(docker container inspect "$CONTAINER_NAME" > /dev/null 2>&1) ); then

View File

@ -1,6 +1,6 @@
{
"name": "argos",
"version": "3.0.0",
"version": "3.2.0",
"description": "Argos visual diff tests",
"license": "MIT",
"private": true,
@ -10,8 +10,8 @@
"report": "playwright show-report"
},
"dependencies": {
"@argos-ci/playwright": "^1.0.1",
"@playwright/test": "^1.36.1",
"@argos-ci/playwright": "^1.9.3",
"@playwright/test": "^1.41.2",
"cheerio": "^1.0.0-rc.12"
}
}

View File

@ -28,7 +28,7 @@ tags: [greetings]
Congratulations, you have made your first post!
Feel free to play around and edit this post as much you like.
Feel free to play around and edit this post as much as you like.
```
A new blog post is now available at [http://localhost:3000/blog/greetings](http://localhost:3000/blog/greetings).

View File

@ -61,13 +61,13 @@ You can reference images relative to the current file as well. This is particula
Markdown code blocks are supported with Syntax highlighting.
```jsx title="src/components/HelloDocusaurus.js"
function HelloDocusaurus() {
return (
<h1>Hello, Docusaurus!</h1>
)
}
```
````md
```jsx title="src/components/HelloDocusaurus.js"
function HelloDocusaurus() {
return <h1>Hello, Docusaurus!</h1>;
}
```
````
```jsx title="src/components/HelloDocusaurus.js"
function HelloDocusaurus() {
@ -79,17 +79,19 @@ function HelloDocusaurus() {
Docusaurus has a special syntax to create admonitions and callouts:
:::tip My tip
```md
:::tip My tip
Use this awesome feature option
Use this awesome feature option
:::
:::
:::danger Take care
:::danger Take care
This action is dangerous
This action is dangerous
:::
:::
```
:::tip My tip

View File

@ -16,8 +16,8 @@
"dev": "docusaurus start"
},
"dependencies": {
"@docusaurus/core": "3.0.1",
"@docusaurus/preset-classic": "3.0.1",
"@docusaurus/core": "3.2.0",
"@docusaurus/preset-classic": "3.2.0",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0",
@ -25,9 +25,9 @@
"react-dom": "^18.0.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.0.1",
"@docusaurus/tsconfig": "3.0.1",
"@docusaurus/types": "3.0.1",
"@docusaurus/module-type-aliases": "3.2.0",
"@docusaurus/tsconfig": "3.2.0",
"@docusaurus/types": "3.2.0",
"typescript": "~5.2.2"
},
"browserslist": {

File diff suppressed because it is too large Load Diff

View File

@ -28,7 +28,7 @@ tags: [greetings]
Congratulations, you have made your first post!
Feel free to play around and edit this post as much you like.
Feel free to play around and edit this post as much as you like.
```
A new blog post is now available at [http://localhost:3000/blog/greetings](http://localhost:3000/blog/greetings).

View File

@ -61,13 +61,13 @@ You can reference images relative to the current file as well. This is particula
Markdown code blocks are supported with Syntax highlighting.
```jsx title="src/components/HelloDocusaurus.js"
function HelloDocusaurus() {
return (
<h1>Hello, Docusaurus!</h1>
)
}
```
````md
```jsx title="src/components/HelloDocusaurus.js"
function HelloDocusaurus() {
return <h1>Hello, Docusaurus!</h1>;
}
```
````
```jsx title="src/components/HelloDocusaurus.js"
function HelloDocusaurus() {
@ -79,17 +79,19 @@ function HelloDocusaurus() {
Docusaurus has a special syntax to create admonitions and callouts:
:::tip My tip
```md
:::tip My tip
Use this awesome feature option
Use this awesome feature option
:::
:::
:::danger Take care
:::danger Take care
This action is dangerous
This action is dangerous
:::
:::
```
:::tip My tip

View File

@ -15,8 +15,8 @@
"dev": "docusaurus start"
},
"dependencies": {
"@docusaurus/core": "3.0.1",
"@docusaurus/preset-classic": "3.0.1",
"@docusaurus/core": "3.2.0",
"@docusaurus/preset-classic": "3.2.0",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0",
@ -24,8 +24,8 @@
"react-dom": "^18.0.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.0.1",
"@docusaurus/types": "3.0.1"
"@docusaurus/module-type-aliases": "3.2.0",
"@docusaurus/types": "3.2.0"
},
"browserslist": {
"production": [

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
{
"version": "3.0.0",
"version": "3.2.0",
"npmClient": "yarn",
"useWorkspaces": true,
"useNx": false,

View File

@ -25,7 +25,7 @@ For Docusaurus maintainers, templates can be tested with:
```bash
cd `git rev-parse --show-toplevel` # Back to repo root
rm -rf test-website
yarn create-docusaurus test-website classic
yarn create-docusaurus test-website classic --javascript
cd test-website
yarn start
```
@ -37,7 +37,7 @@ Use the following to test the templates against local packages:
```bash
cd `git rev-parse --show-toplevel` # Back to repo root
rm -rf test-website-in-workspace
yarn create-docusaurus test-website-in-workspace classic
yarn create-docusaurus test-website-in-workspace classic --javascript
cd test-website-in-workspace
yarn build
yarn start

View File

@ -38,6 +38,7 @@ program
'Do not run package manager immediately after scaffolding',
)
.option('-t, --typescript', 'Use the TypeScript template variant')
.option('-j, --javascript', 'Use the JavaScript template variant')
.option(
'-g, --git-strategy <strategy>',
`Only used if the template is a git repository.

View File

@ -1,6 +1,6 @@
{
"name": "create-docusaurus",
"version": "3.0.0",
"version": "3.2.0",
"description": "Create Docusaurus apps easily.",
"type": "module",
"repository": {
@ -22,8 +22,8 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/logger": "3.0.0",
"@docusaurus/utils": "3.0.0",
"@docusaurus/logger": "3.2.0",
"@docusaurus/utils": "3.2.0",
"commander": "^5.1.0",
"fs-extra": "^11.1.1",
"lodash": "^4.17.21",

View File

@ -13,15 +13,29 @@ import logger from '@docusaurus/logger';
import shell from 'shelljs';
import prompts, {type Choice} from 'prompts';
import supportsColor from 'supports-color';
import {escapeShellArg} from '@docusaurus/utils';
import {escapeShellArg, askPreferredLanguage} from '@docusaurus/utils';
type CLIOptions = {
type LanguagesOptions = {
javascript?: boolean;
typescript?: boolean;
};
type CLIOptions = LanguagesOptions & {
packageManager?: PackageManager;
skipInstall?: boolean;
typescript?: boolean;
gitStrategy?: GitStrategy;
};
async function getLanguage(options: LanguagesOptions) {
if (options.typescript) {
return 'typescript';
}
if (options.javascript) {
return 'javascript';
}
return askPreferredLanguage();
}
// Only used in the rare, rare case of running globally installed create +
// using --skip-install. We need a default name to show the tip text
const defaultPackageManager = 'npm';
@ -153,11 +167,14 @@ async function readTemplates(): Promise<Template[]> {
async function copyTemplate(
template: Template,
dest: string,
typescript: boolean,
language: 'javascript' | 'typescript',
): Promise<void> {
await fs.copy(path.join(templatesDir, 'shared'), dest);
await fs.copy(typescript ? template.tsVariantPath! : template.path, dest, {
const sourcePath =
language === 'typescript' ? template.tsVariantPath! : template.path;
await fs.copy(sourcePath, dest, {
// Symlinks don't exist in published npm packages anymore, so this is only
// to prevent errors during local testing
filter: async (filePath) => !(await fs.lstat(filePath)).isSymbolicLink(),
@ -183,6 +200,33 @@ function createTemplateChoices(templates: Template[]): Choice[] {
];
}
async function askTemplateChoice({
templates,
cliOptions,
}: {
templates: Template[];
cliOptions: CLIOptions;
}) {
return cliOptions.gitStrategy
? 'Git repository'
: (
(await prompts(
{
type: 'select',
name: 'template',
message: 'Select a template below...',
choices: createTemplateChoices(templates),
},
{
onCancel() {
logger.error('A choice is required.');
process.exit(1);
},
},
)) as {template: Template | 'Git repository' | 'Local template'}
).template;
}
function isValidGitRepoUrl(gitRepoUrl: string): boolean {
return ['https://', 'git@'].some((item) => gitRepoUrl.startsWith(item));
}
@ -260,7 +304,7 @@ type Source =
| {
type: 'template';
template: Template;
typescript: boolean;
language: 'javascript' | 'typescript';
}
| {
type: 'git';
@ -272,166 +316,193 @@ type Source =
path: string;
};
async function createTemplateSource({
template,
cliOptions,
}: {
template: Template;
cliOptions: CLIOptions;
}): Promise<Source> {
const language = await getLanguage(cliOptions);
if (language === 'typescript' && !template.tsVariantPath) {
logger.error`Template name=${template.name} doesn't provide a TypeScript variant.`;
process.exit(1);
}
return {
type: 'template',
template,
language,
};
}
async function getTemplateSource({
templateName,
templates,
cliOptions,
}: {
templateName: string;
templates: Template[];
cliOptions: CLIOptions;
}): Promise<Source> {
const template = templates.find((t) => t.name === templateName);
if (!template) {
logger.error('Invalid template.');
process.exit(1);
}
return createTemplateSource({template, cliOptions});
}
// Get the template source explicitly requested by the user provided cli option
async function getUserProvidedSource({
reqTemplate,
templates,
cliOptions,
}: {
reqTemplate: string;
templates: Template[];
cliOptions: CLIOptions;
}): Promise<Source> {
if (isValidGitRepoUrl(reqTemplate)) {
if (
cliOptions.gitStrategy &&
!gitStrategies.includes(cliOptions.gitStrategy)
) {
logger.error`Invalid git strategy: name=${
cliOptions.gitStrategy
}. Value must be one of ${gitStrategies.join(', ')}.`;
process.exit(1);
}
return {
type: 'git',
url: reqTemplate,
strategy: cliOptions.gitStrategy ?? 'deep',
};
}
if (await fs.pathExists(path.resolve(reqTemplate))) {
return {
type: 'local',
path: path.resolve(reqTemplate),
};
}
return getTemplateSource({
templateName: reqTemplate,
templates,
cliOptions,
});
}
async function askGitRepositorySource({
cliOptions,
}: {
cliOptions: CLIOptions;
}): Promise<Source> {
const {gitRepoUrl} = (await prompts(
{
type: 'text',
name: 'gitRepoUrl',
validate: (url?: string) => {
if (url && isValidGitRepoUrl(url)) {
return true;
}
return logger.red('Invalid repository URL');
},
message: logger.interpolate`Enter a repository URL from GitHub, Bitbucket, GitLab, or any other public repo.
(e.g: url=${'https://github.com/ownerName/repoName.git'})`,
},
{
onCancel() {
logger.error('A git repo URL is required.');
process.exit(1);
},
},
)) as {gitRepoUrl: string};
let strategy = cliOptions.gitStrategy;
if (!strategy) {
({strategy} = (await prompts(
{
type: 'select',
name: 'strategy',
message: 'How should we clone this repo?',
choices: [
{title: 'Deep clone: preserve full history', value: 'deep'},
{title: 'Shallow clone: clone with --depth=1', value: 'shallow'},
{
title: 'Copy: do a shallow clone, but do not create a git repo',
value: 'copy',
},
{
title: 'Custom: enter your custom git clone command',
value: 'custom',
},
],
},
{
onCancel() {
logger.info`Falling back to name=${'deep'}`;
},
},
)) as {strategy?: GitStrategy});
}
return {
type: 'git',
url: gitRepoUrl,
strategy: strategy ?? 'deep',
};
}
async function askLocalSource(): Promise<Source> {
const {templateDir} = (await prompts(
{
type: 'text',
name: 'templateDir',
validate: async (dir?: string) => {
if (dir) {
const fullDir = path.resolve(dir);
if (await fs.pathExists(fullDir)) {
return true;
}
return logger.red(
logger.interpolate`path=${fullDir} does not exist.`,
);
}
return logger.red('Please enter a valid path.');
},
message:
'Enter a local folder path, relative to the current working directory.',
},
{
onCancel() {
logger.error('A file path is required.');
process.exit(1);
},
},
)) as {templateDir: string};
return {
type: 'local',
path: templateDir,
};
}
async function getSource(
reqTemplate: string | undefined,
templates: Template[],
cliOptions: CLIOptions,
): Promise<Source> {
if (reqTemplate) {
if (isValidGitRepoUrl(reqTemplate)) {
if (
cliOptions.gitStrategy &&
!gitStrategies.includes(cliOptions.gitStrategy)
) {
logger.error`Invalid git strategy: name=${
cliOptions.gitStrategy
}. Value must be one of ${gitStrategies.join(', ')}.`;
process.exit(1);
}
return {
type: 'git',
url: reqTemplate,
strategy: cliOptions.gitStrategy ?? 'deep',
};
} else if (await fs.pathExists(path.resolve(reqTemplate))) {
return {
type: 'local',
path: path.resolve(reqTemplate),
};
}
const template = templates.find((t) => t.name === reqTemplate);
if (!template) {
logger.error('Invalid template.');
process.exit(1);
}
if (cliOptions.typescript && !template.tsVariantPath) {
logger.error`Template name=${reqTemplate} doesn't provide the TypeScript variant.`;
process.exit(1);
}
return {
type: 'template',
template,
typescript: cliOptions.typescript ?? false,
};
return getUserProvidedSource({reqTemplate, templates, cliOptions});
}
const template = cliOptions.gitStrategy
? 'Git repository'
: (
(await prompts(
{
type: 'select',
name: 'template',
message: 'Select a template below...',
choices: createTemplateChoices(templates),
},
{
onCancel() {
logger.error('A choice is required.');
process.exit(1);
},
},
)) as {template: Template | 'Git repository' | 'Local template'}
).template;
const template = await askTemplateChoice({templates, cliOptions});
if (template === 'Git repository') {
const {gitRepoUrl} = (await prompts(
{
type: 'text',
name: 'gitRepoUrl',
validate: (url?: string) => {
if (url && isValidGitRepoUrl(url)) {
return true;
}
return logger.red('Invalid repository URL');
},
message: logger.interpolate`Enter a repository URL from GitHub, Bitbucket, GitLab, or any other public repo.
(e.g: url=${'https://github.com/ownerName/repoName.git'})`,
},
{
onCancel() {
logger.error('A git repo URL is required.');
process.exit(1);
},
},
)) as {gitRepoUrl: string};
let strategy = cliOptions.gitStrategy;
if (!strategy) {
({strategy} = (await prompts(
{
type: 'select',
name: 'strategy',
message: 'How should we clone this repo?',
choices: [
{title: 'Deep clone: preserve full history', value: 'deep'},
{title: 'Shallow clone: clone with --depth=1', value: 'shallow'},
{
title: 'Copy: do a shallow clone, but do not create a git repo',
value: 'copy',
},
{
title: 'Custom: enter your custom git clone command',
value: 'custom',
},
],
},
{
onCancel() {
logger.info`Falling back to name=${'deep'}`;
},
},
)) as {strategy?: GitStrategy});
}
return {
type: 'git',
url: gitRepoUrl,
strategy: strategy ?? 'deep',
};
} else if (template === 'Local template') {
const {templateDir} = (await prompts(
{
type: 'text',
name: 'templateDir',
validate: async (dir?: string) => {
if (dir) {
const fullDir = path.resolve(dir);
if (await fs.pathExists(fullDir)) {
return true;
}
return logger.red(
logger.interpolate`path=${fullDir} does not exist.`,
);
}
return logger.red('Please enter a valid path.');
},
message:
'Enter a local folder path, relative to the current working directory.',
},
{
onCancel() {
logger.error('A file path is required.');
process.exit(1);
},
},
)) as {templateDir: string};
return {
type: 'local',
path: templateDir,
};
return askGitRepositorySource({cliOptions});
}
let useTS = cliOptions.typescript;
if (!useTS && template.tsVariantPath) {
({useTS} = (await prompts({
type: 'confirm',
name: 'useTS',
message:
'This template is available in TypeScript. Do you want to use the TS variant?',
initial: false,
})) as {useTS?: boolean});
if (template === 'Local template') {
return askLocalSource();
}
return {
type: 'template',
return createTemplateSource({
template,
typescript: useTS ?? false,
};
cliOptions,
});
}
async function updatePkg(pkgPath: string, obj: {[key: string]: unknown}) {
@ -452,6 +523,7 @@ export default async function init(
getSiteName(reqName, rootDir),
]);
const dest = path.resolve(rootDir, siteName);
const source = await getSource(reqTemplate, templates, cliOptions);
logger.info('Creating new Docusaurus project...');
@ -470,7 +542,7 @@ export default async function init(
}
} else if (source.type === 'template') {
try {
await copyTemplate(source.template, dest, source.typescript);
await copyTemplate(source.template, dest, source.language);
} catch (err) {
logger.error`Copying Docusaurus template name=${source.template.name} failed!`;
throw err;

View File

@ -1,6 +1,6 @@
{
"name": "docusaurus-2-classic-typescript-template",
"version": "3.0.0",
"version": "3.2.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
@ -15,8 +15,8 @@
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "3.0.0",
"@docusaurus/preset-classic": "3.0.0",
"@docusaurus/core": "3.2.0",
"@docusaurus/preset-classic": "3.2.0",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0",
@ -24,9 +24,9 @@
"react-dom": "^18.0.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.0.0",
"@docusaurus/tsconfig": "3.0.0",
"@docusaurus/types": "3.0.0",
"@docusaurus/module-type-aliases": "3.2.0",
"@docusaurus/tsconfig": "3.2.0",
"@docusaurus/types": "3.2.0",
"typescript": "~5.2.2"
},
"browserslist": {

View File

@ -1,6 +1,6 @@
{
"name": "docusaurus-2-classic-template",
"version": "3.0.0",
"version": "3.2.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
@ -14,8 +14,8 @@
"write-heading-ids": "docusaurus write-heading-ids"
},
"dependencies": {
"@docusaurus/core": "3.0.0",
"@docusaurus/preset-classic": "3.0.0",
"@docusaurus/core": "3.2.0",
"@docusaurus/preset-classic": "3.2.0",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0",
@ -23,8 +23,8 @@
"react-dom": "^18.0.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.0.0",
"@docusaurus/types": "3.0.0"
"@docusaurus/module-type-aliases": "3.2.0",
"@docusaurus/types": "3.2.0"
},
"browserslist": {
"production": [

View File

@ -28,7 +28,7 @@ tags: [greetings]
Congratulations, you have made your first post!
Feel free to play around and edit this post as much you like.
Feel free to play around and edit this post as much as you like.
```
A new blog post is now available at [http://localhost:3000/blog/greetings](http://localhost:3000/blog/greetings).

View File

@ -1,6 +1,6 @@
{
"name": "@docusaurus/cssnano-preset",
"version": "3.0.0",
"version": "3.2.0",
"description": "Advanced cssnano preset for maximum optimization.",
"main": "lib/index.js",
"license": "MIT",

View File

@ -1,6 +1,6 @@
{
"name": "@docusaurus/logger",
"version": "3.0.0",
"version": "3.2.0",
"description": "An encapsulated logger for semantically formatting console messages.",
"main": "./lib/index.js",
"repository": {

View File

@ -1,6 +1,6 @@
{
"name": "@docusaurus/mdx-loader",
"version": "3.0.0",
"version": "3.2.0",
"description": "Docusaurus Loader for MDX",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@ -18,11 +18,9 @@
},
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.22.7",
"@babel/traverse": "^7.22.8",
"@docusaurus/logger": "3.0.0",
"@docusaurus/utils": "3.0.0",
"@docusaurus/utils-validation": "3.0.0",
"@docusaurus/logger": "3.2.0",
"@docusaurus/utils": "3.2.0",
"@docusaurus/utils-validation": "3.2.0",
"@mdx-js/mdx": "^3.0.0",
"@slorber/remark-comment": "^1.0.0",
"escape-html": "^1.0.3",
@ -46,7 +44,7 @@
"webpack": "^5.88.1"
},
"devDependencies": {
"@docusaurus/types": "3.0.0",
"@docusaurus/types": "3.2.0",
"@types/escape-html": "^1.0.2",
"@types/mdast": "^4.0.2",
"@types/stringify-object": "^3.3.1",

View File

@ -7,7 +7,7 @@
import {mdxLoader} from './loader';
import type {TOCItem as TOCItemImported} from './remark/toc';
import type {TOCItem as TOCItemImported} from './remark/toc/types';
export default mdxLoader;

View File

@ -71,6 +71,21 @@ some **markdown** *content*
expect(result.data.contentTitle).toBeUndefined();
});
it('ignore contentTitle if after thematic break', async () => {
const result = await process(`
Hey
---
# contentTitle 1
some **markdown** *content*
`);
expect(result.data.contentTitle).toBeUndefined();
});
it('is able to decently serialize Markdown syntax', async () => {
const result = await process(`
# some **markdown** \`content\` _italic_

View File

@ -34,17 +34,24 @@ const plugin: Plugin = function plugin(
const {toString} = await import('mdast-util-to-string');
const {visit, EXIT} = await import('unist-util-visit');
visit(root, 'heading', (headingNode: Heading, index, parent) => {
if (headingNode.depth === 1) {
vfile.data.contentTitle = toString(headingNode);
if (removeContentTitle) {
// @ts-expect-error: TODO how to fix?
parent!.children.splice(index, 1);
visit(root, ['heading', 'thematicBreak'], (node, index, parent) => {
if (node.type === 'heading') {
const headingNode = node as Heading;
if (headingNode.depth === 1) {
vfile.data.contentTitle = toString(headingNode);
if (removeContentTitle) {
// @ts-expect-error: TODO how to fix?
parent!.children.splice(index, 1);
}
return EXIT; // We only handle the very first heading
}
// We only handle contentTitle if it's the very first heading found
if (headingNode.depth >= 1) {
return EXIT;
}
return EXIT; // We only handle the very first heading
}
// We only handle contentTitle if it's the very first heading found
if (headingNode.depth >= 1) {
// We only handle contentTitle when it's above the first thematic break
if (node.type === 'thematicBreak') {
return EXIT;
}
return undefined;

View File

@ -0,0 +1,5 @@
import React from 'react';
export default function SomeComponent() {
return <div>Some component</div>;
}

View File

@ -0,0 +1,7 @@
## Partial 1
Partial 1
### Partial 1 Sub Heading
Content

View File

@ -0,0 +1,3 @@
## Partial 2 Nested
Partial 2 Nested

View File

@ -0,0 +1,11 @@
## Partial 2
Partial 2
### Partial 2 Sub Heading
Content
import Partial2Nested from './partial2-nested.md';
<Partial2Nested />

View File

@ -0,0 +1,7 @@
## Partial 3
Partial 3
### Partial 3 Sub Heading
Content

View File

@ -0,0 +1,49 @@
import Partial1 from './_partial1.md';
import SomeComponent from './SomeComponent';
# Index
Some text
import Partial2 from './_partial2.md';
## Index section 1
Foo
<Partial1 />
Some text
<SomeComponent />
## Index section 2
<Partial2 />
## Unused partials
Unused partials (that are only imported but not rendered) shouldn't alter the TOC
import UnusedPartialImport from './_partial3.md';
## NonExisting Partials
Partials that do not exist should alter the TOC
It's not the responsibility of the Remark plugin to check for their existence
import DoesNotExist from './_doesNotExist.md';
<DoesNotExist />
## Duplicate partials
It's fine if we use partials at the end
<Partial1 />
And we can use the partial multiple times!
<Partial1 />

View File

@ -0,0 +1,7 @@
# Partial used before import
While it looks weird to import after usage, this remains valid MDX usage.
<Partial />
import Partial from './_partial.md';

View File

@ -1,238 +1,601 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`toc remark plugin does not overwrite TOC var if no TOC 1`] = `
"foo
\`bar\`
\`\`\`js
baz
\`\`\`
"import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";
export const toc = 1;
function _createMdxContent(props) {
const _components = {
code: "code",
p: "p",
pre: "pre",
...props.components
};
return _jsxs(_Fragment, {
children: [_jsx(_components.p, {
children: "foo"
}), "/n", _jsx(_components.p, {
children: _jsx(_components.code, {
children: "bar"
})
}), "/n", _jsx(_components.pre, {
children: _jsx(_components.code, {
className: "language-js",
children: "baz/n"
})
})]
});
}
export default function MDXContent(props = {}) {
const {wrapper: MDXLayout} = props.components || ({});
return MDXLayout ? _jsx(MDXLayout, {
...props,
children: _jsx(_createMdxContent, {
...props
})
}) : _createMdxContent(props);
}
"
`;
exports[`toc remark plugin escapes inline code 1`] = `
"export const toc = [
{
value: '<code>&lt;Head /&gt;</code>',
id: 'head-',
level: 2
},
{
value: '<code>&lt;Head&gt;Test&lt;/Head&gt;</code>',
id: 'headtesthead',
level: 3
},
{
value: '<code>&lt;div /&gt;</code>',
id: 'div-',
level: 2
},
{
value: '<code>&lt;div&gt; Test &lt;/div&gt;</code>',
id: 'div-test-div',
level: 2
},
{
value: '<code>&lt;div&gt;&lt;i&gt;Test&lt;/i&gt;&lt;/div&gt;</code>',
id: 'divitestidiv',
level: 2
},
{
value: '<code>&lt;div&gt;&lt;i&gt;Test&lt;/i&gt;&lt;/div&gt;</code>',
id: 'divitestidiv-1',
level: 2
}
]
## \`<Head />\`
### \`<Head>Test</Head>\`
## \`<div />\`
## \`<div> Test </div>\`
## \`<div><i>Test</i></div>\`
## [\`<div><i>Test</i></div>\`](/some/link)
"import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";
export const toc = [{
"value": "<code>&lt;Head /&gt;</code>",
"id": "head-",
"level": 2
}, {
"value": "<code>&lt;Head&gt;Test&lt;/Head&gt;</code>",
"id": "headtesthead",
"level": 3
}, {
"value": "<code>&lt;div /&gt;</code>",
"id": "div-",
"level": 2
}, {
"value": "<code>&lt;div&gt; Test &lt;/div&gt;</code>",
"id": "div-test-div",
"level": 2
}, {
"value": "<code>&lt;div&gt;&lt;i&gt;Test&lt;/i&gt;&lt;/div&gt;</code>",
"id": "divitestidiv",
"level": 2
}, {
"value": "<code>&lt;div&gt;&lt;i&gt;Test&lt;/i&gt;&lt;/div&gt;</code>",
"id": "divitestidiv-1",
"level": 2
}];
function _createMdxContent(props) {
const _components = {
a: "a",
code: "code",
h2: "h2",
h3: "h3",
...props.components
};
return _jsxs(_Fragment, {
children: [_jsx(_components.h2, {
id: "head-",
children: _jsx(_components.code, {
children: "<Head />"
})
}), "/n", _jsx(_components.h3, {
id: "headtesthead",
children: _jsx(_components.code, {
children: "<Head>Test</Head>"
})
}), "/n", _jsx(_components.h2, {
id: "div-",
children: _jsx(_components.code, {
children: "<div />"
})
}), "/n", _jsx(_components.h2, {
id: "div-test-div",
children: _jsx(_components.code, {
children: "<div> Test </div>"
})
}), "/n", _jsx(_components.h2, {
id: "divitestidiv",
children: _jsx(_components.code, {
children: "<div><i>Test</i></div>"
})
}), "/n", _jsx(_components.h2, {
id: "divitestidiv-1",
children: _jsx(_components.a, {
href: "/some/link",
children: _jsx(_components.code, {
children: "<div><i>Test</i></div>"
})
})
})]
});
}
export default function MDXContent(props = {}) {
const {wrapper: MDXLayout} = props.components || ({});
return MDXLayout ? _jsx(MDXLayout, {
...props,
children: _jsx(_createMdxContent, {
...props
})
}) : _createMdxContent(props);
}
"
`;
exports[`toc remark plugin exports even with existing name 1`] = `
"export const toc = [
{
value: 'Thanos',
id: 'thanos',
level: 2
},
{
value: 'Tony Stark',
id: 'tony-stark',
level: 2
},
{
value: 'Avengers',
id: 'avengers',
level: 3
}
]
## Thanos
## Tony Stark
### Avengers
"import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";
export const toc = ['replaceMe'];
function _createMdxContent(props) {
const _components = {
h2: "h2",
h3: "h3",
...props.components
};
return _jsxs(_Fragment, {
children: [_jsx(_components.h2, {
id: "thanos",
children: "Thanos"
}), "/n", _jsx(_components.h2, {
id: "tony-stark",
children: "Tony Stark"
}), "/n", _jsx(_components.h3, {
id: "avengers",
children: "Avengers"
})]
});
}
export default function MDXContent(props = {}) {
const {wrapper: MDXLayout} = props.components || ({});
return MDXLayout ? _jsx(MDXLayout, {
...props,
children: _jsx(_createMdxContent, {
...props
})
}) : _createMdxContent(props);
}
"
`;
exports[`toc remark plugin handles empty headings 1`] = `
"export const toc = []
# Ignore this
##
## ![](an-image.svg)
"import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";
export const toc = [];
function _createMdxContent(props) {
const _components = {
h1: "h1",
h2: "h2",
img: "img",
...props.components
};
return _jsxs(_Fragment, {
children: [_jsx(_components.h1, {
id: "ignore-this",
children: "Ignore this"
}), "/n", _jsx(_components.h2, {
id: ""
}), "/n", _jsx(_components.h2, {
id: "-1",
children: _jsx(_components.img, {
src: "an-image.svg",
alt: ""
})
})]
});
}
export default function MDXContent(props = {}) {
const {wrapper: MDXLayout} = props.components || ({});
return MDXLayout ? _jsx(MDXLayout, {
...props,
children: _jsx(_createMdxContent, {
...props
})
}) : _createMdxContent(props);
}
"
`;
exports[`toc remark plugin inserts below imports 1`] = `
"import something from 'something';
"import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";
import something from 'something';
import somethingElse from 'something-else';
export const toc = [
{
value: 'Title',
id: 'title',
level: 2
},
{
value: 'Test',
id: 'test',
level: 2
},
{
value: 'Again',
id: 'again',
level: 3
}
]
## Title
## Test
### Again
Content.
export const toc = [{
"value": "Title",
"id": "title",
"level": 2
}, {
"value": "Test",
"id": "test",
"level": 2
}, {
"value": "Again",
"id": "again",
"level": 3
}];
function _createMdxContent(props) {
const _components = {
h2: "h2",
h3: "h3",
p: "p",
...props.components
};
return _jsxs(_Fragment, {
children: [_jsx(_components.h2, {
id: "title",
children: "Title"
}), "/n", _jsx(_components.h2, {
id: "test",
children: "Test"
}), "/n", _jsx(_components.h3, {
id: "again",
children: "Again"
}), "/n", _jsx(_components.p, {
children: "Content."
})]
});
}
export default function MDXContent(props = {}) {
const {wrapper: MDXLayout} = props.components || ({});
return MDXLayout ? _jsx(MDXLayout, {
...props,
children: _jsx(_createMdxContent, {
...props
})
}) : _createMdxContent(props);
}
"
`;
exports[`toc remark plugin outputs empty array for no TOC 1`] = `
"export const toc = []
foo
\`bar\`
\`\`\`js
baz
\`\`\`
"import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";
export const toc = [];
function _createMdxContent(props) {
const _components = {
code: "code",
p: "p",
pre: "pre",
...props.components
};
return _jsxs(_Fragment, {
children: [_jsx(_components.p, {
children: "foo"
}), "/n", _jsx(_components.p, {
children: _jsx(_components.code, {
children: "bar"
})
}), "/n", _jsx(_components.pre, {
children: _jsx(_components.code, {
className: "language-js",
children: "baz/n"
})
})]
});
}
export default function MDXContent(props = {}) {
const {wrapper: MDXLayout} = props.components || ({});
return MDXLayout ? _jsx(MDXLayout, {
...props,
children: _jsx(_createMdxContent, {
...props
})
}) : _createMdxContent(props);
}
"
`;
exports[`toc remark plugin works on non text phrasing content 1`] = `
"export const toc = [
{
value: '<em>Emphasis</em>',
id: 'emphasis',
level: 2
},
{
value: '<strong>Importance</strong>',
id: 'importance',
level: 3
},
{
value: '<del>Strikethrough</del>',
id: 'strikethrough',
level: 2
},
{
value: '<i>HTML</i>',
id: 'html',
level: 2
},
{
value: '<code>inline.code()</code>',
id: 'inlinecode',
level: 2
},
{
value: 'some <span class="some-class">styled</span> <strong>heading</strong> <span class="myClassName &lt;&gt; weird char"></span> test',
id: 'some-styled-heading--test',
level: 2
}
]
## *Emphasis*
### **Importance**
## ~~Strikethrough~~
## <i>HTML</i>
## \`inline.code()\`
## some <span className="some-class" style={{border: "solid"}}>styled</span> <strong>heading</strong> <span class="myClass" className="myClassName <> weird char" data-random-attr="456" /> test
"import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";
export const toc = [{
"value": "<em>Emphasis</em>",
"id": "emphasis",
"level": 2
}, {
"value": "<strong>Importance</strong>",
"id": "importance",
"level": 3
}, {
"value": "<del>Strikethrough</del>",
"id": "strikethrough",
"level": 2
}, {
"value": "<i>HTML</i>",
"id": "html",
"level": 2
}, {
"value": "<code>inline.code()</code>",
"id": "inlinecode",
"level": 2
}, {
"value": "some <span class=\\"some-class\\">styled</span> <strong>heading</strong> <span class=\\"myClassName &lt;&gt; weird char\\"></span> test",
"id": "some-styled-heading--test",
"level": 2
}];
function _createMdxContent(props) {
const _components = {
code: "code",
del: "del",
em: "em",
h2: "h2",
h3: "h3",
strong: "strong",
...props.components
};
return _jsxs(_Fragment, {
children: [_jsx(_components.h2, {
id: "emphasis",
children: _jsx(_components.em, {
children: "Emphasis"
})
}), "/n", _jsx(_components.h3, {
id: "importance",
children: _jsx(_components.strong, {
children: "Importance"
})
}), "/n", _jsx(_components.h2, {
id: "strikethrough",
children: _jsx(_components.del, {
children: "Strikethrough"
})
}), "/n", _jsx(_components.h2, {
id: "html",
children: _jsx("i", {
children: "HTML"
})
}), "/n", _jsx(_components.h2, {
id: "inlinecode",
children: _jsx(_components.code, {
children: "inline.code()"
})
}), "/n", _jsxs(_components.h2, {
id: "some-styled-heading--test",
children: ["some ", _jsx("span", {
className: "some-class",
style: {
border: "solid"
},
children: "styled"
}), " ", _jsx("strong", {
children: "heading"
}), " ", _jsx("span", {
class: "myClass",
className: "myClassName <> weird char",
"data-random-attr": "456"
}), " test"]
})]
});
}
export default function MDXContent(props = {}) {
const {wrapper: MDXLayout} = props.components || ({});
return MDXLayout ? _jsx(MDXLayout, {
...props,
children: _jsx(_createMdxContent, {
...props
})
}) : _createMdxContent(props);
}
"
`;
exports[`toc remark plugin works on text content 1`] = `
"export const toc = [
{
value: 'Endi',
id: 'endi',
level: 3
},
{
value: 'Endi',
id: 'endi-1',
level: 2
},
{
value: 'Yangshun',
id: 'yangshun',
level: 3
},
{
value: 'I ♥ unicode.',
id: 'i--unicode',
level: 2
}
]
### Endi
\`\`\`md
## This is ignored
\`\`\`
## Endi
Lorem ipsum
### Yangshun
Some content here
## I ♥ unicode.
"import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";
export const c = 1;
export const toc = [{
"value": "Endi",
"id": "endi",
"level": 3
}, {
"value": "Endi",
"id": "endi-1",
"level": 2
}, {
"value": "Yangshun",
"id": "yangshun",
"level": 3
}, {
"value": "I ♥ unicode.",
"id": "i--unicode",
"level": 2
}];
function _createMdxContent(props) {
const _components = {
code: "code",
h2: "h2",
h3: "h3",
p: "p",
pre: "pre",
...props.components
};
return _jsxs(_Fragment, {
children: [_jsx(_components.h3, {
id: "endi",
children: "Endi"
}), "/n", _jsx(_components.pre, {
children: _jsx(_components.code, {
className: "language-md",
children: "## This is ignored/n"
})
}), "/n", _jsx(_components.h2, {
id: "endi-1",
children: "Endi"
}), "/n", _jsx(_components.p, {
children: "Lorem ipsum"
}), "/n", _jsx(_components.h3, {
id: "yangshun",
children: "Yangshun"
}), "/n", _jsx(_components.p, {
children: "Some content here"
}), "/n", _jsx(_components.h2, {
id: "i--unicode",
children: "I ♥ unicode."
})]
});
}
export default function MDXContent(props = {}) {
const {wrapper: MDXLayout} = props.components || ({});
return MDXLayout ? _jsx(MDXLayout, {
...props,
children: _jsx(_createMdxContent, {
...props
})
}) : _createMdxContent(props);
}
"
`;
exports[`toc remark plugin works with imported markdown 1`] = `
"import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";
import Partial1, {toc as __tocPartial1} from './_partial1.md';
import SomeComponent from './SomeComponent';
import Partial2, {toc as __tocPartial2} from './_partial2.md';
import UnusedPartialImport from './_partial3.md';
import DoesNotExist, {toc as __tocDoesNotExist} from './_doesNotExist.md';
export const toc = [{
"value": "Index section 1",
"id": "index-section-1",
"level": 2
}, ...__tocPartial1, {
"value": "Index section 2",
"id": "index-section-2",
"level": 2
}, ...__tocPartial2, {
"value": "Unused partials",
"id": "unused-partials",
"level": 2
}, {
"value": "NonExisting Partials",
"id": "nonexisting-partials",
"level": 2
}, ...__tocDoesNotExist, {
"value": "Duplicate partials",
"id": "duplicate-partials",
"level": 2
}, ...__tocPartial1, ...__tocPartial1];
function _createMdxContent(props) {
const _components = {
h1: "h1",
h2: "h2",
p: "p",
...props.components
};
return _jsxs(_Fragment, {
children: [_jsx(_components.h1, {
id: "index",
children: "Index"
}), "/n", _jsx(_components.p, {
children: "Some text"
}), "/n", "/n", _jsx(_components.h2, {
id: "index-section-1",
children: "Index section 1"
}), "/n", _jsx(_components.p, {
children: "Foo"
}), "/n", _jsx(Partial1, {}), "/n", _jsx(_components.p, {
children: "Some text"
}), "/n", _jsx(SomeComponent, {}), "/n", _jsx(_components.h2, {
id: "index-section-2",
children: "Index section 2"
}), "/n", _jsx(Partial2, {}), "/n", _jsx(_components.h2, {
id: "unused-partials",
children: "Unused partials"
}), "/n", _jsx(_components.p, {
children: "Unused partials (that are only imported but not rendered) shouldn't alter the TOC"
}), "/n", "/n", _jsx(_components.h2, {
id: "nonexisting-partials",
children: "NonExisting Partials"
}), "/n", _jsx(_components.p, {
children: "Partials that do not exist should alter the TOC"
}), "/n", _jsx(_components.p, {
children: "It's not the responsibility of the Remark plugin to check for their existence"
}), "/n", "/n", _jsx(DoesNotExist, {}), "/n", _jsx(_components.h2, {
id: "duplicate-partials",
children: "Duplicate partials"
}), "/n", _jsx(_components.p, {
children: "It's fine if we use partials at the end"
}), "/n", _jsx(Partial1, {}), "/n", _jsx(_components.p, {
children: "And we can use the partial multiple times!"
}), "/n", _jsx(Partial1, {})]
});
}
export default function MDXContent(props = {}) {
const {wrapper: MDXLayout} = props.components || ({});
return MDXLayout ? _jsx(MDXLayout, {
...props,
children: _jsx(_createMdxContent, {
...props
})
}) : _createMdxContent(props);
}
"
`;
exports[`toc remark plugin works with partial imported after its usage 1`] = `
"import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";
import Partial, {toc as __tocPartial} from './_partial.md';
export const toc = [...__tocPartial];
function _createMdxContent(props) {
const _components = {
h1: "h1",
p: "p",
...props.components
};
return _jsxs(_Fragment, {
children: [_jsx(_components.h1, {
id: "partial-used-before-import",
children: "Partial used before import"
}), "/n", _jsx(_components.p, {
children: "While it looks weird to import after usage, this remains valid MDX usage."
}), "/n", _jsx(Partial, {})]
});
}
export default function MDXContent(props = {}) {
const {wrapper: MDXLayout} = props.components || ({});
return MDXLayout ? _jsx(MDXLayout, {
...props,
children: _jsx(_createMdxContent, {
...props
})
}) : _createMdxContent(props);
}
"
`;
exports[`toc remark plugin works with partials importing other partials 1`] = `
"import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";
import Partial2Nested, {toc as __tocPartial2Nested} from './partial2-nested.md';
export const toc = [{
"value": "Partial 2",
"id": "partial-2",
"level": 2
}, {
"value": "Partial 2 Sub Heading",
"id": "partial-2-sub-heading",
"level": 3
}, ...__tocPartial2Nested];
function _createMdxContent(props) {
const _components = {
h2: "h2",
h3: "h3",
p: "p",
...props.components
};
return _jsxs(_Fragment, {
children: [_jsx(_components.h2, {
id: "partial-2",
children: "Partial 2"
}), "/n", _jsx(_components.p, {
children: "Partial 2"
}), "/n", _jsx(_components.h3, {
id: "partial-2-sub-heading",
children: "Partial 2 Sub Heading"
}), "/n", _jsx(_components.p, {
children: "Content"
}), "/n", "/n", _jsx(Partial2Nested, {})]
});
}
export default function MDXContent(props = {}) {
const {wrapper: MDXLayout} = props.components || ({});
return MDXLayout ? _jsx(MDXLayout, {
...props,
children: _jsx(_createMdxContent, {
...props
})
}) : _createMdxContent(props);
}
"
`;

View File

@ -11,18 +11,23 @@ import plugin from '../index';
import headings from '../../headings/index';
const processFixture = async (name: string) => {
const {remark} = await import('remark');
const {default: gfm} = await import('remark-gfm');
const {default: mdx} = await import('remark-mdx');
const filePath = path.join(__dirname, '__fixtures__', `${name}.md`);
const {compile} = await import('@mdx-js/mdx');
const filePath = path.join(
__dirname,
'__fixtures__',
name.endsWith('.mdx') ? name : `${name}.md`,
);
const file = await vfile.read(filePath);
const result = await remark()
.use(headings)
.use(gfm)
.use(mdx)
.use(plugin)
.process(file);
const result = await compile(file, {
format: 'mdx',
remarkPlugins: [headings, gfm, plugin],
rehypePlugins: [],
});
return result.value;
};
@ -70,4 +75,21 @@ describe('toc remark plugin', () => {
const result = await processFixture('empty-headings');
expect(result).toMatchSnapshot();
});
it('works with imported markdown', async () => {
const result = await processFixture('partials/index.mdx');
expect(result).toMatchSnapshot();
});
it('works with partials importing other partials', async () => {
const result = await processFixture('partials/_partial2.mdx');
expect(result).toMatchSnapshot();
});
it('works with partial imported after its usage', async () => {
const result = await processFixture(
'partials/partial-used-before-import.mdx',
);
expect(result).toMatchSnapshot();
});
});

View File

@ -5,154 +5,183 @@
* LICENSE file in the root directory of this source tree.
*/
import {parse, type ParserOptions} from '@babel/parser';
import traverse from '@babel/traverse';
import stringifyObject from 'stringify-object';
import {toValue} from '../utils';
import type {Identifier} from '@babel/types';
import type {Node, Parent} from 'unist';
import type {Heading, Literal} from 'mdast';
import {
addTocSliceImportIfNeeded,
createTOCExportNodeAST,
findDefaultImportName,
getImportDeclarations,
isMarkdownImport,
isNamedExport,
} from './utils';
import type {Heading, Root} from 'mdast';
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
import type {Transformer} from 'unified';
import type {
MdxjsEsm,
MdxJsxFlowElement,
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
} from 'mdast-util-mdx';
// TODO as of April 2023, no way to import/re-export this ESM type easily :/
// TODO upgrade to TS 5.3
// See https://github.com/microsoft/TypeScript/issues/49721#issuecomment-1517839391
// import type {Plugin} from 'unified';
type Plugin = any; // TODO fix this asap
export type TOCItem = {
readonly value: string;
readonly id: string;
readonly level: number;
};
const parseOptions: ParserOptions = {
plugins: ['jsx'],
sourceType: 'module',
};
const isImport = (child: any): child is Literal =>
child.type === 'mdxjsEsm' && child.value.startsWith('import');
const hasImports = (index: number) => index > -1;
const isExport = (child: any): child is Literal =>
child.type === 'mdxjsEsm' && child.value.startsWith('export');
import type {TOCItems} from './types';
import type {ImportDeclaration} from 'estree';
interface PluginOptions {
name?: string;
}
const isTarget = (child: Literal, name: string) => {
let found = false;
const ast = parse(child.value, parseOptions);
traverse(ast, {
VariableDeclarator: (path) => {
if ((path.node.id as Identifier).name === name) {
found = true;
}
},
});
return found;
};
// ComponentName (default export) => ImportDeclaration mapping
type MarkdownImports = Map<string, {declaration: ImportDeclaration}>;
const getOrCreateExistingTargetIndex = async (
children: Node[],
name: string,
) => {
let importsIndex = -1;
let targetIndex = -1;
// MdxjsEsm node representing an already existing "export const toc" declaration
type ExistingTOCExport = MdxjsEsm | null;
children.forEach((child, index) => {
if (isImport(child)) {
importsIndex = index;
} else if (isExport(child) && isTarget(child, name)) {
targetIndex = index;
function createTocSliceImportName({
tocExportName,
componentName,
}: {
tocExportName: string;
componentName: string;
}) {
// The name of the toc slice import alias doesn't matter much
// We just need to ensure it's valid and won't conflict with other names
return `__${tocExportName}${componentName}`;
}
async function collectImportsExports({
root,
tocExportName,
}: {
root: Root;
tocExportName: string;
}): Promise<{
markdownImports: MarkdownImports;
existingTocExport: ExistingTOCExport;
}> {
const {visit} = await import('unist-util-visit');
const markdownImports = new Map<string, {declaration: ImportDeclaration}>();
let existingTocExport: MdxjsEsm | null = null;
visit(root, 'mdxjsEsm', (node) => {
if (!node.data?.estree) {
return;
}
if (isNamedExport(node, tocExportName)) {
existingTocExport = node;
}
});
if (targetIndex === -1) {
const target = await createExportNode(name, []);
targetIndex = hasImports(importsIndex) ? importsIndex + 1 : 0;
children.splice(targetIndex, 0, target);
}
return targetIndex;
};
const plugin: Plugin = function plugin(
options: PluginOptions = {},
): Transformer {
const name = options.name || 'toc';
return async (root) => {
const {toString} = await import('mdast-util-to-string');
const {visit} = await import('unist-util-visit');
const headings: TOCItem[] = [];
visit(root, 'heading', (child: Heading) => {
const value = toString(child);
// depth:1 headings are titles and not included in the TOC
if (!value || child.depth < 2) {
getImportDeclarations(node.data.estree).forEach((declaration) => {
if (!isMarkdownImport(declaration)) {
return;
}
headings.push({
value: toValue(child, toString),
id: child.data!.id!,
level: child.depth,
const componentName = findDefaultImportName(declaration);
if (!componentName) {
return;
}
markdownImports.set(componentName, {
declaration,
});
});
});
const {children} = root as Parent;
const targetIndex = await getOrCreateExistingTargetIndex(children, name);
return {markdownImports, existingTocExport};
}
if (headings?.length) {
children[targetIndex] = await createExportNode(name, headings);
async function collectTOCItems({
root,
tocExportName,
markdownImports,
}: {
root: Root;
tocExportName: string;
markdownImports: MarkdownImports;
}): Promise<{
// The toc items we collected in the tree
tocItems: TOCItems;
}> {
const {toString} = await import('mdast-util-to-string');
const {visit} = await import('unist-util-visit');
const tocItems: TOCItems = [];
visit(root, (child) => {
if (child.type === 'heading') {
visitHeading(child);
} else if (child.type === 'mdxJsxFlowElement') {
visitJSXElement(child);
}
};
};
});
export default plugin;
return {tocItems};
async function createExportNode(name: string, object: any): Promise<MdxjsEsm> {
const {valueToEstree} = await import('estree-util-value-to-estree');
// Visit Markdown headings
function visitHeading(node: Heading) {
const value = toString(node);
// depth:1 headings are titles and not included in the TOC
if (!value || node.depth < 2) {
return;
}
tocItems.push({
type: 'heading',
heading: node,
});
}
return {
type: 'mdxjsEsm',
value: `export const ${name} = ${stringifyObject(object)}`,
data: {
estree: {
type: 'Program',
body: [
{
type: 'ExportNamedDeclaration',
declaration: {
type: 'VariableDeclaration',
declarations: [
{
type: 'VariableDeclarator',
id: {
type: 'Identifier',
name,
},
init: valueToEstree(object),
},
],
kind: 'const',
},
specifiers: [],
source: null,
},
],
sourceType: 'module',
},
},
// Visit JSX elements, such as <Partial/>
function visitJSXElement(node: MdxJsxFlowElement) {
const componentName = node.name;
if (!componentName) {
return;
}
const importDeclaration = markdownImports.get(componentName)?.declaration;
if (!importDeclaration) {
return;
}
const tocSliceImportName = createTocSliceImportName({
tocExportName,
componentName,
});
tocItems.push({
type: 'slice',
importName: tocSliceImportName,
});
addTocSliceImportIfNeeded({
importDeclaration,
tocExportName,
tocSliceImportName,
});
}
}
export default function plugin(options: PluginOptions = {}): Transformer<Root> {
const tocExportName = options.name || 'toc';
return async (root) => {
const {markdownImports, existingTocExport} = await collectImportsExports({
root,
tocExportName,
});
// If user explicitly writes "export const toc" in his mdx file
// We keep it as is do not override their explicit toc structure
// See https://github.com/facebook/docusaurus/pull/7530#discussion_r1458087876
if (existingTocExport) {
return;
}
const {tocItems} = await collectTOCItems({
root,
tocExportName,
markdownImports,
});
root.children.push(
await createTOCExportNodeAST({
tocExportName,
tocItems,
}),
);
};
}

View File

@ -0,0 +1,29 @@
/**
* 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 type {Heading} from 'mdast';
// Note: this type is exported from mdx-loader and used in theme
// Need to keep it retro compatible
export type TOCItem = {
readonly value: string;
readonly id: string;
readonly level: number;
};
export type TOCHeading = {
readonly type: 'heading';
readonly heading: Heading;
};
// A TOC slice represents a TOCItem[] imported from a partial
export type TOCSlice = {
readonly type: 'slice';
readonly importName: string;
};
export type TOCItems = (TOCHeading | TOCSlice)[];

View File

@ -0,0 +1,177 @@
/**
* 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 {toValue} from '../utils';
import type {Node} from 'unist';
import type {
MdxjsEsm,
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
} from 'mdast-util-mdx';
import type {TOCHeading, TOCItem, TOCItems, TOCSlice} from './types';
import type {
Program,
SpreadElement,
ImportDeclaration,
ImportSpecifier,
} from 'estree';
export function getImportDeclarations(program: Program): ImportDeclaration[] {
return program.body.filter(
(item): item is ImportDeclaration => item.type === 'ImportDeclaration',
);
}
export function isMarkdownImport(node: Node): node is ImportDeclaration {
if (node.type !== 'ImportDeclaration') {
return false;
}
const importPath = (node as ImportDeclaration).source.value;
return typeof importPath === 'string' && /\.mdx?$/.test(importPath);
}
export function findDefaultImportName(
importDeclaration: ImportDeclaration,
): string | undefined {
return importDeclaration.specifiers.find(
(o: Node) => o.type === 'ImportDefaultSpecifier',
)?.local.name;
}
export function findNamedImportSpecifier(
importDeclaration: ImportDeclaration,
localName: string,
): ImportSpecifier | undefined {
return importDeclaration?.specifiers.find(
(specifier): specifier is ImportSpecifier =>
specifier.type === 'ImportSpecifier' &&
specifier.local.name === localName,
);
}
// Before: import Partial from "partial"
// After: import Partial, {toc as __tocPartial} from "partial"
export function addTocSliceImportIfNeeded({
importDeclaration,
tocExportName,
tocSliceImportName,
}: {
importDeclaration: ImportDeclaration;
tocExportName: string;
tocSliceImportName: string;
}): void {
// We only add the toc slice named import if it doesn't exist already
if (!findNamedImportSpecifier(importDeclaration, tocSliceImportName)) {
importDeclaration.specifiers.push({
type: 'ImportSpecifier',
imported: {type: 'Identifier', name: tocExportName},
local: {type: 'Identifier', name: tocSliceImportName},
});
}
}
export function isNamedExport(
node: Node,
exportName: string,
): node is MdxjsEsm {
if (node.type !== 'mdxjsEsm') {
return false;
}
const program = (node as MdxjsEsm).data?.estree;
if (!program) {
return false;
}
if (program.body.length !== 1) {
return false;
}
const exportDeclaration = program.body[0]!;
if (exportDeclaration.type !== 'ExportNamedDeclaration') {
return false;
}
const variableDeclaration = exportDeclaration.declaration;
if (variableDeclaration?.type !== 'VariableDeclaration') {
return false;
}
const {id} = variableDeclaration.declarations[0]!;
if (id.type !== 'Identifier') {
return false;
}
return id.name === exportName;
}
export async function createTOCExportNodeAST({
tocExportName,
tocItems,
}: {
tocExportName: string;
tocItems: TOCItems;
}): Promise<MdxjsEsm> {
function createTOCSliceAST(tocSlice: TOCSlice): SpreadElement {
return {
type: 'SpreadElement',
argument: {type: 'Identifier', name: tocSlice.importName},
};
}
async function createTOCHeadingAST({heading}: TOCHeading) {
const {toString} = await import('mdast-util-to-string');
const {valueToEstree} = await import('estree-util-value-to-estree');
const value: TOCItem = {
value: toValue(heading, toString),
id: heading.data!.id!,
level: heading.depth,
};
return valueToEstree(value);
}
async function createTOCItemAST(tocItem: TOCItems[number]) {
switch (tocItem.type) {
case 'slice':
return createTOCSliceAST(tocItem);
case 'heading':
return createTOCHeadingAST(tocItem);
default: {
throw new Error(`unexpected toc item type`);
}
}
}
return {
type: 'mdxjsEsm',
value: '', // See https://github.com/facebook/docusaurus/pull/9684#discussion_r1457595181
data: {
estree: {
type: 'Program',
body: [
{
type: 'ExportNamedDeclaration',
declaration: {
type: 'VariableDeclaration',
declarations: [
{
type: 'VariableDeclarator',
id: {
type: 'Identifier',
name: tocExportName,
},
init: {
type: 'ArrayExpression',
elements: await Promise.all(tocItems.map(createTOCItemAST)),
},
},
],
kind: 'const',
},
specifiers: [],
source: null,
},
],
sourceType: 'module',
},
},
};
}

View File

@ -1,6 +1,6 @@
{
"name": "@docusaurus/module-type-aliases",
"version": "3.0.0",
"version": "3.2.0",
"description": "Docusaurus module type aliases.",
"types": "./src/index.d.ts",
"publishConfig": {
@ -13,7 +13,7 @@
},
"dependencies": {
"@docusaurus/react-loadable": "5.5.2",
"@docusaurus/types": "3.0.0",
"@docusaurus/types": "3.2.0",
"@types/history": "^4.7.11",
"@types/react": "*",
"@types/react-router-config": "*",

View File

@ -262,8 +262,8 @@ declare module '@docusaurus/useRouteContext' {
declare module '@docusaurus/useBrokenLinks' {
export type BrokenLinks = {
collectLink: (link: string) => void;
collectAnchor: (anchor: string) => void;
collectLink: (link: string | undefined) => void;
collectAnchor: (anchor: string | undefined) => void;
};
export default function useBrokenLinks(): BrokenLinks;

View File

@ -1,6 +1,6 @@
{
"name": "@docusaurus/plugin-client-redirects",
"version": "3.0.0",
"version": "3.2.0",
"description": "Client redirects plugin for Docusaurus.",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@ -18,18 +18,18 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.0.0",
"@docusaurus/logger": "3.0.0",
"@docusaurus/utils": "3.0.0",
"@docusaurus/utils-common": "3.0.0",
"@docusaurus/utils-validation": "3.0.0",
"@docusaurus/core": "3.2.0",
"@docusaurus/logger": "3.2.0",
"@docusaurus/utils": "3.2.0",
"@docusaurus/utils-common": "3.2.0",
"@docusaurus/utils-validation": "3.2.0",
"eta": "^2.2.0",
"fs-extra": "^11.1.1",
"lodash": "^4.17.21",
"tslib": "^2.6.0"
},
"devDependencies": {
"@docusaurus/types": "3.0.0"
"@docusaurus/types": "3.2.0"
},
"peerDependencies": {
"react": "^18.0.0",

View File

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {removeTrailingSlash} from '@docusaurus/utils';
import {removeTrailingSlash} from '@docusaurus/utils-common';
import {normalizePluginOptions} from '@docusaurus/utils-validation';
import collectRedirects from '../collectRedirects';
import {validateOptions} from '../options';

View File

@ -7,8 +7,11 @@
import _ from 'lodash';
import logger from '@docusaurus/logger';
import {addTrailingSlash, removeTrailingSlash} from '@docusaurus/utils';
import {applyTrailingSlash} from '@docusaurus/utils-common';
import {
applyTrailingSlash,
addTrailingSlash,
removeTrailingSlash,
} from '@docusaurus/utils-common';
import {
createFromExtensionsRedirects,
createToExtensionsRedirects,

View File

@ -9,7 +9,7 @@ import {
addTrailingSlash,
removeSuffix,
removeTrailingSlash,
} from '@docusaurus/utils';
} from '@docusaurus/utils-common';
import type {RedirectItem} from './types';
const ExtensionAdditionalMessage =

View File

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {removePrefix, addLeadingSlash} from '@docusaurus/utils';
import {addLeadingSlash, removePrefix} from '@docusaurus/utils-common';
import collectRedirects from './collectRedirects';
import writeRedirectFiles, {
toRedirectFiles,

View File

@ -1,12 +1,24 @@
{
"name": "@docusaurus/plugin-content-blog",
"version": "3.0.0",
"version": "3.2.0",
"description": "Blog plugin for Docusaurus.",
"main": "lib/index.js",
"types": "src/plugin-content-blog.d.ts",
"exports": {
"./lib/*": "./lib/*",
"./src/*": "./src/*",
"./client": {
"type": "./lib/client/index.d.ts",
"default": "./lib/client/index.js"
},
".": {
"types": "./src/plugin-content-blog.d.ts",
"default": "./lib/index.js"
}
},
"scripts": {
"build": "tsc",
"watch": "tsc --watch",
"build": "tsc --build",
"watch": "tsc --build --watch",
"test:generate-build-snap": "yarn docusaurus build src/__tests__/__fixtures__/website --out-dir build-snap && yarn rimraf src/__tests__/__fixtures__/website/.docusaurus && yarn rimraf src/__tests__/__fixtures__/website/build-snap/assets && git add src/__tests__/__fixtures__/website/build-snap"
},
"repository": {
@ -19,13 +31,13 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.0.0",
"@docusaurus/logger": "3.0.0",
"@docusaurus/mdx-loader": "3.0.0",
"@docusaurus/types": "3.0.0",
"@docusaurus/utils": "3.0.0",
"@docusaurus/utils-common": "3.0.0",
"@docusaurus/utils-validation": "3.0.0",
"@docusaurus/core": "3.2.0",
"@docusaurus/logger": "3.2.0",
"@docusaurus/mdx-loader": "3.2.0",
"@docusaurus/types": "3.2.0",
"@docusaurus/utils": "3.2.0",
"@docusaurus/utils-common": "3.2.0",
"@docusaurus/utils-validation": "3.2.0",
"cheerio": "^1.0.0-rc.12",
"feed": "^4.2.2",
"fs-extra": "^11.1.1",
@ -43,5 +55,8 @@
},
"engines": {
"node": ">=18.0"
},
"devDependencies": {
"@total-typescript/shoehorn": "^0.1.2"
}
}

View File

@ -0,0 +1,9 @@
---
title: Author
slug: author
author: ozaki
last_update:
author: seb
---
author

View File

@ -0,0 +1,11 @@
---
title: Both
slug: both
date: 2020-01-01
last_update:
date: 2021-01-01
author: seb
author: ozaki
---
last update date

View File

@ -0,0 +1,9 @@
---
title: Last update date
slug: lastUpdateDate
date: 2020-01-01
last_update:
date: 2021-01-01
---
last update date

View File

@ -0,0 +1,6 @@
---
title: Nothing
slug: nothing
---
nothing

View File

@ -23,7 +23,32 @@ title: This post links to another one!
[Linked post](/blog/2018/12/14/Happy-First-Birthday-Slash)"
`;
exports[`paginateBlogPosts generates right pages 1`] = `
exports[`paginateBlogPosts generates a single page 1`] = `
[
{
"items": [
"post1",
"post2",
"post3",
"post4",
"post5",
],
"metadata": {
"blogDescription": "Blog Description",
"blogTitle": "Blog Title",
"nextPage": undefined,
"page": 1,
"permalink": "/",
"postsPerPage": 10,
"previousPage": undefined,
"totalCount": 5,
"totalPages": 1,
},
},
]
`;
exports[`paginateBlogPosts generates pages 1`] = `
[
{
"items": [
@ -78,7 +103,7 @@ exports[`paginateBlogPosts generates right pages 1`] = `
]
`;
exports[`paginateBlogPosts generates right pages 2`] = `
exports[`paginateBlogPosts generates pages at blog root 1`] = `
[
{
"items": [
@ -133,26 +158,56 @@ exports[`paginateBlogPosts generates right pages 2`] = `
]
`;
exports[`paginateBlogPosts generates right pages 3`] = `
exports[`paginateBlogPosts generates pages with custom pageBasePath 1`] = `
[
{
"items": [
"post1",
"post2",
],
"metadata": {
"blogDescription": "Blog Description",
"blogTitle": "Blog Title",
"nextPage": "/blog/customPageBasePath/2",
"page": 1,
"permalink": "/blog",
"postsPerPage": 2,
"previousPage": undefined,
"totalCount": 5,
"totalPages": 3,
},
},
{
"items": [
"post3",
"post4",
],
"metadata": {
"blogDescription": "Blog Description",
"blogTitle": "Blog Title",
"nextPage": "/blog/customPageBasePath/3",
"page": 2,
"permalink": "/blog/customPageBasePath/2",
"postsPerPage": 2,
"previousPage": "/blog",
"totalCount": 5,
"totalPages": 3,
},
},
{
"items": [
"post5",
],
"metadata": {
"blogDescription": "Blog Description",
"blogTitle": "Blog Title",
"nextPage": undefined,
"page": 1,
"permalink": "/",
"postsPerPage": 10,
"previousPage": undefined,
"page": 3,
"permalink": "/blog/customPageBasePath/3",
"postsPerPage": 2,
"previousPage": "/blog/customPageBasePath/2",
"totalCount": 5,
"totalPages": 1,
"totalPages": 3,
},
},
]

View File

@ -220,6 +220,134 @@ exports[`atom has feed item for each post 1`] = `
]
`;
exports[`atom has feed item for each post - with trailing slash 1`] = `
[
"<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<id>https://docusaurus.io/myBaseUrl/blog/</id>
<title>Hello Blog</title>
<updated>2023-07-23T00:00:00.000Z</updated>
<generator>https://github.com/jpmonette/feed</generator>
<link rel="alternate" href="https://docusaurus.io/myBaseUrl/blog/"/>
<subtitle>Hello Blog</subtitle>
<icon>https://docusaurus.io/myBaseUrl/image/favicon.ico</icon>
<rights>Copyright</rights>
<entry>
<title type="html"><![CDATA[test links]]></title>
<id>https://docusaurus.io/myBaseUrl/blog/blog-with-links/</id>
<link href="https://docusaurus.io/myBaseUrl/blog/blog-with-links/"/>
<updated>2023-07-23T00:00:00.000Z</updated>
<summary type="html"><![CDATA[absolute full url]]></summary>
<content type="html"><![CDATA[<p><a href="https://github.com/facebook/docusaurus" target="_blank" rel="noopener noreferrer">absolute full url</a></p>
<p><a href="https://docusaurus.io/blog/heading-as-title">absolute pathname</a></p>
<p><a href="https://docusaurus.io/blog/heading-as-title">relative pathname</a></p>
<p><a href="https://docusaurus.io/blog/heading-as-title">md link</a></p>
<p><a href="https://docusaurus.io/myBaseUrl/blog/blog-with-links/#title">anchor</a></p>
<p><a href="https://docusaurus.io/blog/heading-as-title#title">relative pathname + anchor</a></p>
<p><img loading="lazy" src="https://docusaurus.io/assets/images/test-image-742d39e51f41482e8132e79c09ad4eea.png" width="760" height="160" class="img_yGFe"></p>
<p><img loading="lazy" src="https://docusaurus.io/assets/images/slash-introducing-411a16dd05086935b8e9ddae38ae9b45.svg" alt="" class="img_yGFe"></p>
<img srcset="https://docusaurus.io/img/test-image.png 300w, https://docusaurus.io/img/docusaurus-social-card.png 500w">
<img src="https://docusaurus.io/img/test-image.png">]]></content>
</entry>
<entry>
<title type="html"><![CDATA[MDX Blog Sample with require calls]]></title>
<id>https://docusaurus.io/myBaseUrl/blog/mdx-require-blog-post/</id>
<link href="https://docusaurus.io/myBaseUrl/blog/mdx-require-blog-post/"/>
<updated>2021-03-06T00:00:00.000Z</updated>
<summary type="html"><![CDATA[Test MDX with require calls]]></summary>
<content type="html"><![CDATA[<p>Test MDX with require calls</p>
<!-- -->
<!-- -->
<img src="https://docusaurus.io/img/test-image.png">
<img src="https://docusaurus.io/assets/images/test-image-742d39e51f41482e8132e79c09ad4eea.png">
<img src="https://docusaurus.io/assets/images/test-image-742d39e51f41482e8132e79c09ad4eea.png">]]></content>
</entry>
<entry>
<title type="html"><![CDATA[Full Blog Sample]]></title>
<id>https://docusaurus.io/myBaseUrl/blog/mdx-blog-post/</id>
<link href="https://docusaurus.io/myBaseUrl/blog/mdx-blog-post/"/>
<updated>2021-03-05T00:00:00.000Z</updated>
<summary type="html"><![CDATA[HTML Heading 1]]></summary>
<content type="html"><![CDATA[<h1>HTML Heading 1</h1>
<h2>HTML Heading 2</h2>
<p>HTML Paragraph</p>
<!-- -->
<!-- -->
<p>Import DOM</p>
<h1>Heading 1</h1>
<h2 class="anchor anchorWithHideOnScrollNavbar_G5V2" id="heading-2">Heading 2<a href="https://docusaurus.io/myBaseUrl/blog/mdx-blog-post/#heading-2" class="hash-link" aria-label="Direct link to Heading 2" title="Direct link to Heading 2"></a></h2>
<h3 class="anchor anchorWithHideOnScrollNavbar_G5V2" id="heading-3">Heading 3<a href="https://docusaurus.io/myBaseUrl/blog/mdx-blog-post/#heading-3" class="hash-link" aria-label="Direct link to Heading 3" title="Direct link to Heading 3"></a></h3>
<h4 class="anchor anchorWithHideOnScrollNavbar_G5V2" id="heading-4">Heading 4<a href="https://docusaurus.io/myBaseUrl/blog/mdx-blog-post/#heading-4" class="hash-link" aria-label="Direct link to Heading 4" title="Direct link to Heading 4"></a></h4>
<h5 class="anchor anchorWithHideOnScrollNavbar_G5V2" id="heading-5">Heading 5<a href="https://docusaurus.io/myBaseUrl/blog/mdx-blog-post/#heading-5" class="hash-link" aria-label="Direct link to Heading 5" title="Direct link to Heading 5"></a></h5>
<ul>
<li>list1</li>
<li>list2</li>
<li>list3</li>
</ul>
<ul>
<li>list1</li>
<li>list2</li>
<li>list3</li>
</ul>
<p>Normal Text <em>Italics Text</em> <strong>Bold Text</strong></p>
<p><a href="https://v2.docusaurus.io/" target="_blank" rel="noopener noreferrer">link</a> <img loading="lazy" src="https://v2.docusaurus.io/" alt="image" class="img_yGFe"></p>]]></content>
</entry>
<entry>
<title type="html"><![CDATA[Complex Slug]]></title>
<id>https://docusaurus.io/myBaseUrl/blog/hey/my super path/héllô/</id>
<link href="https://docusaurus.io/myBaseUrl/blog/hey/my super path/héllô/"/>
<updated>2020-08-16T00:00:00.000Z</updated>
<summary type="html"><![CDATA[complex url slug]]></summary>
<content type="html"><![CDATA[<p>complex url slug</p>]]></content>
<category label="date" term="date"/>
<category label="complex" term="complex"/>
</entry>
<entry>
<title type="html"><![CDATA[Simple Slug]]></title>
<id>https://docusaurus.io/myBaseUrl/blog/simple/slug/</id>
<link href="https://docusaurus.io/myBaseUrl/blog/simple/slug/"/>
<updated>2020-08-15T00:00:00.000Z</updated>
<summary type="html"><![CDATA[simple url slug]]></summary>
<content type="html"><![CDATA[<p>simple url slug</p>]]></content>
<author>
<name>Sébastien Lorber</name>
<uri>https://sebastienlorber.com</uri>
</author>
</entry>
<entry>
<title type="html"><![CDATA[some heading]]></title>
<id>https://docusaurus.io/myBaseUrl/blog/heading-as-title/</id>
<link href="https://docusaurus.io/myBaseUrl/blog/heading-as-title/"/>
<updated>2019-01-02T00:00:00.000Z</updated>
</entry>
<entry>
<title type="html"><![CDATA[date-matter]]></title>
<id>https://docusaurus.io/myBaseUrl/blog/date-matter/</id>
<link href="https://docusaurus.io/myBaseUrl/blog/date-matter/"/>
<updated>2019-01-01T00:00:00.000Z</updated>
<summary type="html"><![CDATA[date inside front matter]]></summary>
<content type="html"><![CDATA[<p>date inside front matter</p>]]></content>
<category label="date" term="date"/>
</entry>
<entry>
<title type="html"><![CDATA[Happy 1st Birthday Slash! (translated)]]></title>
<id>https://docusaurus.io/myBaseUrl/blog/2018/12/14/Happy-First-Birthday-Slash/</id>
<link href="https://docusaurus.io/myBaseUrl/blog/2018/12/14/Happy-First-Birthday-Slash/"/>
<updated>2018-12-14T00:00:00.000Z</updated>
<summary type="html"><![CDATA[Happy birthday! (translated)]]></summary>
<content type="html"><![CDATA[<p>Happy birthday! (translated)</p>]]></content>
<author>
<name>Yangshun Tay (translated)</name>
</author>
<author>
<name>Sébastien Lorber (translated)</name>
<email>lorber.sebastien@gmail.com</email>
</author>
</entry>
</feed>",
]
`;
exports[`json filters to the first two entries 1`] = `
[
"{
@ -378,6 +506,102 @@ exports[`json has feed item for each post 1`] = `
]
`;
exports[`json has feed item for each post - with trailing slash 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": "<p><a href=\\"https://github.com/facebook/docusaurus\\" target=\\"_blank\\" rel=\\"noopener noreferrer\\">absolute full url</a></p>/n<p><a href=\\"https://docusaurus.io/blog/heading-as-title\\">absolute pathname</a></p>/n<p><a href=\\"https://docusaurus.io/blog/heading-as-title\\">relative pathname</a></p>/n<p><a href=\\"https://docusaurus.io/blog/heading-as-title\\">md link</a></p>/n<p><a href=\\"https://docusaurus.io/myBaseUrl/blog/blog-with-links/#title\\">anchor</a></p>/n<p><a href=\\"https://docusaurus.io/blog/heading-as-title#title\\">relative pathname + anchor</a></p>/n<p><img loading=\\"lazy\\" src=\\"https://docusaurus.io/assets/images/test-image-742d39e51f41482e8132e79c09ad4eea.png\\" width=\\"760\\" height=\\"160\\" class=\\"img_yGFe\\"></p>/n<p><img loading=\\"lazy\\" src=\\"https://docusaurus.io/assets/images/slash-introducing-411a16dd05086935b8e9ddae38ae9b45.svg\\" alt=\\"\\" class=\\"img_yGFe\\"></p>/n<img srcset=\\"https://docusaurus.io/img/test-image.png 300w, https://docusaurus.io/img/docusaurus-social-card.png 500w\\">/n<img src=\\"https://docusaurus.io/img/test-image.png\\">",
"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": "<p>Test MDX with require calls</p>/n<!-- -->/n<!-- -->/n<img src=\\"https://docusaurus.io/img/test-image.png\\">/n<img src=\\"https://docusaurus.io/assets/images/test-image-742d39e51f41482e8132e79c09ad4eea.png\\">/n<img src=\\"https://docusaurus.io/assets/images/test-image-742d39e51f41482e8132e79c09ad4eea.png\\">",
"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": "<h1>HTML Heading 1</h1>/n<h2>HTML Heading 2</h2>/n<p>HTML Paragraph</p>/n<!-- -->/n<!-- -->/n<p>Import DOM</p>/n<h1>Heading 1</h1>/n<h2 class=\\"anchor anchorWithHideOnScrollNavbar_G5V2\\" id=\\"heading-2\\">Heading 2<a href=\\"https://docusaurus.io/myBaseUrl/blog/mdx-blog-post/#heading-2\\" class=\\"hash-link\\" aria-label=\\"Direct link to Heading 2\\" title=\\"Direct link to Heading 2\\"></a></h2>/n<h3 class=\\"anchor anchorWithHideOnScrollNavbar_G5V2\\" id=\\"heading-3\\">Heading 3<a href=\\"https://docusaurus.io/myBaseUrl/blog/mdx-blog-post/#heading-3\\" class=\\"hash-link\\" aria-label=\\"Direct link to Heading 3\\" title=\\"Direct link to Heading 3\\"></a></h3>/n<h4 class=\\"anchor anchorWithHideOnScrollNavbar_G5V2\\" id=\\"heading-4\\">Heading 4<a href=\\"https://docusaurus.io/myBaseUrl/blog/mdx-blog-post/#heading-4\\" class=\\"hash-link\\" aria-label=\\"Direct link to Heading 4\\" title=\\"Direct link to Heading 4\\"></a></h4>/n<h5 class=\\"anchor anchorWithHideOnScrollNavbar_G5V2\\" id=\\"heading-5\\">Heading 5<a href=\\"https://docusaurus.io/myBaseUrl/blog/mdx-blog-post/#heading-5\\" class=\\"hash-link\\" aria-label=\\"Direct link to Heading 5\\" title=\\"Direct link to Heading 5\\"></a></h5>/n<ul>/n<li>list1</li>/n<li>list2</li>/n<li>list3</li>/n</ul>/n<ul>/n<li>list1</li>/n<li>list2</li>/n<li>list3</li>/n</ul>/n<p>Normal Text <em>Italics Text</em> <strong>Bold Text</strong></p>/n<p><a href=\\"https://v2.docusaurus.io/\\" target=\\"_blank\\" rel=\\"noopener noreferrer\\">link</a> <img loading=\\"lazy\\" src=\\"https://v2.docusaurus.io/\\" alt=\\"image\\" class=\\"img_yGFe\\"></p>",
"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": "<p>complex url slug</p>",
"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": "<p>simple url slug</p>",
"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": "<p>date inside front matter</p>",
"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": "<p>Happy birthday! (translated)</p>",
"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[`rss filters to the first two entries 1`] = `
[
"<?xml version="1.0" encoding="utf-8"?>
@ -593,3 +817,123 @@ exports[`rss has feed item for each post 1`] = `
</rss>",
]
`;
exports[`rss has feed item for each post - with trailing slash 1`] = `
[
"<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>Hello Blog</title>
<link>https://docusaurus.io/myBaseUrl/blog/</link>
<description>Hello Blog</description>
<lastBuildDate>Sun, 23 Jul 2023 00:00:00 GMT</lastBuildDate>
<docs>https://validator.w3.org/feed/docs/rss2.html</docs>
<generator>https://github.com/jpmonette/feed</generator>
<language>en</language>
<copyright>Copyright</copyright>
<item>
<title><![CDATA[test links]]></title>
<link>https://docusaurus.io/myBaseUrl/blog/blog-with-links/</link>
<guid>https://docusaurus.io/myBaseUrl/blog/blog-with-links/</guid>
<pubDate>Sun, 23 Jul 2023 00:00:00 GMT</pubDate>
<description><![CDATA[absolute full url]]></description>
<content:encoded><![CDATA[<p><a href="https://github.com/facebook/docusaurus" target="_blank" rel="noopener noreferrer">absolute full url</a></p>
<p><a href="https://docusaurus.io/blog/heading-as-title">absolute pathname</a></p>
<p><a href="https://docusaurus.io/blog/heading-as-title">relative pathname</a></p>
<p><a href="https://docusaurus.io/blog/heading-as-title">md link</a></p>
<p><a href="https://docusaurus.io/myBaseUrl/blog/blog-with-links/#title">anchor</a></p>
<p><a href="https://docusaurus.io/blog/heading-as-title#title">relative pathname + anchor</a></p>
<p><img loading="lazy" src="https://docusaurus.io/assets/images/test-image-742d39e51f41482e8132e79c09ad4eea.png" width="760" height="160" class="img_yGFe"></p>
<p><img loading="lazy" src="https://docusaurus.io/assets/images/slash-introducing-411a16dd05086935b8e9ddae38ae9b45.svg" alt="" class="img_yGFe"></p>
<img srcset="https://docusaurus.io/img/test-image.png 300w, https://docusaurus.io/img/docusaurus-social-card.png 500w">
<img src="https://docusaurus.io/img/test-image.png">]]></content:encoded>
</item>
<item>
<title><![CDATA[MDX Blog Sample with require calls]]></title>
<link>https://docusaurus.io/myBaseUrl/blog/mdx-require-blog-post/</link>
<guid>https://docusaurus.io/myBaseUrl/blog/mdx-require-blog-post/</guid>
<pubDate>Sat, 06 Mar 2021 00:00:00 GMT</pubDate>
<description><![CDATA[Test MDX with require calls]]></description>
<content:encoded><![CDATA[<p>Test MDX with require calls</p>
<!-- -->
<!-- -->
<img src="https://docusaurus.io/img/test-image.png">
<img src="https://docusaurus.io/assets/images/test-image-742d39e51f41482e8132e79c09ad4eea.png">
<img src="https://docusaurus.io/assets/images/test-image-742d39e51f41482e8132e79c09ad4eea.png">]]></content:encoded>
</item>
<item>
<title><![CDATA[Full Blog Sample]]></title>
<link>https://docusaurus.io/myBaseUrl/blog/mdx-blog-post/</link>
<guid>https://docusaurus.io/myBaseUrl/blog/mdx-blog-post/</guid>
<pubDate>Fri, 05 Mar 2021 00:00:00 GMT</pubDate>
<description><![CDATA[HTML Heading 1]]></description>
<content:encoded><![CDATA[<h1>HTML Heading 1</h1>
<h2>HTML Heading 2</h2>
<p>HTML Paragraph</p>
<!-- -->
<!-- -->
<p>Import DOM</p>
<h1>Heading 1</h1>
<h2 class="anchor anchorWithHideOnScrollNavbar_G5V2" id="heading-2">Heading 2<a href="https://docusaurus.io/myBaseUrl/blog/mdx-blog-post/#heading-2" class="hash-link" aria-label="Direct link to Heading 2" title="Direct link to Heading 2"></a></h2>
<h3 class="anchor anchorWithHideOnScrollNavbar_G5V2" id="heading-3">Heading 3<a href="https://docusaurus.io/myBaseUrl/blog/mdx-blog-post/#heading-3" class="hash-link" aria-label="Direct link to Heading 3" title="Direct link to Heading 3"></a></h3>
<h4 class="anchor anchorWithHideOnScrollNavbar_G5V2" id="heading-4">Heading 4<a href="https://docusaurus.io/myBaseUrl/blog/mdx-blog-post/#heading-4" class="hash-link" aria-label="Direct link to Heading 4" title="Direct link to Heading 4"></a></h4>
<h5 class="anchor anchorWithHideOnScrollNavbar_G5V2" id="heading-5">Heading 5<a href="https://docusaurus.io/myBaseUrl/blog/mdx-blog-post/#heading-5" class="hash-link" aria-label="Direct link to Heading 5" title="Direct link to Heading 5"></a></h5>
<ul>
<li>list1</li>
<li>list2</li>
<li>list3</li>
</ul>
<ul>
<li>list1</li>
<li>list2</li>
<li>list3</li>
</ul>
<p>Normal Text <em>Italics Text</em> <strong>Bold Text</strong></p>
<p><a href="https://v2.docusaurus.io/" target="_blank" rel="noopener noreferrer">link</a> <img loading="lazy" src="https://v2.docusaurus.io/" alt="image" class="img_yGFe"></p>]]></content:encoded>
</item>
<item>
<title><![CDATA[Complex Slug]]></title>
<link>https://docusaurus.io/myBaseUrl/blog/hey/my super path/héllô/</link>
<guid>https://docusaurus.io/myBaseUrl/blog/hey/my super path/héllô/</guid>
<pubDate>Sun, 16 Aug 2020 00:00:00 GMT</pubDate>
<description><![CDATA[complex url slug]]></description>
<content:encoded><![CDATA[<p>complex url slug</p>]]></content:encoded>
<category>date</category>
<category>complex</category>
</item>
<item>
<title><![CDATA[Simple Slug]]></title>
<link>https://docusaurus.io/myBaseUrl/blog/simple/slug/</link>
<guid>https://docusaurus.io/myBaseUrl/blog/simple/slug/</guid>
<pubDate>Sat, 15 Aug 2020 00:00:00 GMT</pubDate>
<description><![CDATA[simple url slug]]></description>
<content:encoded><![CDATA[<p>simple url slug</p>]]></content:encoded>
</item>
<item>
<title><![CDATA[some heading]]></title>
<link>https://docusaurus.io/myBaseUrl/blog/heading-as-title/</link>
<guid>https://docusaurus.io/myBaseUrl/blog/heading-as-title/</guid>
<pubDate>Wed, 02 Jan 2019 00:00:00 GMT</pubDate>
</item>
<item>
<title><![CDATA[date-matter]]></title>
<link>https://docusaurus.io/myBaseUrl/blog/date-matter/</link>
<guid>https://docusaurus.io/myBaseUrl/blog/date-matter/</guid>
<pubDate>Tue, 01 Jan 2019 00:00:00 GMT</pubDate>
<description><![CDATA[date inside front matter]]></description>
<content:encoded><![CDATA[<p>date inside front matter</p>]]></content:encoded>
<category>date</category>
</item>
<item>
<title><![CDATA[Happy 1st Birthday Slash! (translated)]]></title>
<link>https://docusaurus.io/myBaseUrl/blog/2018/12/14/Happy-First-Birthday-Slash/</link>
<guid>https://docusaurus.io/myBaseUrl/blog/2018/12/14/Happy-First-Birthday-Slash/</guid>
<pubDate>Fri, 14 Dec 2018 00:00:00 GMT</pubDate>
<description><![CDATA[Happy birthday! (translated)]]></description>
<content:encoded><![CDATA[<p>Happy birthday! (translated)</p>]]></content:encoded>
<author>lorber.sebastien@gmail.com (Sébastien Lorber (translated))</author>
</item>
</channel>
</rss>",
]
`;

View File

@ -1,5 +1,250 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`blog plugin process blog posts load content 1`] = `
{
"/blog/tags/tag-1": {
"items": [
"/simple/slug/another",
"/another/tags",
"/another/tags2",
],
"label": "tag1",
"pages": [
{
"items": [
"/simple/slug/another",
],
"metadata": {
"blogDescription": "Blog",
"blogTitle": "Blog",
"nextPage": "/blog/tags/tag-1/page/2",
"page": 1,
"permalink": "/blog/tags/tag-1",
"postsPerPage": 1,
"previousPage": undefined,
"totalCount": 3,
"totalPages": 3,
},
},
{
"items": [
"/another/tags",
],
"metadata": {
"blogDescription": "Blog",
"blogTitle": "Blog",
"nextPage": "/blog/tags/tag-1/page/3",
"page": 2,
"permalink": "/blog/tags/tag-1/page/2",
"postsPerPage": 1,
"previousPage": "/blog/tags/tag-1",
"totalCount": 3,
"totalPages": 3,
},
},
{
"items": [
"/another/tags2",
],
"metadata": {
"blogDescription": "Blog",
"blogTitle": "Blog",
"nextPage": undefined,
"page": 3,
"permalink": "/blog/tags/tag-1/page/3",
"postsPerPage": 1,
"previousPage": "/blog/tags/tag-1/page/2",
"totalCount": 3,
"totalPages": 3,
},
},
],
"permalink": "/blog/tags/tag-1",
"unlisted": false,
},
"/blog/tags/tag-2": {
"items": [
"/another/tags",
"/another/tags2",
],
"label": "tag2",
"pages": [
{
"items": [
"/another/tags",
],
"metadata": {
"blogDescription": "Blog",
"blogTitle": "Blog",
"nextPage": "/blog/tags/tag-2/page/2",
"page": 1,
"permalink": "/blog/tags/tag-2",
"postsPerPage": 1,
"previousPage": undefined,
"totalCount": 2,
"totalPages": 2,
},
},
{
"items": [
"/another/tags2",
],
"metadata": {
"blogDescription": "Blog",
"blogTitle": "Blog",
"nextPage": undefined,
"page": 2,
"permalink": "/blog/tags/tag-2/page/2",
"postsPerPage": 1,
"previousPage": "/blog/tags/tag-2",
"totalCount": 2,
"totalPages": 2,
},
},
],
"permalink": "/blog/tags/tag-2",
"unlisted": false,
},
}
`;
exports[`blog plugin process blog posts load content 2`] = `
[
{
"content": "simple url slug",
"id": "/simple/slug/another",
"metadata": {
"authors": [
{
"imageURL": undefined,
"name": "Sébastien Lorber",
"title": "Docusaurus maintainer",
"url": "https://sebastienlorber.com",
},
],
"date": 2020-08-15T00:00:00.000Z,
"description": "simple url slug",
"editUrl": "https://baseEditUrl.com/edit/blog/another-simple-slug-with-tags.md",
"frontMatter": {
"author": "Sébastien Lorber",
"author_title": "Docusaurus maintainer",
"author_url": "https://sebastienlorber.com",
"date": 2020-08-15T00:00:00.000Z,
"slug": "/simple/slug/another",
"tags": [
"tag1",
],
"title": "Another Simple Slug",
},
"hasTruncateMarker": false,
"lastUpdatedAt": undefined,
"lastUpdatedBy": undefined,
"nextItem": {
"permalink": "/blog/another/tags",
"title": "Another With Tag",
},
"permalink": "/blog/simple/slug/another",
"readingTime": 0.015,
"source": "@site/blog/another-simple-slug-with-tags.md",
"tags": [
{
"label": "tag1",
"permalink": "/blog/tags/tag-1",
},
],
"title": "Another Simple Slug",
"unlisted": false,
},
},
{
"content": "with tag",
"id": "/another/tags",
"metadata": {
"authors": [],
"date": 2020-08-15T00:00:00.000Z,
"description": "with tag",
"editUrl": "https://baseEditUrl.com/edit/blog/another-with-tags.md",
"frontMatter": {
"date": 2020-08-15T00:00:00.000Z,
"slug": "/another/tags",
"tags": [
"tag1",
"tag2",
],
"title": "Another With Tag",
},
"hasTruncateMarker": false,
"lastUpdatedAt": undefined,
"lastUpdatedBy": undefined,
"nextItem": {
"permalink": "/blog/another/tags2",
"title": "Another With Tag",
},
"permalink": "/blog/another/tags",
"prevItem": {
"permalink": "/blog/simple/slug/another",
"title": "Another Simple Slug",
},
"readingTime": 0.01,
"source": "@site/blog/another-with-tags.md",
"tags": [
{
"label": "tag1",
"permalink": "/blog/tags/tag-1",
},
{
"label": "tag2",
"permalink": "/blog/tags/tag-2",
},
],
"title": "Another With Tag",
"unlisted": false,
},
},
{
"content": "with tag",
"id": "/another/tags2",
"metadata": {
"authors": [],
"date": 2020-08-15T00:00:00.000Z,
"description": "with tag",
"editUrl": "https://baseEditUrl.com/edit/blog/another-with-tags2.md",
"frontMatter": {
"date": 2020-08-15T00:00:00.000Z,
"slug": "/another/tags2",
"tags": [
"tag1",
"tag2",
],
"title": "Another With Tag",
},
"hasTruncateMarker": false,
"lastUpdatedAt": undefined,
"lastUpdatedBy": undefined,
"permalink": "/blog/another/tags2",
"prevItem": {
"permalink": "/blog/another/tags",
"title": "Another With Tag",
},
"readingTime": 0.01,
"source": "@site/blog/another-with-tags2.md",
"tags": [
{
"label": "tag1",
"permalink": "/blog/tags/tag-1",
},
{
"label": "tag2",
"permalink": "/blog/tags/tag-2",
},
],
"title": "Another With Tag",
"unlisted": false,
},
},
]
`;
exports[`blog plugin works on blog tags without pagination 1`] = `
{
"/blog/tags/tag-1": {

View File

@ -50,7 +50,6 @@ exports[`translateContent falls back when translation is incomplete 1`] = `
"authors": [],
"date": 2021-07-19T00:00:00.000Z,
"description": "/blog/2021/06/19/hello",
"formattedDate": "June 19, 2021",
"frontMatter": {},
"hasTruncateMarker": true,
"permalink": "/blog/2021/06/19/hello",
@ -94,7 +93,6 @@ exports[`translateContent returns translated loaded 1`] = `
"authors": [],
"date": 2021-07-19T00:00:00.000Z,
"description": "/blog/2021/06/19/hello",
"formattedDate": "June 19, 2021",
"frontMatter": {},
"hasTruncateMarker": true,
"permalink": "/blog/2021/06/19/hello",

View File

@ -8,12 +8,14 @@
import {jest} from '@jest/globals';
import fs from 'fs-extra';
import path from 'path';
import {fromPartial} from '@total-typescript/shoehorn';
import {
truncate,
parseBlogFileName,
linkify,
getSourceToPermalink,
paginateBlogPosts,
applyProcessBlogPosts,
type LinkifyParams,
} from '../blogUtils';
import type {BlogBrokenMarkdownLink, BlogContentPaths} from '../types';
@ -38,14 +40,15 @@ describe('truncate', () => {
});
describe('paginateBlogPosts', () => {
it('generates right pages', () => {
const blogPosts = [
{id: 'post1', metadata: {}, content: 'Foo 1'},
{id: 'post2', metadata: {}, content: 'Foo 2'},
{id: 'post3', metadata: {}, content: 'Foo 3'},
{id: 'post4', metadata: {}, content: 'Foo 4'},
{id: 'post5', metadata: {}, content: 'Foo 5'},
] as BlogPost[];
const blogPosts = [
{id: 'post1', metadata: {}, content: 'Foo 1'},
{id: 'post2', metadata: {}, content: 'Foo 2'},
{id: 'post3', metadata: {}, content: 'Foo 3'},
{id: 'post4', metadata: {}, content: 'Foo 4'},
{id: 'post5', metadata: {}, content: 'Foo 5'},
] as BlogPost[];
it('generates pages', () => {
expect(
paginateBlogPosts({
blogPosts,
@ -53,8 +56,12 @@ describe('paginateBlogPosts', () => {
blogTitle: 'Blog Title',
blogDescription: 'Blog Description',
postsPerPageOption: 2,
pageBasePath: 'page',
}),
).toMatchSnapshot();
});
it('generates pages at blog root', () => {
expect(
paginateBlogPosts({
blogPosts,
@ -62,8 +69,12 @@ describe('paginateBlogPosts', () => {
blogTitle: 'Blog Title',
blogDescription: 'Blog Description',
postsPerPageOption: 2,
pageBasePath: 'page',
}),
).toMatchSnapshot();
});
it('generates a single page', () => {
expect(
paginateBlogPosts({
blogPosts,
@ -71,6 +82,20 @@ describe('paginateBlogPosts', () => {
blogTitle: 'Blog Title',
blogDescription: 'Blog Description',
postsPerPageOption: 10,
pageBasePath: 'page',
}),
).toMatchSnapshot();
});
it('generates pages with custom pageBasePath', () => {
expect(
paginateBlogPosts({
blogPosts,
basePageUrl: '/blog',
blogTitle: 'Blog Title',
blogDescription: 'Blog Description',
postsPerPageOption: 2,
pageBasePath: 'customPageBasePath',
}),
).toMatchSnapshot();
});
@ -213,7 +238,7 @@ describe('linkify', () => {
hasTruncateMarker: false,
frontMatter: {},
authors: [],
formattedDate: '',
unlisted: false,
},
content: '',
},
@ -272,3 +297,81 @@ describe('linkify', () => {
} as BlogBrokenMarkdownLink);
});
});
describe('processBlogPosts', () => {
const blogPost2022: BlogPost = fromPartial({
metadata: {date: new Date('2022-01-01')},
});
const blogPost2023: BlogPost = fromPartial({
metadata: {date: new Date('2023-01-01')},
});
const blogPost2024: BlogPost = fromPartial({
metadata: {date: new Date('2024-01-01')},
});
it('filter blogs only from 2024', async () => {
const processedBlogPosts = await applyProcessBlogPosts({
blogPosts: [blogPost2022, blogPost2023, blogPost2024],
processBlogPosts: async ({blogPosts}: {blogPosts: BlogPost[]}) =>
blogPosts.filter(
(blogPost) => blogPost.metadata.date.getFullYear() === 2024,
),
});
expect(processedBlogPosts).toEqual([blogPost2024]);
});
it('sort blogs by date in ascending order', async () => {
const processedBlogPosts = await applyProcessBlogPosts({
blogPosts: [blogPost2023, blogPost2022, blogPost2024],
processBlogPosts: async ({blogPosts}: {blogPosts: BlogPost[]}) =>
blogPosts.sort(
(a, b) => a.metadata.date.getTime() - b.metadata.date.getTime(),
),
});
expect(processedBlogPosts).toEqual([
blogPost2022,
blogPost2023,
blogPost2024,
]);
});
it('sort blogs by date in descending order', async () => {
const processedBlogPosts = await applyProcessBlogPosts({
blogPosts: [blogPost2023, blogPost2022, blogPost2024],
processBlogPosts: async ({blogPosts}: {blogPosts: BlogPost[]}) =>
blogPosts.sort(
(a, b) => b.metadata.date.getTime() - a.metadata.date.getTime(),
),
});
expect(processedBlogPosts).toEqual([
blogPost2024,
blogPost2023,
blogPost2022,
]);
});
it('processBlogPosts return 2022 only', async () => {
const processedBlogPosts = await applyProcessBlogPosts({
blogPosts: [blogPost2023, blogPost2022, blogPost2024],
processBlogPosts: async () => [blogPost2022],
});
expect(processedBlogPosts).toEqual([blogPost2022]);
});
it('processBlogPosts return undefined', async () => {
const processedBlogPosts = await applyProcessBlogPosts({
blogPosts: [blogPost2023, blogPost2022, blogPost2024],
processBlogPosts: async () => {},
});
expect(processedBlogPosts).toEqual([
blogPost2023,
blogPost2022,
blogPost2024,
]);
});
});

View File

@ -9,6 +9,7 @@ import {jest} from '@jest/globals';
import path from 'path';
import fs from 'fs-extra';
import {DEFAULT_PARSE_FRONT_MATTER} from '@docusaurus/utils';
import {fromPartial} from '@total-typescript/shoehorn';
import {DEFAULT_OPTIONS} from '../options';
import {generateBlogPosts} from '../blogUtils';
import {createBlogFeedFiles} from '../feed';
@ -80,12 +81,12 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
const outDir = path.join(siteDir, 'build-snap');
await testGenerateFeeds(
{
fromPartial({
siteDir,
siteConfig,
i18n: DefaultI18N,
outDir,
} as LoadContext,
}),
{
path: 'invalid-blog-path',
routeBasePath: 'blog',
@ -120,12 +121,12 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
// Build is quite difficult to mock, so we built the blog beforehand and
// copied the output to the fixture...
await testGenerateFeeds(
{
fromPartial({
siteDir,
siteConfig,
i18n: DefaultI18N,
outDir,
} as LoadContext,
}),
{
path: 'blog',
routeBasePath: 'blog',
@ -163,12 +164,12 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
// Build is quite difficult to mock, so we built the blog beforehand and
// copied the output to the fixture...
await testGenerateFeeds(
{
fromPartial({
siteDir,
siteConfig,
i18n: DefaultI18N,
outDir,
} as LoadContext,
}),
{
path: 'blog',
routeBasePath: 'blog',
@ -216,12 +217,12 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
// Build is quite difficult to mock, so we built the blog beforehand and
// copied the output to the fixture...
await testGenerateFeeds(
{
fromPartial({
siteDir,
siteConfig,
i18n: DefaultI18N,
outDir,
} as LoadContext,
}),
{
path: 'blog',
routeBasePath: 'blog',
@ -245,4 +246,48 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
).toMatchSnapshot();
fsMock.mockClear();
});
it('has feed item for each post - with trailing slash', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'website');
const outDir = path.join(siteDir, 'build-snap');
const siteConfig = {
title: 'Hello',
baseUrl: '/myBaseUrl/',
url: 'https://docusaurus.io',
favicon: 'image/favicon.ico',
trailingSlash: true,
markdown,
};
// Build is quite difficult to mock, so we built the blog beforehand and
// copied the output to the fixture...
await testGenerateFeeds(
fromPartial({
siteDir,
siteConfig,
i18n: DefaultI18N,
outDir,
}),
{
path: 'blog',
routeBasePath: 'blog',
tagsBasePath: 'tags',
authorsMapPath: 'authors.yml',
include: DEFAULT_OPTIONS.include,
exclude: DEFAULT_OPTIONS.exclude,
feedOptions: {
type: [feedType],
copyright: 'Copyright',
},
readingTime: ({content, defaultReadingTime}) =>
defaultReadingTime({content}),
truncateMarker: /<!--\s*truncate\s*-->/,
} as PluginOptions,
);
expect(
fsMock.mock.calls.map((call) => call[1] as string),
).toMatchSnapshot();
fsMock.mockClear();
});
});

View File

@ -8,7 +8,11 @@
import {jest} from '@jest/globals';
import path from 'path';
import {normalizePluginOptions} from '@docusaurus/utils-validation';
import {posixPath, getFileCommitDate} from '@docusaurus/utils';
import {
posixPath,
getFileCommitDate,
LAST_UPDATE_FALLBACK,
} from '@docusaurus/utils';
import pluginContentBlog from '../index';
import {validateOptions} from '../options';
import type {
@ -41,6 +45,7 @@ const markdown: MarkdownConfig = {
}
return result;
},
remarkRehypeOptions: undefined,
};
function findByTitle(
@ -175,7 +180,6 @@ describe('blog plugin', () => {
description: `date inside front matter`,
authors: [],
date: new Date('2019-01-01'),
formattedDate: 'January 1, 2019',
frontMatter: {
date: new Date('2019-01-01'),
tags: ['date'],
@ -220,7 +224,6 @@ describe('blog plugin', () => {
},
],
date: new Date('2018-12-14'),
formattedDate: 'December 14, 2018',
frontMatter: {
authors: [
{
@ -256,7 +259,6 @@ describe('blog plugin', () => {
title: 'Simple Slug',
},
date: new Date('2020-08-16'),
formattedDate: 'August 16, 2020',
frontMatter: {
date: '2020/08/16',
slug: '/hey/my super path/héllô',
@ -302,7 +304,6 @@ describe('blog plugin', () => {
title: 'draft',
},
date: new Date('2020-08-15'),
formattedDate: 'August 15, 2020',
frontMatter: {
author: 'Sébastien Lorber',
author_title: 'Docusaurus maintainer',
@ -328,7 +329,6 @@ describe('blog plugin', () => {
description: '',
authors: [],
date: new Date('2019-01-02'),
formattedDate: 'January 2, 2019',
frontMatter: {
date: new Date('2019-01-02'),
},
@ -343,39 +343,6 @@ describe('blog plugin', () => {
});
});
it('builds simple website blog with localized dates', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'website');
const blogPostsFrench = await getBlogPosts(siteDir, {}, getI18n('fr'));
expect(blogPostsFrench).toHaveLength(10);
expect(blogPostsFrench[0]!.metadata.formattedDate).toMatchInlineSnapshot(
`"23 juillet 2023"`,
);
expect(blogPostsFrench[1]!.metadata.formattedDate).toMatchInlineSnapshot(
`"6 mars 2021"`,
);
expect(blogPostsFrench[2]!.metadata.formattedDate).toMatchInlineSnapshot(
`"5 mars 2021"`,
);
expect(blogPostsFrench[3]!.metadata.formattedDate).toMatchInlineSnapshot(
`"16 août 2020"`,
);
expect(blogPostsFrench[4]!.metadata.formattedDate).toMatchInlineSnapshot(
`"15 août 2020"`,
);
expect(blogPostsFrench[5]!.metadata.formattedDate).toMatchInlineSnapshot(
`"27 février 2020"`,
);
expect(blogPostsFrench[6]!.metadata.formattedDate).toMatchInlineSnapshot(
`"27 février 2020"`,
);
expect(blogPostsFrench[7]!.metadata.formattedDate).toMatchInlineSnapshot(
`"2 janvier 2019"`,
);
expect(blogPostsFrench[8]!.metadata.formattedDate).toMatchInlineSnapshot(
`"1 janvier 2019"`,
);
});
it('handles edit URL with editLocalizedBlogs: true', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'website');
const blogPosts = await getBlogPosts(siteDir, {editLocalizedFiles: true});
@ -474,13 +441,8 @@ describe('blog plugin', () => {
const noDateSource = path.posix.join('@site', PluginPath, 'no date.md');
const noDateSourceFile = path.posix.join(siteDir, PluginPath, 'no date.md');
// We know the file exists and we know we have git
const result = getFileCommitDate(noDateSourceFile, {age: 'oldest'});
const result = await getFileCommitDate(noDateSourceFile, {age: 'oldest'});
const noDateSourceTime = result.date;
const formattedDate = Intl.DateTimeFormat('en', {
day: 'numeric',
month: 'long',
year: 'numeric',
}).format(noDateSourceTime);
expect({
...getByTitle(blogPosts, 'no date').metadata,
@ -494,7 +456,6 @@ describe('blog plugin', () => {
description: `no date`,
authors: [],
date: noDateSourceTime,
formattedDate,
frontMatter: {},
tags: [],
prevItem: undefined,
@ -541,4 +502,165 @@ describe('blog plugin', () => {
expect(blogTags).toMatchSnapshot();
});
it('process blog posts load content', async () => {
const siteDir = path.join(
__dirname,
'__fixtures__',
'website-blog-with-tags',
);
const plugin = await getPlugin(
siteDir,
{
postsPerPage: 1,
processBlogPosts: async ({blogPosts}) =>
blogPosts.filter((blog) => blog.metadata.tags[0]?.label === 'tag1'),
},
DefaultI18N,
);
const {blogPosts, blogTags, blogListPaginated} =
(await plugin.loadContent!())!;
expect(blogListPaginated).toHaveLength(3);
expect(Object.keys(blogTags)).toHaveLength(2);
expect(blogTags).toMatchSnapshot();
expect(blogPosts).toHaveLength(3);
expect(blogPosts).toMatchSnapshot();
});
});
describe('last update', () => {
const siteDir = path.join(
__dirname,
'__fixtures__',
'website-blog-with-last-update',
);
const lastUpdateFor = (date: string) => new Date(date).getTime();
it('author and time', async () => {
const plugin = await getPlugin(
siteDir,
{
showLastUpdateAuthor: true,
showLastUpdateTime: true,
},
DefaultI18N,
);
const {blogPosts} = (await plugin.loadContent!())!;
expect(blogPosts[0]?.metadata.lastUpdatedBy).toBe('seb');
expect(blogPosts[0]?.metadata.lastUpdatedAt).toBe(
LAST_UPDATE_FALLBACK.lastUpdatedAt,
);
expect(blogPosts[1]?.metadata.lastUpdatedBy).toBe(
LAST_UPDATE_FALLBACK.lastUpdatedBy,
);
expect(blogPosts[1]?.metadata.lastUpdatedAt).toBe(
LAST_UPDATE_FALLBACK.lastUpdatedAt,
);
expect(blogPosts[2]?.metadata.lastUpdatedBy).toBe('seb');
expect(blogPosts[2]?.metadata.lastUpdatedAt).toBe(
lastUpdateFor('2021-01-01'),
);
expect(blogPosts[3]?.metadata.lastUpdatedBy).toBe(
LAST_UPDATE_FALLBACK.lastUpdatedBy,
);
expect(blogPosts[3]?.metadata.lastUpdatedAt).toBe(
lastUpdateFor('2021-01-01'),
);
});
it('time only', async () => {
const plugin = await getPlugin(
siteDir,
{
showLastUpdateAuthor: false,
showLastUpdateTime: true,
},
DefaultI18N,
);
const {blogPosts} = (await plugin.loadContent!())!;
expect(blogPosts[0]?.metadata.title).toBe('Author');
expect(blogPosts[0]?.metadata.lastUpdatedBy).toBeUndefined();
expect(blogPosts[0]?.metadata.lastUpdatedAt).toBe(
LAST_UPDATE_FALLBACK.lastUpdatedAt,
);
expect(blogPosts[1]?.metadata.title).toBe('Nothing');
expect(blogPosts[1]?.metadata.lastUpdatedBy).toBeUndefined();
expect(blogPosts[1]?.metadata.lastUpdatedAt).toBe(
LAST_UPDATE_FALLBACK.lastUpdatedAt,
);
expect(blogPosts[2]?.metadata.title).toBe('Both');
expect(blogPosts[2]?.metadata.lastUpdatedBy).toBeUndefined();
expect(blogPosts[2]?.metadata.lastUpdatedAt).toBe(
lastUpdateFor('2021-01-01'),
);
expect(blogPosts[3]?.metadata.title).toBe('Last update date');
expect(blogPosts[3]?.metadata.lastUpdatedBy).toBeUndefined();
expect(blogPosts[3]?.metadata.lastUpdatedAt).toBe(
lastUpdateFor('2021-01-01'),
);
});
it('author only', async () => {
const plugin = await getPlugin(
siteDir,
{
showLastUpdateAuthor: true,
showLastUpdateTime: false,
},
DefaultI18N,
);
const {blogPosts} = (await plugin.loadContent!())!;
expect(blogPosts[0]?.metadata.lastUpdatedBy).toBe('seb');
expect(blogPosts[0]?.metadata.lastUpdatedAt).toBeUndefined();
expect(blogPosts[1]?.metadata.lastUpdatedBy).toBe(
LAST_UPDATE_FALLBACK.lastUpdatedBy,
);
expect(blogPosts[1]?.metadata.lastUpdatedAt).toBeUndefined();
expect(blogPosts[2]?.metadata.lastUpdatedBy).toBe('seb');
expect(blogPosts[2]?.metadata.lastUpdatedAt).toBeUndefined();
expect(blogPosts[3]?.metadata.lastUpdatedBy).toBe(
LAST_UPDATE_FALLBACK.lastUpdatedBy,
);
expect(blogPosts[3]?.metadata.lastUpdatedAt).toBeUndefined();
});
it('none', async () => {
const plugin = await getPlugin(
siteDir,
{
showLastUpdateAuthor: false,
showLastUpdateTime: false,
},
DefaultI18N,
);
const {blogPosts} = (await plugin.loadContent!())!;
expect(blogPosts[0]?.metadata.lastUpdatedBy).toBeUndefined();
expect(blogPosts[0]?.metadata.lastUpdatedAt).toBeUndefined();
expect(blogPosts[1]?.metadata.lastUpdatedBy).toBeUndefined();
expect(blogPosts[1]?.metadata.lastUpdatedAt).toBeUndefined();
expect(blogPosts[2]?.metadata.lastUpdatedBy).toBeUndefined();
expect(blogPosts[2]?.metadata.lastUpdatedAt).toBeUndefined();
expect(blogPosts[3]?.metadata.lastUpdatedBy).toBeUndefined();
expect(blogPosts[3]?.metadata.lastUpdatedAt).toBeUndefined();
});
});

View File

@ -29,7 +29,6 @@ const sampleBlogPosts: BlogPost[] = [
source: '/blog/2021/06/19/hello',
description: '/blog/2021/06/19/hello',
date: new Date(2021, 6, 19),
formattedDate: 'June 19, 2021',
tags: [],
title: 'Hello',
hasTruncateMarker: true,

View File

@ -26,6 +26,7 @@ import {
getContentPathList,
isUnlisted,
isDraft,
readLastUpdateData,
} from '@docusaurus/utils';
import {validateBlogPostFrontMatter} from './frontMatter';
import {type AuthorsMap, getAuthorsMap, getBlogPostAuthors} from './authors';
@ -57,12 +58,14 @@ export function paginateBlogPosts({
blogTitle,
blogDescription,
postsPerPageOption,
pageBasePath,
}: {
blogPosts: BlogPost[];
basePageUrl: string;
blogTitle: string;
blogDescription: string;
postsPerPageOption: number | 'ALL';
pageBasePath: string;
}): BlogPaginated[] {
const totalCount = blogPosts.length;
const postsPerPage =
@ -73,7 +76,7 @@ export function paginateBlogPosts({
function permalink(page: number) {
return page > 0
? normalizeUrl([basePageUrl, `page/${page + 1}`])
? normalizeUrl([basePageUrl, pageBasePath, `${page + 1}`])
: basePageUrl;
}
@ -111,6 +114,7 @@ export function getBlogTags({
blogTitle: string;
blogDescription: string;
postsPerPageOption: number | 'ALL';
pageBasePath: string;
}): BlogTags {
const groups = groupTaggedItems(
blogPosts,
@ -161,25 +165,6 @@ export function parseBlogFileName(
return {date: undefined, text, slug};
}
function formatBlogPostDate(
locale: string,
date: Date,
calendar: string,
): string {
try {
return new Intl.DateTimeFormat(locale, {
day: 'numeric',
month: 'long',
year: 'numeric',
timeZone: 'UTC',
calendar,
}).format(date);
} catch (err) {
logger.error`Can't format blog post date "${String(date)}"`;
throw err;
}
}
async function parseBlogPostMarkdownFile({
filePath,
parseFrontMatter,
@ -247,6 +232,12 @@ async function processBlogSourceFile(
const aliasedSource = aliasedSitePath(blogSourceAbsolute, siteDir);
const lastUpdate = await readLastUpdateData(
blogSourceAbsolute,
options,
frontMatter.last_update,
);
const draft = isDraft({frontMatter});
const unlisted = isUnlisted({frontMatter});
@ -274,10 +265,11 @@ async function processBlogSourceFile(
}
try {
const result = getFileCommitDate(blogSourceAbsolute, {
const result = await getFileCommitDate(blogSourceAbsolute, {
age: 'oldest',
includeAuthor: false,
});
return result.date;
} catch (err) {
logger.warn(err);
@ -286,11 +278,6 @@ async function processBlogSourceFile(
}
const date = await getDate();
const formattedDate = formatBlogPostDate(
i18n.currentLocale,
date,
i18n.localeConfigs[i18n.currentLocale]!.calendar,
);
const title = frontMatter.title ?? contentTitle ?? parsedBlogFileName.text;
const description = frontMatter.description ?? excerpt ?? '';
@ -345,7 +332,6 @@ async function processBlogSourceFile(
title,
description,
date,
formattedDate,
tags: normalizeFrontMatterTags(tagsBasePath, frontMatter.tags),
readingTime: showReadingTime
? options.readingTime({
@ -358,6 +344,8 @@ async function processBlogSourceFile(
authors,
frontMatter,
unlisted,
lastUpdatedAt: lastUpdate.lastUpdatedAt,
lastUpdatedBy: lastUpdate.lastUpdatedBy,
},
content,
};
@ -443,3 +431,19 @@ export function linkify({
return newContent;
}
export async function applyProcessBlogPosts({
blogPosts,
processBlogPosts,
}: {
blogPosts: BlogPost[];
processBlogPosts: PluginOptions['processBlogPosts'];
}): Promise<BlogPost[]> {
const processedBlogPosts = await processBlogPosts({blogPosts});
if (Array.isArray(processedBlogPosts)) {
return processedBlogPosts;
}
return blogPosts;
}

View File

@ -0,0 +1,20 @@
/**
* 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 useRouteContext from '@docusaurus/useRouteContext';
import type {BlogMetadata} from '@docusaurus/plugin-content-blog';
export function useBlogMetadata(): BlogMetadata {
const routeContext = useRouteContext();
const blogMetadata = routeContext?.data?.blogMetadata;
if (!blogMetadata) {
throw new Error(
"useBlogMetadata() can't be called on the current route because the blog metadata could not be found in route context",
);
}
return blogMetadata as BlogMetadata;
}

View File

@ -11,7 +11,10 @@ import logger from '@docusaurus/logger';
import {Feed, type Author as FeedAuthor} from 'feed';
import * as srcset from 'srcset';
import {normalizeUrl, readOutputHTMLFile} from '@docusaurus/utils';
import {blogPostContainerID} from '@docusaurus/utils-common';
import {
blogPostContainerID,
applyTrailingSlash,
} from '@docusaurus/utils-common';
import {load as cheerioLoad} from 'cheerio';
import type {DocusaurusConfig} from '@docusaurus/types';
import type {
@ -40,8 +43,14 @@ async function generateBlogFeed({
}
const {feedOptions, routeBasePath} = options;
const {url: siteUrl, baseUrl, title, favicon} = siteConfig;
const blogBaseUrl = normalizeUrl([siteUrl, baseUrl, routeBasePath]);
const {url: siteUrl, baseUrl, title, favicon, trailingSlash} = siteConfig;
const blogBaseUrl = applyTrailingSlash(
normalizeUrl([siteUrl, baseUrl, routeBasePath]),
{
trailingSlash,
baseUrl,
},
);
const blogPostsForFeed =
feedOptions.limit === false || feedOptions.limit === null
@ -85,7 +94,7 @@ async function defaultCreateFeedItems({
siteConfig: DocusaurusConfig;
outDir: string;
}): Promise<BlogFeedItem[]> {
const {url: siteUrl} = siteConfig;
const {url: siteUrl, baseUrl, trailingSlash} = siteConfig;
function toFeedAuthor(author: Author): FeedAuthor {
return {name: author.name, link: author.url, email: author.email};
@ -105,13 +114,19 @@ async function defaultCreateFeedItems({
} = post;
const content = await readOutputHTMLFile(
permalink.replace(siteConfig.baseUrl, ''),
permalink.replace(baseUrl, ''),
outDir,
siteConfig.trailingSlash,
trailingSlash,
);
const $ = cheerioLoad(content);
const blogPostAbsoluteUrl = normalizeUrl([siteUrl, permalink]);
const blogPostAbsoluteUrl = applyTrailingSlash(
normalizeUrl([siteUrl, permalink]),
{
trailingSlash,
baseUrl,
},
);
const toAbsoluteUrl = (src: string) =>
String(new URL(src, blogPostAbsoluteUrl));

View File

@ -4,14 +4,14 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
ContentVisibilitySchema,
FrontMatterLastUpdateSchema,
FrontMatterTOCHeadingLevels,
FrontMatterTagsSchema,
JoiFrontMatter as Joi, // Custom instance for front matter
URISchema,
validateFrontMatter,
FrontMatterTagsSchema,
FrontMatterTOCHeadingLevels,
ContentVisibilitySchema,
} from '@docusaurus/utils-validation';
import type {BlogPostFrontMatter} from '@docusaurus/plugin-content-blog';
@ -69,6 +69,7 @@ const BlogFrontMatterSchema = Joi.object<BlogPostFrontMatter>({
hide_table_of_contents: Joi.boolean(),
...FrontMatterTOCHeadingLevels,
last_update: FrontMatterLastUpdateSchema,
})
.messages({
'deprecate.error':

View File

@ -11,6 +11,7 @@ import {
normalizeUrl,
docuHash,
aliasedSitePath,
aliasedSitePathToRelativePath,
getPluginI18nPath,
posixPath,
addTrailingPathSeparator,
@ -20,11 +21,12 @@ import {
DEFAULT_PLUGIN_ID,
} from '@docusaurus/utils';
import {
generateBlogPosts,
getSourceToPermalink,
getBlogTags,
paginateBlogPosts,
shouldBeListed,
applyProcessBlogPosts,
generateBlogPosts,
} from './blogUtils';
import footnoteIDFixer from './remark/footnoteIDFixer';
import {translateContent, getTranslationFiles} from './translations';
@ -32,7 +34,12 @@ import {createBlogFeedFiles} from './feed';
import {toTagProp, toTagsProp} from './props';
import type {BlogContentPaths, BlogMarkdownLoaderOptions} from './types';
import type {LoadContext, Plugin, HtmlTags} from '@docusaurus/types';
import type {
LoadContext,
Plugin,
HtmlTags,
RouteMetadata,
} from '@docusaurus/types';
import type {
PluginOptions,
BlogPostFrontMatter,
@ -42,6 +49,7 @@ import type {
BlogTags,
BlogContent,
BlogPaginated,
BlogMetadata,
} from '@docusaurus/plugin-content-blog';
export default async function pluginContentBlog(
@ -107,11 +115,16 @@ export default async function pluginContentBlog(
blogDescription,
blogTitle,
blogSidebarTitle,
pageBasePath,
} = options;
const baseBlogUrl = normalizeUrl([baseUrl, routeBasePath]);
const blogTagsListPath = normalizeUrl([baseBlogUrl, tagsBasePath]);
const blogPosts = await generateBlogPosts(contentPaths, context, options);
let blogPosts = await generateBlogPosts(contentPaths, context, options);
blogPosts = await applyProcessBlogPosts({
blogPosts,
processBlogPosts: options.processBlogPosts,
});
const listedBlogPosts = blogPosts.filter(shouldBeListed);
if (!blogPosts.length) {
@ -121,11 +134,10 @@ export default async function pluginContentBlog(
blogListPaginated: [],
blogTags: {},
blogTagsListPath,
blogTagsPaginated: [],
};
}
// Colocate next and prev metadata.
// Collocate next and prev metadata.
listedBlogPosts.forEach((blogPost, index) => {
const prevItem = index > 0 ? listedBlogPosts[index - 1] : null;
if (prevItem) {
@ -153,6 +165,7 @@ export default async function pluginContentBlog(
blogDescription,
postsPerPageOption,
basePageUrl: baseBlogUrl,
pageBasePath,
});
const blogTags: BlogTags = getBlogTags({
@ -160,6 +173,7 @@ export default async function pluginContentBlog(
postsPerPageOption,
blogDescription,
blogTitle,
pageBasePath,
});
return {
@ -180,6 +194,7 @@ export default async function pluginContentBlog(
blogArchiveComponent,
routeBasePath,
archiveBasePath,
blogTitle,
} = options;
const {addRoute, createData} = actions;
@ -255,6 +270,24 @@ export default async function pluginContentBlog(
),
);
const blogMetadata: BlogMetadata = {
blogBasePath: normalizeUrl([baseUrl, routeBasePath]),
blogTitle,
};
const blogMetadataPath = await createData(
`blogMetadata-${pluginId}.json`,
JSON.stringify(blogMetadata, null, 2),
);
function createBlogPostRouteMetadata(
blogPostMeta: BlogPostMetadata,
): RouteMetadata {
return {
sourceFilePath: aliasedSitePathToRelativePath(blogPostMeta.source),
lastUpdatedAt: blogPostMeta.lastUpdatedAt,
};
}
// Create routes for blog entries.
await Promise.all(
blogPosts.map(async (blogPost) => {
@ -274,6 +307,10 @@ export default async function pluginContentBlog(
sidebar: aliasedSource(sidebarProp),
content: metadata.source,
},
metadata: createBlogPostRouteMetadata(metadata),
context: {
blogMetadata: aliasedSource(blogMetadataPath),
},
});
blogItemsToMetadata[id] = metadata;

View File

@ -45,11 +45,15 @@ export const DEFAULT_OPTIONS: PluginOptions = {
routeBasePath: 'blog',
tagsBasePath: 'tags',
archiveBasePath: 'archive',
pageBasePath: 'page',
path: 'blog',
editLocalizedFiles: false,
authorsMapPath: 'authors.yml',
readingTime: ({content, defaultReadingTime}) => defaultReadingTime({content}),
sortPosts: 'descending',
showLastUpdateTime: false,
showLastUpdateAuthor: false,
processBlogPosts: async () => undefined,
};
const PluginOptionSchema = Joi.object<PluginOptions>({
@ -59,6 +63,7 @@ const PluginOptionSchema = Joi.object<PluginOptions>({
.allow(null),
routeBasePath: RouteBasePathSchema.default(DEFAULT_OPTIONS.routeBasePath),
tagsBasePath: Joi.string().default(DEFAULT_OPTIONS.tagsBasePath),
pageBasePath: Joi.string().default(DEFAULT_OPTIONS.pageBasePath),
include: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.include),
exclude: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.exclude),
postsPerPage: Joi.alternatives()
@ -132,6 +137,13 @@ const PluginOptionSchema = Joi.object<PluginOptions>({
sortPosts: Joi.string()
.valid('descending', 'ascending')
.default(DEFAULT_OPTIONS.sortPosts),
showLastUpdateTime: Joi.bool().default(DEFAULT_OPTIONS.showLastUpdateTime),
showLastUpdateAuthor: Joi.bool().default(
DEFAULT_OPTIONS.showLastUpdateAuthor,
),
processBlogPosts: Joi.function()
.optional()
.default(() => DEFAULT_OPTIONS.processBlogPosts),
}).default(DEFAULT_OPTIONS);
export function validateOptions({

View File

@ -5,10 +5,17 @@
* LICENSE file in the root directory of this source tree.
*/
/// <reference types="@docusaurus/module-type-aliases" />
declare module '@docusaurus/plugin-content-blog' {
import type {LoadedMDXContent} from '@docusaurus/mdx-loader';
import type {MDXOptions} from '@docusaurus/mdx-loader';
import type {FrontMatterTag, Tag} from '@docusaurus/utils';
import type {
FrontMatterTag,
Tag,
LastUpdateData,
FrontMatterLastUpdate,
} from '@docusaurus/utils';
import type {DocusaurusConfig, Plugin, LoadContext} from '@docusaurus/types';
import type {Item as FeedItem} from 'feed';
import type {Overwrite} from 'utility-types';
@ -154,6 +161,8 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
toc_min_heading_level?: number;
/** Maximum TOC heading level. Must be between 2 and 6. */
toc_max_heading_level?: number;
/** Allows overriding the last updated author and/or date. */
last_update?: FrontMatterLastUpdate;
};
export type BlogPostFrontMatterAuthor = Author & {
@ -178,7 +187,7 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
| BlogPostFrontMatterAuthor
| (string | BlogPostFrontMatterAuthor)[];
export type BlogPostMetadata = {
export type BlogPostMetadata = LastUpdateData & {
/** Path to the Markdown source, with `@site` alias. */
readonly source: string;
/**
@ -190,11 +199,6 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
* into a string.
*/
readonly date: Date;
/**
* Publish date formatted according to the locale, so that the client can
* render the date regardless of the existence of `Intl.DateTimeFormat`.
*/
readonly formattedDate: string;
/** Full link including base URL. */
readonly permalink: string;
/**
@ -333,6 +337,11 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
defaultReadingTime: ReadingTimeFunction;
},
) => number | undefined;
export type ProcessBlogPostsFn = (params: {
blogPosts: BlogPost[];
}) => Promise<void | BlogPost[]>;
/**
* Plugin options after normalization.
*/
@ -351,10 +360,15 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
routeBasePath: string;
/**
* URL route for the tags section of your blog. Will be appended to
* `routeBasePath`. **DO NOT** include a trailing slash.
* `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.
@ -419,6 +433,14 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
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;
};
/**
@ -461,6 +483,13 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
blogTagsListPath: string;
};
export type BlogMetadata = {
/** the path to the base of the blog */
blogBasePath: string;
/** title of the overall blog */
blogTitle: string;
};
export type BlogTags = {
[permalink: string]: BlogTag;
};
@ -532,6 +561,7 @@ declare module '@theme/BlogPostPage' {
BlogPostFrontMatter,
BlogSidebar,
PropBlogPostContent,
BlogMetadata,
} from '@docusaurus/plugin-content-blog';
export type FrontMatter = BlogPostFrontMatter;
@ -543,6 +573,8 @@ declare module '@theme/BlogPostPage' {
readonly sidebar: BlogSidebar;
/** Content of this post as an MDX component, with useful metadata. */
readonly content: Content;
/** Metadata about the blog. */
readonly blogMetadata: BlogMetadata;
}
export default function BlogPostPage(props: Props): JSX.Element;
@ -552,6 +584,10 @@ declare module '@theme/BlogPostPage/Metadata' {
export default function BlogPostPageMetadata(): JSX.Element;
}
declare module '@theme/BlogPostPage/StructuredData' {
export default function BlogPostStructuredData(): JSX.Element;
}
declare module '@theme/BlogListPage' {
import type {Content} from '@theme/BlogPostPage';
import type {
@ -574,6 +610,28 @@ declare module '@theme/BlogListPage' {
export default function BlogListPage(props: Props): JSX.Element;
}
declare module '@theme/BlogListPage/StructuredData' {
import type {Content} from '@theme/BlogPostPage';
import type {
BlogSidebar,
BlogPaginatedMetadata,
} from '@docusaurus/plugin-content-blog';
export interface Props {
/** Blog sidebar. */
readonly sidebar: BlogSidebar;
/** Metadata of the current listing page. */
readonly metadata: BlogPaginatedMetadata;
/**
* Array of blog posts included on this page. Every post's metadata is also
* available.
*/
readonly items: readonly {readonly content: Content}[];
}
export default function BlogListPageStructuredData(props: Props): JSX.Element;
}
declare module '@theme/BlogTagsListPage' {
import type {BlogSidebar} from '@docusaurus/plugin-content-blog';
import type {TagsListItem} from '@docusaurus/utils';

View File

@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"noEmit": false,
"composite": true,
"incremental": true,
"tsBuildInfoFile": "./lib/.tsbuildinfo-client",
"moduleResolution": "bundler",
"module": "esnext",
"target": "esnext",
"rootDir": "src",
"outDir": "lib"
},
"include": ["src/client", "src/*.d.ts"],
"exclude": ["**/__tests__/**"]
}

View File

@ -1,5 +1,6 @@
{
"extends": "../../tsconfig.json",
"references": [{"path": "./tsconfig.client.json"}],
"compilerOptions": {
"noEmit": false,
"incremental": true,
@ -8,5 +9,5 @@
"outDir": "lib"
},
"include": ["src"],
"exclude": ["**/__tests__/**"]
"exclude": ["src/client", "**/__tests__/**"]
}

View File

@ -1,6 +1,6 @@
{
"name": "@docusaurus/plugin-content-docs",
"version": "3.0.0",
"version": "3.2.0",
"description": "Docs plugin for Docusaurus.",
"main": "lib/index.js",
"sideEffects": false,
@ -35,13 +35,14 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.0.0",
"@docusaurus/logger": "3.0.0",
"@docusaurus/mdx-loader": "3.0.0",
"@docusaurus/module-type-aliases": "3.0.0",
"@docusaurus/types": "3.0.0",
"@docusaurus/utils": "3.0.0",
"@docusaurus/utils-validation": "3.0.0",
"@docusaurus/core": "3.2.0",
"@docusaurus/logger": "3.2.0",
"@docusaurus/mdx-loader": "3.2.0",
"@docusaurus/module-type-aliases": "3.2.0",
"@docusaurus/types": "3.2.0",
"@docusaurus/utils": "3.2.0",
"@docusaurus/utils-common": "3.2.0",
"@docusaurus/utils-validation": "3.2.0",
"@types/react-router-config": "^5.0.7",
"combine-promises": "^1.1.0",
"fs-extra": "^11.1.1",

View File

@ -52,7 +52,6 @@ exports[`simple website content 1`] = `
"description": "Images",
"draft": false,
"editUrl": undefined,
"formattedLastUpdatedAt": undefined,
"frontMatter": {
"id": "baz",
"pagination_label": "baz pagination_label",
@ -105,7 +104,6 @@ exports[`simple website content 2`] = `
"description": "Hi, Endilie here :)",
"draft": false,
"editUrl": undefined,
"formattedLastUpdatedAt": undefined,
"frontMatter": {
"id": "hello",
"sidebar_label": "Hello sidebar_label",
@ -151,7 +149,6 @@ exports[`simple website content 3`] = `
"description": "This is custom description",
"draft": false,
"editUrl": undefined,
"formattedLastUpdatedAt": undefined,
"frontMatter": {
"description": "This is custom description",
"id": "bar",
@ -1485,6 +1482,10 @@ exports[`simple website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "docs/hello.md",
},
"modules": {
"content": "@site/docs/hello.md",
},
@ -1494,6 +1495,10 @@ exports[`simple website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "docs/slugs/absoluteSlug.md",
},
"modules": {
"content": "@site/docs/slugs/absoluteSlug.md",
},
@ -1511,6 +1516,10 @@ exports[`simple website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "docs/customLastUpdate.md",
},
"modules": {
"content": "@site/docs/customLastUpdate.md",
},
@ -1519,6 +1528,10 @@ exports[`simple website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "docs/doc with space.md",
},
"modules": {
"content": "@site/docs/doc with space.md",
},
@ -1527,6 +1540,10 @@ exports[`simple website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "docs/doc-draft.md",
},
"modules": {
"content": "@site/docs/doc-draft.md",
},
@ -1535,6 +1552,10 @@ exports[`simple website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "docs/doc-unlisted.md",
},
"modules": {
"content": "@site/docs/doc-unlisted.md",
},
@ -1544,6 +1565,10 @@ exports[`simple website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "docs/foo/bar.md",
},
"modules": {
"content": "@site/docs/foo/bar.md",
},
@ -1553,6 +1578,10 @@ exports[`simple website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "docs/foo/baz.md",
},
"modules": {
"content": "@site/docs/foo/baz.md",
},
@ -1562,6 +1591,10 @@ exports[`simple website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "docs/headingAsTitle.md",
},
"modules": {
"content": "@site/docs/headingAsTitle.md",
},
@ -1571,6 +1604,10 @@ exports[`simple website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "docs/rootResolvedSlug.md",
},
"modules": {
"content": "@site/docs/rootResolvedSlug.md",
},
@ -1580,6 +1617,10 @@ exports[`simple website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "docs/ipsum.md",
},
"modules": {
"content": "@site/docs/ipsum.md",
},
@ -1588,6 +1629,10 @@ exports[`simple website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "docs/lastUpdateAuthorOnly.md",
},
"modules": {
"content": "@site/docs/lastUpdateAuthorOnly.md",
},
@ -1596,6 +1641,10 @@ exports[`simple website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "docs/lastUpdateDateOnly.md",
},
"modules": {
"content": "@site/docs/lastUpdateDateOnly.md",
},
@ -1604,6 +1653,10 @@ exports[`simple website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "docs/lorem.md",
},
"modules": {
"content": "@site/docs/lorem.md",
},
@ -1612,6 +1665,10 @@ exports[`simple website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "docs/rootAbsoluteSlug.md",
},
"modules": {
"content": "@site/docs/rootAbsoluteSlug.md",
},
@ -1621,6 +1678,10 @@ exports[`simple website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "docs/rootRelativeSlug.md",
},
"modules": {
"content": "@site/docs/rootRelativeSlug.md",
},
@ -1630,6 +1691,10 @@ exports[`simple website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "docs/rootTryToEscapeSlug.md",
},
"modules": {
"content": "@site/docs/rootTryToEscapeSlug.md",
},
@ -1639,6 +1704,10 @@ exports[`simple website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "docs/slugs/resolvedSlug.md",
},
"modules": {
"content": "@site/docs/slugs/resolvedSlug.md",
},
@ -1647,6 +1716,10 @@ exports[`simple website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "docs/slugs/relativeSlug.md",
},
"modules": {
"content": "@site/docs/slugs/relativeSlug.md",
},
@ -1655,6 +1728,10 @@ exports[`simple website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "docs/slugs/tryToEscapeSlug.md",
},
"modules": {
"content": "@site/docs/slugs/tryToEscapeSlug.md",
},
@ -1663,6 +1740,10 @@ exports[`simple website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "docs/unlisted-category/index.md",
},
"modules": {
"content": "@site/docs/unlisted-category/index.md",
},
@ -1672,6 +1753,10 @@ exports[`simple website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "docs/unlisted-category/unlisted-category-doc.md",
},
"modules": {
"content": "@site/docs/unlisted-category/unlisted-category-doc.md",
},
@ -1971,7 +2056,6 @@ exports[`site with full autogenerated sidebar docs in fully generated sidebar ha
"description": "Getting started text",
"draft": false,
"editUrl": undefined,
"formattedLastUpdatedAt": undefined,
"frontMatter": {},
"id": "getting-started",
"lastUpdatedAt": undefined,
@ -1999,7 +2083,6 @@ exports[`site with full autogenerated sidebar docs in fully generated sidebar ha
"description": "Installation text",
"draft": false,
"editUrl": undefined,
"formattedLastUpdatedAt": undefined,
"frontMatter": {},
"id": "installation",
"lastUpdatedAt": undefined,
@ -2030,7 +2113,6 @@ exports[`site with full autogenerated sidebar docs in fully generated sidebar ha
"description": "Guide 1 text",
"draft": false,
"editUrl": undefined,
"formattedLastUpdatedAt": undefined,
"frontMatter": {
"id": "guide1",
"sidebar_position": 1,
@ -2064,7 +2146,6 @@ exports[`site with full autogenerated sidebar docs in fully generated sidebar ha
"description": "Guide 2 text",
"draft": false,
"editUrl": undefined,
"formattedLastUpdatedAt": undefined,
"frontMatter": {
"id": "guide2",
},
@ -2097,7 +2178,6 @@ exports[`site with full autogenerated sidebar docs in fully generated sidebar ha
"description": "Guide 2.5 text",
"draft": false,
"editUrl": undefined,
"formattedLastUpdatedAt": undefined,
"frontMatter": {
"id": "guide2.5",
"sidebar_position": 2.5,
@ -2131,7 +2211,6 @@ exports[`site with full autogenerated sidebar docs in fully generated sidebar ha
"description": "Guide 3 text",
"draft": false,
"editUrl": undefined,
"formattedLastUpdatedAt": undefined,
"frontMatter": {
"id": "guide3",
"sidebar_position": 3,
@ -2165,7 +2244,6 @@ exports[`site with full autogenerated sidebar docs in fully generated sidebar ha
"description": "Guide 4 text",
"draft": false,
"editUrl": undefined,
"formattedLastUpdatedAt": undefined,
"frontMatter": {
"id": "guide4",
},
@ -2198,7 +2276,6 @@ exports[`site with full autogenerated sidebar docs in fully generated sidebar ha
"description": "Guide 5 text",
"draft": false,
"editUrl": undefined,
"formattedLastUpdatedAt": undefined,
"frontMatter": {
"id": "guide5",
},
@ -2231,7 +2308,6 @@ exports[`site with full autogenerated sidebar docs in fully generated sidebar ha
"description": "API Overview text",
"draft": false,
"editUrl": undefined,
"formattedLastUpdatedAt": undefined,
"frontMatter": {},
"id": "API/api-overview",
"lastUpdatedAt": undefined,
@ -2262,7 +2338,6 @@ exports[`site with full autogenerated sidebar docs in fully generated sidebar ha
"description": "Client API text",
"draft": false,
"editUrl": undefined,
"formattedLastUpdatedAt": undefined,
"frontMatter": {},
"id": "API/Core APIs/Client API",
"lastUpdatedAt": undefined,
@ -2293,7 +2368,6 @@ exports[`site with full autogenerated sidebar docs in fully generated sidebar ha
"description": "Server API text",
"draft": false,
"editUrl": undefined,
"formattedLastUpdatedAt": undefined,
"frontMatter": {},
"id": "API/Core APIs/Server API",
"lastUpdatedAt": undefined,
@ -2324,7 +2398,6 @@ exports[`site with full autogenerated sidebar docs in fully generated sidebar ha
"description": "Plugin API text",
"draft": false,
"editUrl": undefined,
"formattedLastUpdatedAt": undefined,
"frontMatter": {},
"id": "API/Extension APIs/Plugin API",
"lastUpdatedAt": undefined,
@ -2355,7 +2428,6 @@ exports[`site with full autogenerated sidebar docs in fully generated sidebar ha
"description": "Theme API text",
"draft": false,
"editUrl": undefined,
"formattedLastUpdatedAt": undefined,
"frontMatter": {},
"id": "API/Extension APIs/Theme API",
"lastUpdatedAt": undefined,
@ -2386,7 +2458,6 @@ exports[`site with full autogenerated sidebar docs in fully generated sidebar ha
"description": "API End text",
"draft": false,
"editUrl": undefined,
"formattedLastUpdatedAt": undefined,
"frontMatter": {},
"id": "API/api-end",
"lastUpdatedAt": undefined,
@ -2566,7 +2637,6 @@ exports[`site with partial autogenerated sidebars docs in partially generated si
"description": "API End text",
"draft": false,
"editUrl": undefined,
"formattedLastUpdatedAt": undefined,
"frontMatter": {},
"id": "API/api-end",
"lastUpdatedAt": undefined,
@ -2594,7 +2664,6 @@ exports[`site with partial autogenerated sidebars docs in partially generated si
"description": "API Overview text",
"draft": false,
"editUrl": undefined,
"formattedLastUpdatedAt": undefined,
"frontMatter": {},
"id": "API/api-overview",
"lastUpdatedAt": undefined,
@ -2625,7 +2694,6 @@ exports[`site with partial autogenerated sidebars docs in partially generated si
"description": "Plugin API text",
"draft": false,
"editUrl": undefined,
"formattedLastUpdatedAt": undefined,
"frontMatter": {},
"id": "API/Extension APIs/Plugin API",
"lastUpdatedAt": undefined,
@ -2656,7 +2724,6 @@ exports[`site with partial autogenerated sidebars docs in partially generated si
"description": "Theme API text",
"draft": false,
"editUrl": undefined,
"formattedLastUpdatedAt": undefined,
"frontMatter": {},
"id": "API/Extension APIs/Theme API",
"lastUpdatedAt": undefined,
@ -2716,7 +2783,6 @@ exports[`versioned website (community) content 1`] = `
"description": "Team current version (translated)",
"draft": false,
"editUrl": undefined,
"formattedLastUpdatedAt": undefined,
"frontMatter": {
"title": "Team title translated",
},
@ -2743,7 +2809,6 @@ exports[`versioned website (community) content 2`] = `
"description": "Team 1.0.0",
"draft": false,
"editUrl": undefined,
"formattedLastUpdatedAt": undefined,
"frontMatter": {},
"id": "team",
"lastUpdatedAt": undefined,
@ -2963,6 +3028,10 @@ exports[`versioned website (community) content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "i18n/en/docusaurus-plugin-content-docs-community/current/team.md",
},
"modules": {
"content": "@site/i18n/en/docusaurus-plugin-content-docs-community/current/team.md",
},
@ -2990,6 +3059,10 @@ exports[`versioned website (community) content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "community_versioned_docs/version-1.0.0/team.md",
},
"modules": {
"content": "@site/community_versioned_docs/version-1.0.0/team.md",
},
@ -3023,7 +3096,6 @@ exports[`versioned website content 1`] = `
"description": "This is next version of bar.",
"draft": false,
"editUrl": undefined,
"formattedLastUpdatedAt": undefined,
"frontMatter": {
"slug": "barSlug",
"tags": [
@ -3074,7 +3146,6 @@ exports[`versioned website content 2`] = `
"description": "Bar 1.0.1 !",
"draft": false,
"editUrl": undefined,
"formattedLastUpdatedAt": undefined,
"frontMatter": {},
"id": "foo/bar",
"lastUpdatedAt": undefined,
@ -3102,7 +3173,6 @@ exports[`versioned website content 3`] = `
"description": "Hello next !",
"draft": false,
"editUrl": undefined,
"formattedLastUpdatedAt": undefined,
"frontMatter": {
"slug": "/",
},
@ -3132,7 +3202,6 @@ exports[`versioned website content 4`] = `
"description": "Hello 1.0.1 !",
"draft": false,
"editUrl": undefined,
"formattedLastUpdatedAt": undefined,
"frontMatter": {
"slug": "/",
},
@ -3162,7 +3231,6 @@ exports[`versioned website content 5`] = `
"description": "Baz 1.0.0 ! This will be deleted in next subsequent versions.",
"draft": false,
"editUrl": undefined,
"formattedLastUpdatedAt": undefined,
"frontMatter": {},
"id": "foo/baz",
"lastUpdatedAt": undefined,
@ -4202,6 +4270,10 @@ exports[`versioned website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md",
},
"modules": {
"content": "@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md",
},
@ -4211,6 +4283,10 @@ exports[`versioned website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "versioned_docs/version-1.0.0/foo/bar.md",
},
"modules": {
"content": "@site/versioned_docs/version-1.0.0/foo/bar.md",
},
@ -4220,6 +4296,10 @@ exports[`versioned website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "versioned_docs/version-1.0.0/foo/baz.md",
},
"modules": {
"content": "@site/versioned_docs/version-1.0.0/foo/baz.md",
},
@ -4279,6 +4359,10 @@ exports[`versioned website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "docs/hello.md",
},
"modules": {
"content": "@site/docs/hello.md",
},
@ -4288,6 +4372,10 @@ exports[`versioned website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "docs/slugs/absoluteSlug.md",
},
"modules": {
"content": "@site/docs/slugs/absoluteSlug.md",
},
@ -4296,6 +4384,10 @@ exports[`versioned website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "docs/foo/bar.md",
},
"modules": {
"content": "@site/docs/foo/bar.md",
},
@ -4305,6 +4397,10 @@ exports[`versioned website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "docs/slugs/resolvedSlug.md",
},
"modules": {
"content": "@site/docs/slugs/resolvedSlug.md",
},
@ -4313,6 +4409,10 @@ exports[`versioned website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "docs/slugs/relativeSlug.md",
},
"modules": {
"content": "@site/docs/slugs/relativeSlug.md",
},
@ -4321,6 +4421,10 @@ exports[`versioned website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "docs/slugs/tryToEscapeSlug.md",
},
"modules": {
"content": "@site/docs/slugs/tryToEscapeSlug.md",
},
@ -4347,6 +4451,10 @@ exports[`versioned website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "versioned_docs/version-withSlugs/slugs/absoluteSlug.md",
},
"modules": {
"content": "@site/versioned_docs/version-withSlugs/slugs/absoluteSlug.md",
},
@ -4355,6 +4463,10 @@ exports[`versioned website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "versioned_docs/version-withSlugs/rootResolvedSlug.md",
},
"modules": {
"content": "@site/versioned_docs/version-withSlugs/rootResolvedSlug.md",
},
@ -4363,6 +4475,10 @@ exports[`versioned website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "versioned_docs/version-withSlugs/rootAbsoluteSlug.md",
},
"modules": {
"content": "@site/versioned_docs/version-withSlugs/rootAbsoluteSlug.md",
},
@ -4372,6 +4488,10 @@ exports[`versioned website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "versioned_docs/version-withSlugs/rootRelativeSlug.md",
},
"modules": {
"content": "@site/versioned_docs/version-withSlugs/rootRelativeSlug.md",
},
@ -4380,6 +4500,10 @@ exports[`versioned website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "versioned_docs/version-withSlugs/rootTryToEscapeSlug.md",
},
"modules": {
"content": "@site/versioned_docs/version-withSlugs/rootTryToEscapeSlug.md",
},
@ -4388,6 +4512,10 @@ exports[`versioned website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "versioned_docs/version-withSlugs/slugs/resolvedSlug.md",
},
"modules": {
"content": "@site/versioned_docs/version-withSlugs/slugs/resolvedSlug.md",
},
@ -4396,6 +4524,10 @@ exports[`versioned website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "versioned_docs/version-withSlugs/slugs/relativeSlug.md",
},
"modules": {
"content": "@site/versioned_docs/version-withSlugs/slugs/relativeSlug.md",
},
@ -4404,6 +4536,10 @@ exports[`versioned website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "versioned_docs/version-withSlugs/slugs/tryToEscapeSlug.md",
},
"modules": {
"content": "@site/versioned_docs/version-withSlugs/slugs/tryToEscapeSlug.md",
},
@ -4430,6 +4566,10 @@ exports[`versioned website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "versioned_docs/version-1.0.1/hello.md",
},
"modules": {
"content": "@site/versioned_docs/version-1.0.1/hello.md",
},
@ -4439,6 +4579,10 @@ exports[`versioned website content: route config 1`] = `
{
"component": "@theme/DocItem",
"exact": true,
"metadata": {
"lastUpdatedAt": undefined,
"sourceFilePath": "versioned_docs/version-1.0.1/foo/bar.md",
},
"modules": {
"content": "@site/versioned_docs/version-1.0.1/foo/bar.md",
},

View File

@ -7,8 +7,13 @@
import {jest} from '@jest/globals';
import path from 'path';
import {loadContext} from '@docusaurus/core/src/server/index';
import {createSlugger, posixPath, DEFAULT_PLUGIN_ID} from '@docusaurus/utils';
import {loadContext} from '@docusaurus/core/src/server/site';
import {
createSlugger,
posixPath,
DEFAULT_PLUGIN_ID,
LAST_UPDATE_FALLBACK,
} from '@docusaurus/utils';
import {createSidebarsUtils} from '../sidebars/utils';
import {
processDocMetadata,
@ -474,9 +479,8 @@ describe('simple site', () => {
custom_edit_url: 'https://github.com/customUrl/docs/lorem.md',
unrelated_front_matter: "won't be part of metadata",
},
lastUpdatedAt: 1539502055,
formattedLastUpdatedAt: 'Oct 14, 2018',
lastUpdatedBy: 'Author',
lastUpdatedAt: LAST_UPDATE_FALLBACK.lastUpdatedAt,
lastUpdatedBy: LAST_UPDATE_FALLBACK.lastUpdatedBy,
tags: [],
unlisted: false,
});
@ -572,8 +576,7 @@ describe('simple site', () => {
},
title: 'Custom Last Update',
},
lastUpdatedAt: new Date('1/1/2000').getTime() / 1000,
formattedLastUpdatedAt: 'Jan 1, 2000',
lastUpdatedAt: new Date('1/1/2000').getTime(),
lastUpdatedBy: 'Custom Author (processed by parseFrontMatter)',
sidebarPosition: undefined,
tags: [],
@ -611,8 +614,7 @@ describe('simple site', () => {
},
title: 'Last Update Author Only',
},
lastUpdatedAt: 1539502055,
formattedLastUpdatedAt: 'Oct 14, 2018',
lastUpdatedAt: LAST_UPDATE_FALLBACK.lastUpdatedAt,
lastUpdatedBy: 'Custom Author (processed by parseFrontMatter)',
sidebarPosition: undefined,
tags: [],
@ -650,8 +652,7 @@ describe('simple site', () => {
},
title: 'Last Update Date Only',
},
lastUpdatedAt: new Date('1/1/2000').getTime() / 1000,
formattedLastUpdatedAt: 'Jan 1, 2000',
lastUpdatedAt: new Date('1/1/2000').getTime(),
lastUpdatedBy: 'Author',
sidebarPosition: undefined,
tags: [],
@ -691,7 +692,6 @@ describe('simple site', () => {
title: 'Custom Last Update',
},
lastUpdatedAt: undefined,
formattedLastUpdatedAt: undefined,
lastUpdatedBy: undefined,
sidebarPosition: undefined,
tags: [],

View File

@ -444,19 +444,19 @@ describe('validateDocFrontMatter last_update', () => {
invalidFrontMatters: [
[
{last_update: null},
'does not look like a valid front matter FileChange object. Please use a FileChange object (with an author and/or date).',
'"last_update" does not look like a valid last update object. Please use an author key with a string or a date with a string or Date',
],
[
{last_update: {}},
'does not look like a valid front matter FileChange object. Please use a FileChange object (with an author and/or date).',
'"last_update" does not look like a valid last update object. Please use an author key with a string or a date with a string or Date',
],
[
{last_update: ''},
'does not look like a valid front matter FileChange object. Please use a FileChange object (with an author and/or date).',
'"last_update" does not look like a valid last update object. Please use an author key with a string or a date with a string or Date',
],
[
{last_update: {invalid: 'key'}},
'does not look like a valid front matter FileChange object. Please use a FileChange object (with an author and/or date).',
'"last_update" does not look like a valid last update object. Please use an author key with a string or a date with a string or Date',
],
[
{last_update: {author: 'test author', date: 'I am not a date :('}},

View File

@ -12,9 +12,9 @@ import _ from 'lodash';
import {isMatch} from 'picomatch';
import commander from 'commander';
import webpack from 'webpack';
import {loadContext} from '@docusaurus/core/src/server/index';
import {loadContext} from '@docusaurus/core/src/server/site';
import {applyConfigureWebpack} from '@docusaurus/core/src/webpack/utils';
import {sortConfig} from '@docusaurus/core/src/server/plugins/routeConfig';
import {sortRoutes} from '@docusaurus/core/src/server/plugins/routeConfig';
import {posixPath} from '@docusaurus/utils';
import {normalizePluginOptions} from '@docusaurus/utils-validation';
@ -109,7 +109,7 @@ Entries created:
expectSnapshot: () => {
// Sort the route config like in src/server/plugins/index.ts for
// consistent snapshot ordering
sortConfig(routeConfigs);
sortRoutes(routeConfigs);
expect(routeConfigs).not.toEqual([]);
expect(routeConfigs).toMatchSnapshot('route config');
expect(dataContainer).toMatchSnapshot('data');

View File

@ -1,117 +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 {jest} from '@jest/globals';
import fs from 'fs-extra';
import path from 'path';
import shell from 'shelljs';
import {createTempRepo} from '@testing-utils/git';
import {getFileLastUpdate} from '../lastUpdate';
describe('getFileLastUpdate', () => {
const existingFilePath = path.join(
__dirname,
'__fixtures__/simple-site/docs/hello.md',
);
it('existing test file in repository with Git timestamp', async () => {
const lastUpdateData = await getFileLastUpdate(existingFilePath);
expect(lastUpdateData).not.toBeNull();
const {author, timestamp} = lastUpdateData!;
expect(author).not.toBeNull();
expect(typeof author).toBe('string');
expect(timestamp).not.toBeNull();
expect(typeof timestamp).toBe('number');
});
it('existing test file with spaces in path', async () => {
const filePathWithSpace = path.join(
__dirname,
'__fixtures__/simple-site/docs/doc with space.md',
);
const lastUpdateData = await getFileLastUpdate(filePathWithSpace);
expect(lastUpdateData).not.toBeNull();
const {author, timestamp} = lastUpdateData!;
expect(author).not.toBeNull();
expect(typeof author).toBe('string');
expect(timestamp).not.toBeNull();
expect(typeof timestamp).toBe('number');
});
it('non-existing file', async () => {
const consoleMock = jest
.spyOn(console, 'warn')
.mockImplementation(() => {});
const nonExistingFileName = '.nonExisting';
const nonExistingFilePath = path.join(
__dirname,
'__fixtures__',
nonExistingFileName,
);
await expect(getFileLastUpdate(nonExistingFilePath)).resolves.toBeNull();
expect(consoleMock).toHaveBeenCalledTimes(1);
expect(consoleMock).toHaveBeenLastCalledWith(
expect.stringMatching(/because the file does not exist./),
);
consoleMock.mockRestore();
});
it('temporary created file that is not tracked by git', async () => {
const consoleMock = jest
.spyOn(console, 'warn')
.mockImplementation(() => {});
const {repoDir} = createTempRepo();
const tempFilePath = path.join(repoDir, 'file.md');
await fs.writeFile(tempFilePath, 'Lorem ipsum :)');
await expect(getFileLastUpdate(tempFilePath)).resolves.toBeNull();
expect(consoleMock).toHaveBeenCalledTimes(1);
expect(consoleMock).toHaveBeenLastCalledWith(
expect.stringMatching(/not tracked by git./),
);
await fs.unlink(tempFilePath);
});
it('multiple files not tracked by git', async () => {
const consoleMock = jest
.spyOn(console, 'warn')
.mockImplementation(() => {});
const {repoDir} = createTempRepo();
const tempFilePath1 = path.join(repoDir, 'file1.md');
const tempFilePath2 = path.join(repoDir, 'file2.md');
await fs.writeFile(tempFilePath1, 'Lorem ipsum :)');
await fs.writeFile(tempFilePath2, 'Lorem ipsum :)');
await expect(getFileLastUpdate(tempFilePath1)).resolves.toBeNull();
await expect(getFileLastUpdate(tempFilePath2)).resolves.toBeNull();
expect(consoleMock).toHaveBeenCalledTimes(1);
expect(consoleMock).toHaveBeenLastCalledWith(
expect.stringMatching(/not tracked by git./),
);
await fs.unlink(tempFilePath1);
await fs.unlink(tempFilePath2);
});
it('git does not exist', async () => {
const mock = jest.spyOn(shell, 'which').mockImplementationOnce(() => null);
const consoleMock = jest
.spyOn(console, 'warn')
.mockImplementation(() => {});
const lastUpdateData = await getFileLastUpdate(existingFilePath);
expect(lastUpdateData).toBeNull();
expect(consoleMock).toHaveBeenLastCalledWith(
expect.stringMatching(
/.*\[WARNING\].* Sorry, the docs plugin last update options require Git\..*/,
),
);
consoleMock.mockRestore();
mock.mockRestore();
});
});

View File

@ -86,10 +86,20 @@ export const useAllDocsData = (): {[pluginId: string]: GlobalPluginData} =>
}
| undefined) ?? StableEmptyObject;
export const useDocsData = (pluginId: string | undefined): GlobalPluginData =>
usePluginData('docusaurus-plugin-content-docs', pluginId, {
failfast: true,
}) as GlobalPluginData;
export const useDocsData = (pluginId: string | undefined): GlobalPluginData => {
try {
return usePluginData('docusaurus-plugin-content-docs', pluginId, {
failfast: true,
}) as GlobalPluginData;
} catch (error) {
throw new Error(
`You are using a feature of the Docusaurus docs plugin, but this plugin does not seem to be enabled${
pluginId === 'Default' ? '' : ` (pluginId=${pluginId}`
}`,
{cause: error as Error},
);
}
};
// TODO this feature should be provided by docusaurus core
export function useActivePlugin(

View File

@ -8,7 +8,6 @@
import path from 'path';
import fs from 'fs-extra';
import _ from 'lodash';
import logger from '@docusaurus/logger';
import {
aliasedSitePath,
getEditUrl,
@ -21,12 +20,11 @@ import {
normalizeFrontMatterTags,
isUnlisted,
isDraft,
readLastUpdateData,
} from '@docusaurus/utils';
import {getFileLastUpdate} from './lastUpdate';
import {validateDocFrontMatter} from './frontMatter';
import getSlug from './slug';
import {stripPathNumberPrefixes} from './numberPrefix';
import {validateDocFrontMatter} from './frontMatter';
import {toDocNavigationLink, toNavigationLink} from './sidebars/utils';
import type {
MetadataOptions,
@ -35,61 +33,13 @@ import type {
DocMetadataBase,
DocMetadata,
PropNavigationLink,
LastUpdateData,
VersionMetadata,
LoadedVersion,
FileChange,
} from '@docusaurus/plugin-content-docs';
import type {LoadContext} from '@docusaurus/types';
import type {SidebarsUtils} from './sidebars/utils';
import type {DocFile} from './types';
type LastUpdateOptions = Pick<
PluginOptions,
'showLastUpdateAuthor' | 'showLastUpdateTime'
>;
async function readLastUpdateData(
filePath: string,
options: LastUpdateOptions,
lastUpdateFrontMatter: FileChange | undefined,
): Promise<LastUpdateData> {
const {showLastUpdateAuthor, showLastUpdateTime} = options;
if (showLastUpdateAuthor || showLastUpdateTime) {
const frontMatterTimestamp = lastUpdateFrontMatter?.date
? new Date(lastUpdateFrontMatter.date).getTime() / 1000
: undefined;
if (lastUpdateFrontMatter?.author && lastUpdateFrontMatter.date) {
return {
lastUpdatedAt: frontMatterTimestamp,
lastUpdatedBy: lastUpdateFrontMatter.author,
};
}
// Use fake data in dev for faster development.
const fileLastUpdateData =
process.env.NODE_ENV === 'production'
? await getFileLastUpdate(filePath)
: {
author: 'Author',
timestamp: 1539502055,
};
const {author, timestamp} = fileLastUpdateData ?? {};
return {
lastUpdatedBy: showLastUpdateAuthor
? lastUpdateFrontMatter?.author ?? author
: undefined,
lastUpdatedAt: showLastUpdateTime
? frontMatterTimestamp ?? timestamp
: undefined,
};
}
return {};
}
export async function readDocFile(
versionMetadata: Pick<
VersionMetadata,
@ -142,7 +92,6 @@ async function doProcessDocMetadata({
const {source, content, contentPath, filePath} = docFile;
const {
siteDir,
i18n,
siteConfig: {
markdown: {parseFrontMatter},
},
@ -257,21 +206,6 @@ async function doProcessDocMetadata({
const draft = isDraft({env, frontMatter});
const unlisted = isUnlisted({env, frontMatter});
const formatDate = (locale: string, date: Date, calendar: string): string => {
try {
return new Intl.DateTimeFormat(locale, {
day: 'numeric',
month: 'short',
year: 'numeric',
timeZone: 'UTC',
calendar,
}).format(date);
} catch (err) {
logger.error`Can't format docs lastUpdatedAt date "${String(date)}"`;
throw err;
}
};
// Assign all of object properties during instantiation (if possible) for
// NodeJS optimization.
// Adding properties to object after instantiation will cause hidden
@ -291,13 +225,6 @@ async function doProcessDocMetadata({
version: versionMetadata.versionName,
lastUpdatedBy: lastUpdate.lastUpdatedBy,
lastUpdatedAt: lastUpdate.lastUpdatedAt,
formattedLastUpdatedAt: lastUpdate.lastUpdatedAt
? formatDate(
i18n.currentLocale,
new Date(lastUpdate.lastUpdatedAt * 1000),
i18n.localeConfigs[i18n.currentLocale]!.calendar,
)
: undefined,
sidebarPosition,
frontMatter,
};

View File

@ -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.
*/
import {
JoiFrontMatter as Joi, // Custom instance for front matter
URISchema,
@ -12,17 +11,15 @@ import {
FrontMatterTOCHeadingLevels,
validateFrontMatter,
ContentVisibilitySchema,
FrontMatterLastUpdateSchema,
} from '@docusaurus/utils-validation';
import type {DocFrontMatter} from '@docusaurus/plugin-content-docs';
const FrontMatterLastUpdateErrorMessage =
'{{#label}} does not look like a valid front matter FileChange object. Please use a FileChange object (with an author and/or date).';
// NOTE: we don't add any default value on purpose here
// We don't want default values to magically appear in doc metadata and props
// While the user did not provide those values explicitly
// We use default values in code instead
const DocFrontMatterSchema = Joi.object<DocFrontMatter>({
export const DocFrontMatterSchema = Joi.object<DocFrontMatter>({
id: Joi.string(),
// See https://github.com/facebook/docusaurus/issues/4591#issuecomment-822372398
title: Joi.string().allow(''),
@ -45,15 +42,7 @@ const DocFrontMatterSchema = Joi.object<DocFrontMatter>({
pagination_next: Joi.string().allow(null),
pagination_prev: Joi.string().allow(null),
...FrontMatterTOCHeadingLevels,
last_update: Joi.object({
author: Joi.string(),
date: Joi.date().raw(),
})
.or('author', 'date')
.messages({
'object.missing': FrontMatterLastUpdateErrorMessage,
'object.base': FrontMatterLastUpdateErrorMessage,
}),
last_update: FrontMatterLastUpdateSchema,
})
.unknown()
.concat(ContentVisibilitySchema);

View File

@ -1,51 +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 logger from '@docusaurus/logger';
import {
getFileCommitDate,
FileNotTrackedError,
GitNotFoundError,
} from '@docusaurus/utils';
let showedGitRequirementError = false;
let showedFileNotTrackedError = false;
export async function getFileLastUpdate(
filePath: string,
): Promise<{timestamp: number; author: string} | null> {
if (!filePath) {
return null;
}
// Wrap in try/catch in case the shell commands fail
// (e.g. project doesn't use Git, etc).
try {
const result = getFileCommitDate(filePath, {
age: 'newest',
includeAuthor: true,
});
return {timestamp: result.timestamp, author: result.author};
} catch (err) {
if (err instanceof GitNotFoundError) {
if (!showedGitRequirementError) {
logger.warn('Sorry, the docs plugin last update options require Git.');
showedGitRequirementError = true;
}
} else if (err instanceof FileNotTrackedError) {
if (!showedFileNotTrackedError) {
logger.warn(
'Cannot infer the update date for some files, as they are not tracked by git.',
);
showedFileNotTrackedError = true;
}
} else {
logger.warn(err);
}
return null;
}
}

View File

@ -16,6 +16,8 @@ declare module '@docusaurus/plugin-content-docs' {
TagsListItem,
TagModule,
Tag,
FrontMatterLastUpdate,
LastUpdateData,
} from '@docusaurus/utils';
import type {Plugin, LoadContext} from '@docusaurus/types';
import type {Overwrite, Required} from 'utility-types';
@ -24,14 +26,6 @@ declare module '@docusaurus/plugin-content-docs' {
image?: string;
};
export type FileChange = {
author?: string;
/** Date can be any
* [parsable date string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse).
*/
date?: Date | string;
};
/**
* Custom callback for parsing number prefixes from file/folder names.
*/
@ -93,9 +87,9 @@ declare module '@docusaurus/plugin-content-docs' {
*/
editLocalizedFiles: boolean;
/** Whether to display the last date the doc was updated. */
showLastUpdateTime?: boolean;
showLastUpdateTime: boolean;
/** Whether to display the author who last updated the doc. */
showLastUpdateAuthor?: boolean;
showLastUpdateAuthor: boolean;
/**
* Custom parsing logic to extract number prefixes from file names. Use
* `false` to disable this behavior and leave the docs untouched, and `true`
@ -401,16 +395,7 @@ declare module '@docusaurus/plugin-content-docs' {
/** Should this doc be accessible but hidden in production builds? */
unlisted?: boolean;
/** Allows overriding the last updated author and/or date. */
last_update?: FileChange;
};
export type LastUpdateData = {
/** A timestamp in **seconds**, directly acquired from `git log`. */
lastUpdatedAt?: number;
/** `lastUpdatedAt` formatted as a date according to the current locale. */
formattedLastUpdatedAt?: string;
/** The author's name directly acquired from `git log`. */
lastUpdatedBy?: string;
last_update?: FrontMatterLastUpdate;
};
export type DocMetadataBase = LastUpdateData & {

View File

@ -7,21 +7,38 @@
import _ from 'lodash';
import logger from '@docusaurus/logger';
import {docuHash, createSlugger, normalizeUrl} from '@docusaurus/utils';
import {
docuHash,
createSlugger,
normalizeUrl,
aliasedSitePathToRelativePath,
} from '@docusaurus/utils';
import {
toTagDocListProp,
toTagsListTagsProp,
toVersionMetadataProp,
} from './props';
import {getVersionTags} from './tags';
import type {PluginContentLoadedActions, RouteConfig} from '@docusaurus/types';
import type {
PluginContentLoadedActions,
RouteConfig,
RouteMetadata,
} from '@docusaurus/types';
import type {FullVersion, VersionTag} from './types';
import type {
CategoryGeneratedIndexMetadata,
DocMetadata,
PluginOptions,
PropTagsListPage,
} from '@docusaurus/plugin-content-docs';
function createDocRouteMetadata(docMeta: DocMetadata): RouteMetadata {
return {
sourceFilePath: aliasedSitePathToRelativePath(docMeta.source),
lastUpdatedAt: docMeta.lastUpdatedAt,
};
}
async function buildVersionCategoryGeneratedIndexRoutes({
version,
actions,
@ -68,26 +85,27 @@ async function buildVersionDocRoutes({
options,
}: BuildVersionRoutesParam): Promise<RouteConfig[]> {
return Promise.all(
version.docs.map(async (metadataItem) => {
version.docs.map(async (doc) => {
await actions.createData(
// Note that this created data path must be in sync with
// metadataPath provided to mdx-loader.
`${docuHash(metadataItem.source)}.json`,
JSON.stringify(metadataItem, null, 2),
`${docuHash(doc.source)}.json`,
JSON.stringify(doc, null, 2),
);
const docRoute: RouteConfig = {
path: metadataItem.permalink,
path: doc.permalink,
component: options.docItemComponent,
exact: true,
modules: {
content: metadataItem.source,
content: doc.source,
},
metadata: createDocRouteMetadata(doc),
// Because the parent (DocRoot) comp need to access it easily
// This permits to render the sidebar once without unmount/remount when
// navigating (and preserve sidebar state)
...(metadataItem.sidebar && {
sidebar: metadataItem.sidebar,
...(doc.sidebar && {
sidebar: doc.sidebar,
}),
};

View File

@ -8,7 +8,7 @@
import path from 'path';
import _ from 'lodash';
import logger from '@docusaurus/logger';
import {addTrailingSlash} from '@docusaurus/utils';
import {addTrailingSlash} from '@docusaurus/utils-common';
import {createDocsByIdIndex, toCategoryIndexMatcherParam} from '../docs';
import type {
SidebarItemDoc,

View File

@ -5,12 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
import {
addLeadingSlash,
addTrailingSlash,
isValidPathname,
resolvePathname,
} from '@docusaurus/utils';
import {isValidPathname, resolvePathname} from '@docusaurus/utils';
import {addLeadingSlash, addTrailingSlash} from '@docusaurus/utils-common';
import {
DefaultNumberPrefixParser,
stripPathNumberPrefixes,

View File

@ -1,6 +1,6 @@
{
"name": "@docusaurus/plugin-content-pages",
"version": "3.0.0",
"version": "3.2.0",
"description": "Pages plugin for Docusaurus.",
"main": "lib/index.js",
"types": "src/plugin-content-pages.d.ts",
@ -18,11 +18,11 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.0.0",
"@docusaurus/mdx-loader": "3.0.0",
"@docusaurus/types": "3.0.0",
"@docusaurus/utils": "3.0.0",
"@docusaurus/utils-validation": "3.0.0",
"@docusaurus/core": "3.2.0",
"@docusaurus/mdx-loader": "3.2.0",
"@docusaurus/types": "3.2.0",
"@docusaurus/utils": "3.2.0",
"@docusaurus/utils-validation": "3.2.0",
"fs-extra": "^11.1.1",
"tslib": "^2.6.0",
"webpack": "^5.88.1"

View File

@ -6,7 +6,7 @@
*/
import path from 'path';
import {loadContext} from '@docusaurus/core/lib/server';
import {loadContext} from '@docusaurus/core/src/server/site';
import {normalizePluginOptions} from '@docusaurus/utils-validation';
import pluginContentPages from '../index';

View File

@ -11,6 +11,7 @@ import {
encodePath,
fileToPath,
aliasedSitePath,
aliasedSitePathToRelativePath,
docuHash,
getPluginI18nPath,
getFolderContainingFile,
@ -24,8 +25,7 @@ import {
isDraft,
} from '@docusaurus/utils';
import {validatePageFrontMatter} from './frontMatter';
import type {LoadContext, Plugin} from '@docusaurus/types';
import type {LoadContext, Plugin, RouteMetadata} from '@docusaurus/types';
import type {PagesContentPaths} from './types';
import type {
PluginOptions,
@ -159,9 +159,20 @@ export default function pluginContentPages(
const {addRoute, createData} = actions;
function createPageRouteMetadata(metadata: Metadata): RouteMetadata {
return {
sourceFilePath: aliasedSitePathToRelativePath(metadata.source),
// TODO add support for last updated date in the page plugin
// at least for Markdown files
// lastUpdatedAt: metadata.lastUpdatedAt,
lastUpdatedAt: undefined,
};
}
await Promise.all(
content.map(async (metadata) => {
const {permalink, source} = metadata;
const routeMetadata = createPageRouteMetadata(metadata);
if (metadata.type === 'mdx') {
await createData(
// Note that this created data path must be in sync with
@ -173,6 +184,7 @@ export default function pluginContentPages(
path: permalink,
component: options.mdxPageComponent,
exact: true,
metadata: routeMetadata,
modules: {
content: source,
},
@ -182,6 +194,7 @@ export default function pluginContentPages(
path: permalink,
component: source,
exact: true,
metadata: routeMetadata,
modules: {
config: `@generated/docusaurus.config`,
},

Some files were not shown because too many files have changed in this diff Show More