Compare commits
6 Commits
main
...
ozaki/util
| Author | SHA1 | Date |
|---|---|---|
|
|
32fa7e0a8d | |
|
|
5453bc517d | |
|
|
2470997ac1 | |
|
|
e09245915a | |
|
|
d9f0cbdb01 | |
|
|
50347c77e7 |
|
|
@ -91,7 +91,7 @@ module.exports = {
|
|||
'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,
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@
|
|||
|
||||
import _ from 'lodash';
|
||||
import logger from '@docusaurus/logger';
|
||||
import {addTrailingSlash, removeTrailingSlash} from '@docusaurus/utils';
|
||||
import {applyTrailingSlash} from '@docusaurus/utils-common';
|
||||
import {removeTrailingSlash} from '@docusaurus/utils';
|
||||
import {applyTrailingSlash, addTrailingSlash} from '@docusaurus/utils-common';
|
||||
import {
|
||||
createFromExtensionsRedirects,
|
||||
createToExtensionsRedirects,
|
||||
|
|
|
|||
|
|
@ -5,11 +5,8 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
addTrailingSlash,
|
||||
removeSuffix,
|
||||
removeTrailingSlash,
|
||||
} from '@docusaurus/utils';
|
||||
import {removeSuffix, removeTrailingSlash} from '@docusaurus/utils';
|
||||
import {addTrailingSlash} from '@docusaurus/utils-common';
|
||||
import type {RedirectItem} from './types';
|
||||
|
||||
const ExtensionAdditionalMessage =
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
title: Author
|
||||
slug: author
|
||||
author: ozaki
|
||||
last_update:
|
||||
author: seb
|
||||
---
|
||||
|
||||
author
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
title: Last update date
|
||||
slug: lastUpdateDate
|
||||
date: 2020-01-01
|
||||
last_update:
|
||||
date: 2021-01-01
|
||||
---
|
||||
|
||||
last update date
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: Nothing
|
||||
slug: nothing
|
||||
---
|
||||
|
||||
nothing
|
||||
|
|
@ -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>",
|
||||
]
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -137,6 +137,8 @@ exports[`blog plugin process blog posts load content 2`] = `
|
|||
"title": "Another Simple Slug",
|
||||
},
|
||||
"hasTruncateMarker": false,
|
||||
"lastUpdatedAt": undefined,
|
||||
"lastUpdatedBy": undefined,
|
||||
"nextItem": {
|
||||
"permalink": "/blog/another/tags",
|
||||
"title": "Another With Tag",
|
||||
|
|
@ -172,6 +174,8 @@ exports[`blog plugin process blog posts load content 2`] = `
|
|||
"title": "Another With Tag",
|
||||
},
|
||||
"hasTruncateMarker": false,
|
||||
"lastUpdatedAt": undefined,
|
||||
"lastUpdatedBy": undefined,
|
||||
"nextItem": {
|
||||
"permalink": "/blog/another/tags2",
|
||||
"title": "Another With Tag",
|
||||
|
|
@ -215,6 +219,8 @@ exports[`blog plugin process blog posts load content 2`] = `
|
|||
"title": "Another With Tag",
|
||||
},
|
||||
"hasTruncateMarker": false,
|
||||
"lastUpdatedAt": undefined,
|
||||
"lastUpdatedBy": undefined,
|
||||
"permalink": "/blog/another/tags2",
|
||||
"prevItem": {
|
||||
"permalink": "/blog/another/tags",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,12 @@
|
|||
import {jest} from '@jest/globals';
|
||||
import path from 'path';
|
||||
import {normalizePluginOptions} from '@docusaurus/utils-validation';
|
||||
import {posixPath, getFileCommitDate} from '@docusaurus/utils';
|
||||
import {
|
||||
posixPath,
|
||||
getFileCommitDate,
|
||||
GIT_FALLBACK_LAST_UPDATE_DATE,
|
||||
GIT_FALLBACK_LAST_UPDATE_AUTHOR,
|
||||
} from '@docusaurus/utils';
|
||||
import pluginContentBlog from '../index';
|
||||
import {validateOptions} from '../options';
|
||||
import type {
|
||||
|
|
@ -510,7 +515,7 @@ describe('blog plugin', () => {
|
|||
{
|
||||
postsPerPage: 1,
|
||||
processBlogPosts: async ({blogPosts}) =>
|
||||
blogPosts.filter((blog) => blog.metadata.tags[0].label === 'tag1'),
|
||||
blogPosts.filter((blog) => blog.metadata.tags[0]?.label === 'tag1'),
|
||||
},
|
||||
DefaultI18N,
|
||||
);
|
||||
|
|
@ -526,3 +531,137 @@ describe('blog plugin', () => {
|
|||
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() / 1000;
|
||||
|
||||
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(
|
||||
GIT_FALLBACK_LAST_UPDATE_DATE,
|
||||
);
|
||||
|
||||
expect(blogPosts[1]?.metadata.lastUpdatedBy).toBe(
|
||||
GIT_FALLBACK_LAST_UPDATE_AUTHOR,
|
||||
);
|
||||
expect(blogPosts[1]?.metadata.lastUpdatedAt).toBe(
|
||||
GIT_FALLBACK_LAST_UPDATE_DATE,
|
||||
);
|
||||
|
||||
expect(blogPosts[2]?.metadata.lastUpdatedBy).toBe('seb');
|
||||
expect(blogPosts[2]?.metadata.lastUpdatedAt).toBe(
|
||||
lastUpdateFor('2021-01-01'),
|
||||
);
|
||||
|
||||
expect(blogPosts[3]?.metadata.lastUpdatedBy).toBe(
|
||||
GIT_FALLBACK_LAST_UPDATE_AUTHOR,
|
||||
);
|
||||
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(
|
||||
GIT_FALLBACK_LAST_UPDATE_DATE,
|
||||
);
|
||||
|
||||
expect(blogPosts[1]?.metadata.title).toBe('Nothing');
|
||||
expect(blogPosts[1]?.metadata.lastUpdatedBy).toBeUndefined();
|
||||
expect(blogPosts[1]?.metadata.lastUpdatedAt).toBe(
|
||||
GIT_FALLBACK_LAST_UPDATE_DATE,
|
||||
);
|
||||
|
||||
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(
|
||||
GIT_FALLBACK_LAST_UPDATE_AUTHOR,
|
||||
);
|
||||
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(
|
||||
GIT_FALLBACK_LAST_UPDATE_AUTHOR,
|
||||
);
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import {
|
|||
getContentPathList,
|
||||
isUnlisted,
|
||||
isDraft,
|
||||
readLastUpdateData,
|
||||
} from '@docusaurus/utils';
|
||||
import {validateBlogPostFrontMatter} from './frontMatter';
|
||||
import {type AuthorsMap, getAuthorsMap, getBlogPostAuthors} from './authors';
|
||||
|
|
@ -231,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});
|
||||
|
||||
|
|
@ -337,6 +344,8 @@ async function processBlogSourceFile(
|
|||
authors,
|
||||
frontMatter,
|
||||
unlisted,
|
||||
lastUpdatedAt: lastUpdate.lastUpdatedAt,
|
||||
lastUpdatedBy: lastUpdate.lastUpdatedBy,
|
||||
},
|
||||
content,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ export const DEFAULT_OPTIONS: PluginOptions = {
|
|||
authorsMapPath: 'authors.yml',
|
||||
readingTime: ({content, defaultReadingTime}) => defaultReadingTime({content}),
|
||||
sortPosts: 'descending',
|
||||
showLastUpdateTime: false,
|
||||
showLastUpdateAuthor: false,
|
||||
processBlogPosts: async () => undefined,
|
||||
};
|
||||
|
||||
|
|
@ -135,6 +137,10 @@ 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),
|
||||
|
|
|
|||
|
|
@ -10,7 +10,12 @@
|
|||
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';
|
||||
|
|
@ -156,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 & {
|
||||
|
|
@ -180,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;
|
||||
/**
|
||||
|
|
@ -426,6 +433,10 @@ 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...).
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@
|
|||
"@docusaurus/module-type-aliases": "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",
|
||||
"@types/react-router-config": "^5.0.7",
|
||||
"combine-promises": "^1.1.0",
|
||||
|
|
|
|||
|
|
@ -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 :('}},
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -20,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,
|
||||
|
|
@ -34,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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -1,52 +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 = await 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ declare module '@docusaurus/plugin-content-docs' {
|
|||
TagsListItem,
|
||||
TagModule,
|
||||
Tag,
|
||||
FrontMatterLastUpdate,
|
||||
} from '@docusaurus/utils';
|
||||
import type {Plugin, LoadContext} from '@docusaurus/types';
|
||||
import type {Overwrite, Required} from 'utility-types';
|
||||
|
|
@ -24,14 +25,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 +86,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,7 +394,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;
|
||||
last_update?: FrontMatterLastUpdate;
|
||||
};
|
||||
|
||||
export type LastUpdateData = {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@
|
|||
|
||||
import {
|
||||
addLeadingSlash,
|
||||
addTrailingSlash,
|
||||
isValidPathname,
|
||||
resolvePathname,
|
||||
} from '@docusaurus/utils';
|
||||
import {addTrailingSlash} from '@docusaurus/utils-common';
|
||||
import {
|
||||
DefaultNumberPrefixParser,
|
||||
stripPathNumberPrefixes,
|
||||
|
|
|
|||
|
|
@ -676,6 +676,16 @@ declare module '@theme/DocVersionSuggestions' {
|
|||
export default function DocVersionSuggestions(): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/EditMetaRow' {
|
||||
export interface Props {
|
||||
readonly className: string;
|
||||
readonly editUrl: string | null | undefined;
|
||||
readonly lastUpdatedAt: number | undefined;
|
||||
readonly lastUpdatedBy: string | undefined;
|
||||
}
|
||||
export default function EditMetaRow(props: Props): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/EditThisPage' {
|
||||
export interface Props {
|
||||
readonly editUrl: string;
|
||||
|
|
|
|||
|
|
@ -8,15 +8,21 @@
|
|||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {useBlogPost} from '@docusaurus/theme-common/internal';
|
||||
import EditThisPage from '@theme/EditThisPage';
|
||||
import {ThemeClassNames} from '@docusaurus/theme-common';
|
||||
import EditMetaRow from '@theme/EditMetaRow';
|
||||
import TagsListInline from '@theme/TagsListInline';
|
||||
import ReadMoreLink from '@theme/BlogPostItem/Footer/ReadMoreLink';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
export default function BlogPostItemFooter(): JSX.Element | null {
|
||||
const {metadata, isBlogPostPage} = useBlogPost();
|
||||
const {tags, title, editUrl, hasTruncateMarker} = metadata;
|
||||
const {
|
||||
tags,
|
||||
title,
|
||||
editUrl,
|
||||
hasTruncateMarker,
|
||||
lastUpdatedBy,
|
||||
lastUpdatedAt,
|
||||
} = metadata;
|
||||
|
||||
// A post is truncated if it's in the "list view" and it has a truncate marker
|
||||
const truncatedPost = !isBlogPostPage && hasTruncateMarker;
|
||||
|
|
@ -29,32 +35,56 @@ export default function BlogPostItemFooter(): JSX.Element | null {
|
|||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<footer
|
||||
className={clsx(
|
||||
'row docusaurus-mt-lg',
|
||||
isBlogPostPage && styles.blogPostFooterDetailsFull,
|
||||
)}>
|
||||
{tagsExists && (
|
||||
<div className={clsx('col', {'col--9': truncatedPost})}>
|
||||
<TagsListInline tags={tags} />
|
||||
</div>
|
||||
)}
|
||||
// BlogPost footer - details view
|
||||
if (isBlogPostPage) {
|
||||
const canDisplayEditMetaRow = !!(editUrl || lastUpdatedAt || lastUpdatedBy);
|
||||
|
||||
{isBlogPostPage && editUrl && (
|
||||
<div className="col margin-top--sm">
|
||||
<EditThisPage editUrl={editUrl} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{truncatedPost && (
|
||||
<div
|
||||
className={clsx('col text--right', {
|
||||
'col--3': tagsExists,
|
||||
})}>
|
||||
<ReadMoreLink blogPostTitle={title} to={metadata.permalink} />
|
||||
</div>
|
||||
)}
|
||||
</footer>
|
||||
);
|
||||
return (
|
||||
<footer className="docusaurus-mt-lg">
|
||||
{tagsExists && (
|
||||
<div
|
||||
className={clsx(
|
||||
'row',
|
||||
'margin-top--sm',
|
||||
ThemeClassNames.blog.blogFooterEditMetaRow,
|
||||
)}>
|
||||
<div className="col">
|
||||
<TagsListInline tags={tags} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{canDisplayEditMetaRow && (
|
||||
<EditMetaRow
|
||||
className={clsx(
|
||||
'margin-top--sm',
|
||||
ThemeClassNames.blog.blogFooterEditMetaRow,
|
||||
)}
|
||||
editUrl={editUrl}
|
||||
lastUpdatedAt={lastUpdatedAt}
|
||||
lastUpdatedBy={lastUpdatedBy}
|
||||
/>
|
||||
)}
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
// BlogPost footer - list view
|
||||
else {
|
||||
return (
|
||||
<footer className="row docusaurus-mt-lg">
|
||||
{tagsExists && (
|
||||
<div className={clsx('col', {'col--9': truncatedPost})}>
|
||||
<TagsListInline tags={tags} />
|
||||
</div>
|
||||
)}
|
||||
{truncatedPost && (
|
||||
<div
|
||||
className={clsx('col text--right', {
|
||||
'col--3': tagsExists,
|
||||
})}>
|
||||
<ReadMoreLink blogPostTitle={title} to={metadata.permalink} />
|
||||
</div>
|
||||
)}
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +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.
|
||||
*/
|
||||
|
||||
.blogPostFooterDetailsFull {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
|
@ -8,53 +8,10 @@
|
|||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {ThemeClassNames} from '@docusaurus/theme-common';
|
||||
import {useDoc, type DocContextValue} from '@docusaurus/theme-common/internal';
|
||||
import LastUpdated from '@theme/LastUpdated';
|
||||
import EditThisPage from '@theme/EditThisPage';
|
||||
import TagsListInline, {
|
||||
type Props as TagsListInlineProps,
|
||||
} from '@theme/TagsListInline';
|
||||
import {useDoc} from '@docusaurus/theme-common/internal';
|
||||
import TagsListInline from '@theme/TagsListInline';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
function TagsRow(props: TagsListInlineProps) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
ThemeClassNames.docs.docFooterTagsRow,
|
||||
'row margin-bottom--sm',
|
||||
)}>
|
||||
<div className="col">
|
||||
<TagsListInline {...props} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type EditMetaRowProps = Pick<
|
||||
DocContextValue['metadata'],
|
||||
'editUrl' | 'lastUpdatedAt' | 'lastUpdatedBy'
|
||||
>;
|
||||
function EditMetaRow({
|
||||
editUrl,
|
||||
lastUpdatedAt,
|
||||
lastUpdatedBy,
|
||||
}: EditMetaRowProps) {
|
||||
return (
|
||||
<div className={clsx(ThemeClassNames.docs.docFooterEditMetaRow, 'row')}>
|
||||
<div className="col">{editUrl && <EditThisPage editUrl={editUrl} />}</div>
|
||||
|
||||
<div className={clsx('col', styles.lastUpdated)}>
|
||||
{(lastUpdatedAt || lastUpdatedBy) && (
|
||||
<LastUpdated
|
||||
lastUpdatedAt={lastUpdatedAt}
|
||||
lastUpdatedBy={lastUpdatedBy}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import EditMetaRow from '@theme/EditMetaRow';
|
||||
|
||||
export default function DocItemFooter(): JSX.Element | null {
|
||||
const {metadata} = useDoc();
|
||||
|
|
@ -72,9 +29,23 @@ export default function DocItemFooter(): JSX.Element | null {
|
|||
return (
|
||||
<footer
|
||||
className={clsx(ThemeClassNames.docs.docFooter, 'docusaurus-mt-lg')}>
|
||||
{canDisplayTagsRow && <TagsRow tags={tags} />}
|
||||
{canDisplayTagsRow && (
|
||||
<div
|
||||
className={clsx(
|
||||
'row margin-top--sm',
|
||||
ThemeClassNames.docs.docFooterTagsRow,
|
||||
)}>
|
||||
<div className="col">
|
||||
<TagsListInline tags={tags} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{canDisplayEditMetaRow && (
|
||||
<EditMetaRow
|
||||
className={clsx(
|
||||
'margin-top--sm',
|
||||
ThemeClassNames.docs.docFooterEditMetaRow,
|
||||
)}
|
||||
editUrl={editUrl}
|
||||
lastUpdatedAt={lastUpdatedAt}
|
||||
lastUpdatedBy={lastUpdatedBy}
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ function CollapseButton({
|
|||
{label: categoryLabel},
|
||||
)
|
||||
}
|
||||
aria-expanded={!collapsed}
|
||||
type="button"
|
||||
className="clean-btn menu__caret"
|
||||
onClick={onClick}
|
||||
|
|
@ -193,7 +194,8 @@ export default function DocSidebarItemCategory({
|
|||
}
|
||||
}
|
||||
aria-current={isCurrentPage ? 'page' : undefined}
|
||||
aria-expanded={collapsible ? !collapsed : undefined}
|
||||
role={collapsible && !href ? 'button' : undefined}
|
||||
aria-expanded={collapsible && !href ? !collapsed : undefined}
|
||||
href={collapsible ? hrefWithSSRFallback ?? '#' : hrefWithSSRFallback}
|
||||
{...props}>
|
||||
{label}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* 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 React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import EditThisPage from '@theme/EditThisPage';
|
||||
import type {Props} from '@theme/EditMetaRow';
|
||||
|
||||
import LastUpdated from '@theme/LastUpdated';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
export default function EditMetaRow({
|
||||
className,
|
||||
editUrl,
|
||||
lastUpdatedAt,
|
||||
lastUpdatedBy,
|
||||
}: Props): JSX.Element {
|
||||
return (
|
||||
<div className={clsx('row', className)}>
|
||||
<div className="col">{editUrl && <EditThisPage editUrl={editUrl} />}</div>
|
||||
<div className={clsx('col', styles.lastUpdated)}>
|
||||
{(lastUpdatedAt || lastUpdatedBy) && (
|
||||
<LastUpdated
|
||||
lastUpdatedAt={lastUpdatedAt}
|
||||
lastUpdatedBy={lastUpdatedBy}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -6,9 +6,9 @@
|
|||
*/
|
||||
|
||||
.lastUpdated {
|
||||
margin-top: 0.2rem;
|
||||
font-style: italic;
|
||||
font-size: smaller;
|
||||
font-style: italic;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
@media (min-width: 997px) {
|
||||
|
|
@ -34,7 +34,7 @@ function LastUpdatedAtDate({
|
|||
values={{
|
||||
date: (
|
||||
<b>
|
||||
<time dateTime={atDate.toISOString()}>
|
||||
<time dateTime={atDate.toISOString()} itemProp="dateModified">
|
||||
{formattedLastUpdatedAt}
|
||||
</time>
|
||||
</b>
|
||||
|
|
|
|||
|
|
@ -73,5 +73,7 @@ export const ThemeClassNames = {
|
|||
},
|
||||
blog: {
|
||||
// TODO add other stable classNames here
|
||||
blogFooterTagsRow: 'theme-blog-footer-tags-row',
|
||||
blogFooterEditMetaRow: 'theme-blog-footer-edit-meta-row',
|
||||
},
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -23,19 +23,23 @@ import type {
|
|||
} from '@docusaurus/plugin-content-blog';
|
||||
import type {DocusaurusConfig} from '@docusaurus/types';
|
||||
|
||||
const convertDate = (dateMs: number) => new Date(dateMs * 1000).toISOString();
|
||||
|
||||
function getBlogPost(
|
||||
blogPostContent: PropBlogPostContent,
|
||||
siteConfig: DocusaurusConfig,
|
||||
withBaseUrl: BaseUrlUtils['withBaseUrl'],
|
||||
) {
|
||||
): BlogPosting {
|
||||
const {assets, frontMatter, metadata} = blogPostContent;
|
||||
const {date, title, description} = metadata;
|
||||
const {date, title, description, lastUpdatedAt} = metadata;
|
||||
|
||||
const image = assets.image ?? frontMatter.image;
|
||||
const keywords = frontMatter.keywords ?? [];
|
||||
|
||||
const blogUrl = `${siteConfig.url}${metadata.permalink}`;
|
||||
|
||||
const dateModified = lastUpdatedAt ? convertDate(lastUpdatedAt) : undefined;
|
||||
|
||||
return {
|
||||
'@type': 'BlogPosting',
|
||||
'@id': blogUrl,
|
||||
|
|
@ -45,6 +49,7 @@ function getBlogPost(
|
|||
name: title,
|
||||
description,
|
||||
datePublished: date,
|
||||
...(dateModified ? {dateModified} : {}),
|
||||
...getAuthor(metadata.authors),
|
||||
...getImage(image, withBaseUrl, title),
|
||||
...(keywords ? {keywords} : {}),
|
||||
|
|
@ -108,11 +113,13 @@ export function useBlogPostStructuredData(): WithContext<BlogPosting> {
|
|||
const {siteConfig} = useDocusaurusContext();
|
||||
const {withBaseUrl} = useBaseUrlUtils();
|
||||
|
||||
const {date, title, description, frontMatter} = metadata;
|
||||
const {date, title, description, frontMatter, lastUpdatedAt} = metadata;
|
||||
|
||||
const image = assets.image ?? frontMatter.image;
|
||||
const keywords = frontMatter.keywords ?? [];
|
||||
|
||||
const dateModified = lastUpdatedAt ? convertDate(lastUpdatedAt) : undefined;
|
||||
|
||||
const url = `${siteConfig.url}${metadata.permalink}`;
|
||||
|
||||
// details on structured data support: https://schema.org/BlogPosting
|
||||
|
|
@ -128,6 +135,7 @@ export function useBlogPostStructuredData(): WithContext<BlogPosting> {
|
|||
name: title,
|
||||
description,
|
||||
datePublished: date,
|
||||
...(dateModified ? {dateModified} : {}),
|
||||
...getAuthor(metadata.authors),
|
||||
...getImage(image, withBaseUrl, title),
|
||||
...(keywords ? {keywords} : {}),
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@ function DocSearch({
|
|||
const onClose = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
searchContainer.current?.remove();
|
||||
searchButtonRef.current?.focus();
|
||||
}, [setIsOpen]);
|
||||
|
||||
const onInput = useCallback(
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ export type ApplyTrailingSlashParams = Pick<
|
|||
'trailingSlash' | 'baseUrl'
|
||||
>;
|
||||
|
||||
export function addTrailingSlash(str: string): string {
|
||||
return str.endsWith('/') ? str : `${str}/`;
|
||||
}
|
||||
|
||||
// Trailing slash handling depends in some site configuration options
|
||||
export default function applyTrailingSlash(
|
||||
path: string,
|
||||
|
|
@ -24,10 +28,6 @@ export default function applyTrailingSlash(
|
|||
return path;
|
||||
}
|
||||
|
||||
// TODO deduplicate: also present in @docusaurus/utils
|
||||
function addTrailingSlash(str: string): string {
|
||||
return str.endsWith('/') ? str : `${str}/`;
|
||||
}
|
||||
function removeTrailingSlash(str: string): string {
|
||||
return str.endsWith('/') ? str.slice(0, -1) : str;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const blogPostContainerID = '__blog-post-container';
|
|||
|
||||
export {
|
||||
default as applyTrailingSlash,
|
||||
addTrailingSlash,
|
||||
type ApplyTrailingSlashParams,
|
||||
} from './applyTrailingSlash';
|
||||
export {getErrorCausalChain} from './errorUtils';
|
||||
|
|
|
|||
|
|
@ -30,6 +30,22 @@ exports[`validation schemas contentVisibilitySchema: for value={"unlisted":"bad
|
|||
|
||||
exports[`validation schemas contentVisibilitySchema: for value={"unlisted":42} 1`] = `""unlisted" must be a boolean"`;
|
||||
|
||||
exports[`validation schemas frontMatterLastUpdateSchema schema: for value="string" 1`] = `""value" 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."`;
|
||||
|
||||
exports[`validation schemas frontMatterLastUpdateSchema schema: for value=[] 1`] = `""value" 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."`;
|
||||
|
||||
exports[`validation schemas frontMatterLastUpdateSchema schema: for value={"author":23} 1`] = `""author" must be a string"`;
|
||||
|
||||
exports[`validation schemas frontMatterLastUpdateSchema schema: for value={"date":"20-20-20"} 1`] = `""date" must be a valid date"`;
|
||||
|
||||
exports[`validation schemas frontMatterLastUpdateSchema schema: for value={} 1`] = `""value" 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."`;
|
||||
|
||||
exports[`validation schemas frontMatterLastUpdateSchema schema: for value=42 1`] = `""value" 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."`;
|
||||
|
||||
exports[`validation schemas frontMatterLastUpdateSchema schema: for value=null 1`] = `""value" 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."`;
|
||||
|
||||
exports[`validation schemas frontMatterLastUpdateSchema schema: for value=true 1`] = `""value" 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."`;
|
||||
|
||||
exports[`validation schemas pathnameSchema: for value="foo" 1`] = `""value" (foo) is not a valid pathname. Pathname should start with slash and not contain any domain or query string."`;
|
||||
|
||||
exports[`validation schemas pathnameSchema: for value="https://github.com/foo" 1`] = `""value" (https://github.com/foo) is not a valid pathname. Pathname should start with slash and not contain any domain or query string."`;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
PathnameSchema,
|
||||
RouteBasePathSchema,
|
||||
ContentVisibilitySchema,
|
||||
FrontMatterLastUpdateSchema,
|
||||
} from '../validationSchemas';
|
||||
|
||||
function createTestHelpers({
|
||||
|
|
@ -216,4 +217,28 @@ describe('validation schemas', () => {
|
|||
testFail({unlisted: 42});
|
||||
testFail({draft: true, unlisted: true});
|
||||
});
|
||||
|
||||
it('frontMatterLastUpdateSchema schema', () => {
|
||||
const {testFail, testOK} = createTestHelpers({
|
||||
schema: FrontMatterLastUpdateSchema,
|
||||
});
|
||||
|
||||
testOK(undefined);
|
||||
testOK({date: '2021-01-01'});
|
||||
testOK({date: '2021-01'});
|
||||
testOK({date: '2021'});
|
||||
testOK({date: new Date()});
|
||||
testOK({author: 'author'});
|
||||
testOK({author: 'author', date: '2021-01-01'});
|
||||
testOK({author: 'author', date: new Date()});
|
||||
|
||||
testFail(null);
|
||||
testFail({});
|
||||
testFail('string');
|
||||
testFail(42);
|
||||
testFail(true);
|
||||
testFail([]);
|
||||
testFail({author: 23});
|
||||
testFail({date: '20-20-20'});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -26,4 +26,6 @@ export {
|
|||
FrontMatterTagsSchema,
|
||||
FrontMatterTOCHeadingLevels,
|
||||
ContentVisibilitySchema,
|
||||
FrontMatterLastUpdateErrorMessage,
|
||||
FrontMatterLastUpdateSchema,
|
||||
} from './validationSchemas';
|
||||
|
|
|
|||
|
|
@ -167,3 +167,16 @@ export const ContentVisibilitySchema = JoiFrontMatter.object<ContentVisibility>(
|
|||
"Can't be draft and unlisted at the same time.",
|
||||
})
|
||||
.unknown();
|
||||
|
||||
export const FrontMatterLastUpdateErrorMessage =
|
||||
'{{#label}} 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.';
|
||||
|
||||
export const FrontMatterLastUpdateSchema = Joi.object({
|
||||
author: Joi.string(),
|
||||
date: Joi.date().raw(),
|
||||
})
|
||||
.or('author', 'date')
|
||||
.messages({
|
||||
'object.missing': FrontMatterLastUpdateErrorMessage,
|
||||
'object.base': FrontMatterLastUpdateErrorMessage,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@docusaurus/logger": "3.0.0",
|
||||
"@docusaurus/utils-common": "3.0.0",
|
||||
"@svgr/webpack": "^6.5.1",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"file-loader": "^6.2.0",
|
||||
|
|
|
|||
1
packages/docusaurus-utils/src/__tests__/__fixtures__/simple-site/doc with space.md
generated
Normal file
1
packages/docusaurus-utils/src/__tests__/__fixtures__/simple-site/doc with space.md
generated
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Hoo hoo, if this path tricks you...
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
id: hello
|
||||
title: Hello, World !
|
||||
slug: /
|
||||
---
|
||||
|
||||
Hello
|
||||
|
|
@ -9,6 +9,7 @@ import fs from 'fs-extra';
|
|||
import path from 'path';
|
||||
import {createTempRepo} from '@testing-utils/git';
|
||||
import {FileNotTrackedError, getFileCommitDate} from '../gitUtils';
|
||||
import {getFileLastUpdate} from '../lastUpdateUtils';
|
||||
|
||||
/* eslint-disable no-restricted-properties */
|
||||
function initializeTempRepo() {
|
||||
|
|
@ -136,4 +137,22 @@ describe('getFileCommitDate', () => {
|
|||
/Failed to retrieve git history for ".*nonexistent.txt" because the file does not exist./,
|
||||
);
|
||||
});
|
||||
|
||||
it('multiple files not tracked by git', async () => {
|
||||
const consoleMock = jest
|
||||
.spyOn(console, 'warn')
|
||||
.mockImplementation(() => {});
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,226 @@
|
|||
/**
|
||||
* 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 {createTempRepo} from '@testing-utils/git';
|
||||
import shell from 'shelljs';
|
||||
import {
|
||||
getFileLastUpdate,
|
||||
GIT_FALLBACK_LAST_UPDATE_AUTHOR,
|
||||
GIT_FALLBACK_LAST_UPDATE_DATE,
|
||||
readLastUpdateData,
|
||||
} from '@docusaurus/utils';
|
||||
|
||||
describe('getFileLastUpdate', () => {
|
||||
const {repoDir} = createTempRepo();
|
||||
|
||||
const existingFilePath = path.join(
|
||||
__dirname,
|
||||
'__fixtures__/simple-site/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/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)).rejects.toThrow(
|
||||
/An error occurred when trying to get the last update date/,
|
||||
);
|
||||
expect(consoleMock).toHaveBeenCalledTimes(0);
|
||||
consoleMock.mockRestore();
|
||||
});
|
||||
|
||||
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 last update options require Git\..*/,
|
||||
),
|
||||
);
|
||||
|
||||
consoleMock.mockRestore();
|
||||
mock.mockRestore();
|
||||
});
|
||||
|
||||
it('temporary created file that is not tracked by git', async () => {
|
||||
const consoleMock = jest
|
||||
.spyOn(console, 'warn')
|
||||
.mockImplementation(() => {});
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readLastUpdateData', () => {
|
||||
const testDate = '2021-01-01';
|
||||
const testDateTime = new Date(testDate).getTime() / 1000;
|
||||
const testAuthor = 'ozaki';
|
||||
|
||||
it('read last time show author time', async () => {
|
||||
const {lastUpdatedAt, lastUpdatedBy} = await readLastUpdateData(
|
||||
'',
|
||||
{showLastUpdateAuthor: true, showLastUpdateTime: true},
|
||||
{date: testDate},
|
||||
);
|
||||
expect(lastUpdatedAt).toEqual(testDateTime);
|
||||
expect(lastUpdatedBy).toBe(GIT_FALLBACK_LAST_UPDATE_AUTHOR);
|
||||
});
|
||||
|
||||
it('read last author show author time', async () => {
|
||||
const {lastUpdatedAt, lastUpdatedBy} = await readLastUpdateData(
|
||||
'',
|
||||
{showLastUpdateAuthor: true, showLastUpdateTime: true},
|
||||
{author: testAuthor},
|
||||
);
|
||||
expect(lastUpdatedBy).toEqual(testAuthor);
|
||||
expect(lastUpdatedAt).toBe(GIT_FALLBACK_LAST_UPDATE_DATE);
|
||||
});
|
||||
|
||||
it('read last all show author time', async () => {
|
||||
const {lastUpdatedAt, lastUpdatedBy} = await readLastUpdateData(
|
||||
'',
|
||||
{showLastUpdateAuthor: true, showLastUpdateTime: true},
|
||||
{author: testAuthor, date: testDate},
|
||||
);
|
||||
expect(lastUpdatedBy).toEqual(testAuthor);
|
||||
expect(lastUpdatedAt).toEqual(testDateTime);
|
||||
});
|
||||
|
||||
it('read last default show none', async () => {
|
||||
const lastUpdate = await readLastUpdateData(
|
||||
'',
|
||||
{showLastUpdateAuthor: false, showLastUpdateTime: false},
|
||||
{},
|
||||
);
|
||||
expect(lastUpdate).toEqual({});
|
||||
});
|
||||
|
||||
it('read last author show none', async () => {
|
||||
const lastUpdate = await readLastUpdateData(
|
||||
'',
|
||||
{showLastUpdateAuthor: false, showLastUpdateTime: false},
|
||||
{author: testAuthor},
|
||||
);
|
||||
expect(lastUpdate).toEqual({});
|
||||
});
|
||||
|
||||
it('read last time show author', async () => {
|
||||
const {lastUpdatedAt, lastUpdatedBy} = await readLastUpdateData(
|
||||
'',
|
||||
{showLastUpdateAuthor: true, showLastUpdateTime: false},
|
||||
{date: testDate},
|
||||
);
|
||||
expect(lastUpdatedBy).toBe(GIT_FALLBACK_LAST_UPDATE_AUTHOR);
|
||||
expect(lastUpdatedAt).toBeUndefined();
|
||||
});
|
||||
|
||||
it('read last author show author', async () => {
|
||||
const {lastUpdatedAt, lastUpdatedBy} = await readLastUpdateData(
|
||||
'',
|
||||
{showLastUpdateAuthor: true, showLastUpdateTime: false},
|
||||
{author: testAuthor},
|
||||
);
|
||||
expect(lastUpdatedBy).toBe('ozaki');
|
||||
expect(lastUpdatedAt).toBeUndefined();
|
||||
});
|
||||
|
||||
it('read last default show author default', async () => {
|
||||
const {lastUpdatedAt, lastUpdatedBy} = await readLastUpdateData(
|
||||
'',
|
||||
{showLastUpdateAuthor: true, showLastUpdateTime: false},
|
||||
{},
|
||||
);
|
||||
expect(lastUpdatedBy).toBe(GIT_FALLBACK_LAST_UPDATE_AUTHOR);
|
||||
expect(lastUpdatedAt).toBeUndefined();
|
||||
});
|
||||
|
||||
it('read last time show time', async () => {
|
||||
const {lastUpdatedAt, lastUpdatedBy} = await readLastUpdateData(
|
||||
'',
|
||||
{showLastUpdateAuthor: false, showLastUpdateTime: true},
|
||||
{date: testDate},
|
||||
);
|
||||
expect(lastUpdatedBy).toBeUndefined();
|
||||
expect(lastUpdatedAt).toEqual(testDateTime);
|
||||
});
|
||||
|
||||
it('read last author show time', async () => {
|
||||
const {lastUpdatedAt, lastUpdatedBy} = await readLastUpdateData(
|
||||
'',
|
||||
{showLastUpdateAuthor: false, showLastUpdateTime: true},
|
||||
{author: testAuthor},
|
||||
);
|
||||
expect(lastUpdatedBy).toBeUndefined();
|
||||
expect(lastUpdatedAt).toEqual(GIT_FALLBACK_LAST_UPDATE_DATE);
|
||||
});
|
||||
|
||||
it('read last author show time only - both front matter', async () => {
|
||||
const {lastUpdatedAt, lastUpdatedBy} = await readLastUpdateData(
|
||||
'',
|
||||
{showLastUpdateAuthor: false, showLastUpdateTime: true},
|
||||
{author: testAuthor, date: testDate},
|
||||
);
|
||||
expect(lastUpdatedBy).toBeUndefined();
|
||||
expect(lastUpdatedAt).toEqual(testDateTime);
|
||||
});
|
||||
|
||||
it('read last author show author only - both front matter', async () => {
|
||||
const {lastUpdatedAt, lastUpdatedBy} = await readLastUpdateData(
|
||||
'',
|
||||
{showLastUpdateAuthor: true, showLastUpdateTime: false},
|
||||
{author: testAuthor, date: testDate},
|
||||
);
|
||||
expect(lastUpdatedBy).toEqual(testAuthor);
|
||||
expect(lastUpdatedAt).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -5,12 +5,12 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {addTrailingSlash} from '@docusaurus/utils-common';
|
||||
import {
|
||||
normalizeUrl,
|
||||
getEditUrl,
|
||||
fileToPath,
|
||||
isValidPathname,
|
||||
addTrailingSlash,
|
||||
addLeadingSlash,
|
||||
removeTrailingSlash,
|
||||
resolvePathname,
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ export {
|
|||
parseURLPath,
|
||||
serializeURLPath,
|
||||
addLeadingSlash,
|
||||
addTrailingSlash,
|
||||
removeTrailingSlash,
|
||||
hasSSHProtocol,
|
||||
buildHttpsUrl,
|
||||
|
|
@ -118,3 +117,12 @@ export {
|
|||
export {isDraft, isUnlisted} from './contentVisibilityUtils';
|
||||
export {escapeRegexp} from './regExpUtils';
|
||||
export {askPreferredLanguage} from './cliUtils';
|
||||
|
||||
export {
|
||||
getFileLastUpdate,
|
||||
type LastUpdateData,
|
||||
type FrontMatterLastUpdate,
|
||||
readLastUpdateData,
|
||||
GIT_FALLBACK_LAST_UPDATE_AUTHOR,
|
||||
GIT_FALLBACK_LAST_UPDATE_DATE,
|
||||
} from './lastUpdateUtils';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,132 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import logger from '@docusaurus/logger';
|
||||
import {
|
||||
FileNotTrackedError,
|
||||
GitNotFoundError,
|
||||
getFileCommitDate,
|
||||
} from './gitUtils';
|
||||
import type {PluginOptions} from '@docusaurus/types';
|
||||
|
||||
export const GIT_FALLBACK_LAST_UPDATE_DATE = 1539502055;
|
||||
|
||||
export const GIT_FALLBACK_LAST_UPDATE_AUTHOR = 'Author';
|
||||
|
||||
async function getGitLastUpdate(filePath: string): Promise<LastUpdateData> {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
// Use fake data in dev/test for faster development.
|
||||
return {
|
||||
lastUpdatedBy: GIT_FALLBACK_LAST_UPDATE_AUTHOR,
|
||||
lastUpdatedAt: GIT_FALLBACK_LAST_UPDATE_DATE,
|
||||
};
|
||||
}
|
||||
const {author, timestamp} = (await getFileLastUpdate(filePath)) ?? {};
|
||||
return {lastUpdatedBy: author, lastUpdatedAt: timestamp};
|
||||
}
|
||||
|
||||
export type LastUpdateData = {
|
||||
/** A timestamp in **seconds**, directly acquired from `git log`. */
|
||||
lastUpdatedAt?: number;
|
||||
/** The author's name directly acquired from `git log`. */
|
||||
lastUpdatedBy?: string;
|
||||
};
|
||||
|
||||
export type FrontMatterLastUpdate = {
|
||||
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;
|
||||
};
|
||||
|
||||
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 = await 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 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 {
|
||||
throw new Error(
|
||||
`An error occurred when trying to get the last update date`,
|
||||
{cause: err},
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
type LastUpdateOptions = Pick<
|
||||
PluginOptions,
|
||||
'showLastUpdateAuthor' | 'showLastUpdateTime'
|
||||
>;
|
||||
|
||||
export async function readLastUpdateData(
|
||||
filePath: string,
|
||||
options: LastUpdateOptions,
|
||||
lastUpdateFrontMatter: FrontMatterLastUpdate | undefined,
|
||||
): Promise<LastUpdateData> {
|
||||
const {showLastUpdateAuthor, showLastUpdateTime} = options;
|
||||
|
||||
if (!showLastUpdateAuthor && !showLastUpdateTime) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const frontMatterAuthor = lastUpdateFrontMatter?.author;
|
||||
const frontMatterTimestamp = lastUpdateFrontMatter?.date
|
||||
? new Date(lastUpdateFrontMatter.date).getTime() / 1000
|
||||
: undefined;
|
||||
|
||||
// We try to minimize git last update calls
|
||||
// We call it at most once
|
||||
// If all the data is provided as front matter, we do not call it
|
||||
const getGitLastUpdateMemoized = _.memoize(() => getGitLastUpdate(filePath));
|
||||
const getGitLastUpdateBy = () =>
|
||||
getGitLastUpdateMemoized().then((update) => update.lastUpdatedBy);
|
||||
const getGitLastUpdateAt = () =>
|
||||
getGitLastUpdateMemoized().then((update) => update.lastUpdatedAt);
|
||||
|
||||
const lastUpdatedBy = showLastUpdateAuthor
|
||||
? frontMatterAuthor ?? (await getGitLastUpdateBy())
|
||||
: undefined;
|
||||
|
||||
const lastUpdatedAt = showLastUpdateTime
|
||||
? frontMatterTimestamp ?? (await getGitLastUpdateAt())
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
lastUpdatedBy,
|
||||
lastUpdatedAt,
|
||||
};
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import resolvePathnameUnsafe from 'resolve-pathname';
|
||||
import {addPrefix, addSuffix, removeSuffix} from './jsUtils';
|
||||
import {addPrefix, removeSuffix} from './jsUtils';
|
||||
|
||||
/**
|
||||
* Much like `path.join`, but much better. Takes an array of URL segments, and
|
||||
|
|
@ -237,12 +237,6 @@ export function addLeadingSlash(str: string): string {
|
|||
return addPrefix(str, '/');
|
||||
}
|
||||
|
||||
// TODO deduplicate: also present in @docusaurus/utils-common
|
||||
/** Appends a trailing slash to `str`, if one doesn't exist. */
|
||||
export function addTrailingSlash(str: string): string {
|
||||
return addSuffix(str, '/');
|
||||
}
|
||||
|
||||
/** Removes the trailing slash from `str`. */
|
||||
export function removeTrailingSlash(str: string): string {
|
||||
return removeSuffix(str, '/');
|
||||
|
|
|
|||
|
|
@ -69,8 +69,8 @@
|
|||
"del": "^6.1.1",
|
||||
"detect-port": "^1.5.1",
|
||||
"escape-html": "^1.0.3",
|
||||
"eval": "^0.1.8",
|
||||
"eta": "^2.2.0",
|
||||
"eval": "^0.1.8",
|
||||
"file-loader": "^6.2.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"html-minifier-terser": "^7.2.0",
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ import _ from 'lodash';
|
|||
import logger from '@docusaurus/logger';
|
||||
import {matchRoutes as reactRouterMatchRoutes} from 'react-router-config';
|
||||
import {
|
||||
addTrailingSlash,
|
||||
parseURLPath,
|
||||
removeTrailingSlash,
|
||||
serializeURLPath,
|
||||
type URLPath,
|
||||
} from '@docusaurus/utils';
|
||||
import {addTrailingSlash} from '@docusaurus/utils-common';
|
||||
import {getAllFinalRoutes} from './routes';
|
||||
import type {RouteConfig, ReportingSeverity} from '@docusaurus/types';
|
||||
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@ import {
|
|||
DEFAULT_STATIC_DIR_NAME,
|
||||
DEFAULT_I18N_DIR_NAME,
|
||||
addLeadingSlash,
|
||||
addTrailingSlash,
|
||||
removeTrailingSlash,
|
||||
} from '@docusaurus/utils';
|
||||
import {Joi, printWarning} from '@docusaurus/utils-validation';
|
||||
import {addTrailingSlash} from '@docusaurus/utils-common';
|
||||
import type {
|
||||
DocusaurusConfig,
|
||||
I18nConfig,
|
||||
|
|
|
|||
|
|
@ -227,6 +227,7 @@ orta
|
|||
Outerbounds
|
||||
outerbounds
|
||||
overrideable
|
||||
ozaki
|
||||
O’Shannessy
|
||||
pageview
|
||||
Palenight
|
||||
|
|
|
|||
|
|
@ -76,6 +76,8 @@ Accepted fields:
|
|||
| `feedOptions.language` | `string` (See [documentation](http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes) for possible values) | `undefined` | Language metadata of the feed. |
|
||||
| `sortPosts` | <code>'descending' \| 'ascending' </code> | `'descending'` | Governs the direction of blog post sorting. |
|
||||
| `processBlogPosts` | <code>[ProcessBlogPostsFn](#ProcessBlogPostsFn)</code> | `undefined` | An optional function which can be used to transform blog posts (filter, modify, delete, etc...). |
|
||||
| `showLastUpdateAuthor` | `boolean` | `false` | Whether to display the author who last updated the blog post. |
|
||||
| `showLastUpdateTime` | `boolean` | `false` | Whether to display the last date the blog post was updated. This requires access to git history during the build, so will not work correctly with shallow clones (a common default for CI systems). With GitHub `actions/checkout`, use`fetch-depth: 0`. |
|
||||
|
||||
```mdx-code-block
|
||||
</APITable>
|
||||
|
|
@ -232,12 +234,15 @@ Accepted fields:
|
|||
| `description` | `string` | The first line of Markdown content | The description of your document, which will become the `<meta name="description" content="..."/>` and `<meta property="og:description" content="..."/>` in `<head>`, used by search engines. |
|
||||
| `image` | `string` | `undefined` | Cover or thumbnail image that will be used as the `<meta property="og:image" content="..."/>` in the `<head>`, enhancing link previews on social media and messaging platforms. |
|
||||
| `slug` | `string` | File path | Allows to customize the blog post URL (`/<routeBasePath>/<slug>`). Support multiple patterns: `slug: my-blog-post`, `slug: /my/path/to/blog/post`, slug: `/`. |
|
||||
| `last_update` | `FrontMatterLastUpdate` | `undefined` | Allows overriding the last update author/date. Date can be any [parsable date string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse). |
|
||||
|
||||
```mdx-code-block
|
||||
</APITable>
|
||||
```
|
||||
|
||||
```ts
|
||||
type FrontMatterLastUpdate = {date?: string; author?: string};
|
||||
|
||||
type Tag = string | {label: string; permalink: string};
|
||||
|
||||
// An author key references an author from the global plugin authors.yml file
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ Accepted fields:
|
|||
| `beforeDefaultRemarkPlugins` | `any[]` | `[]` | Custom Remark plugins passed to MDX before the default Docusaurus Remark plugins. |
|
||||
| `beforeDefaultRehypePlugins` | `any[]` | `[]` | Custom Rehype plugins passed to MDX before the default Docusaurus Rehype plugins. |
|
||||
| `showLastUpdateAuthor` | `boolean` | `false` | Whether to display the author who last updated the doc. |
|
||||
| `showLastUpdateTime` | `boolean` | `false` | Whether to display the last date the doc was updated. This requires access to git history during the build, so will not work correctly with shallow clones (a common default for CI systems). |
|
||||
| `showLastUpdateTime` | `boolean` | `false` | Whether to display the last date the doc was updated. This requires access to git history during the build, so will not work correctly with shallow clones (a common default for CI systems). With GitHub `actions/checkout`, use`fetch-depth: 0`. |
|
||||
| `breadcrumbs` | `boolean` | `true` | Enable or disable the breadcrumbs on doc pages. |
|
||||
| `disableVersioning` | `boolean` | `false` | Explicitly disable versioning even when multiple versions exist. This will make the site only include the current version. Will error if `includeCurrentVersion: false` and `disableVersioning: true`. |
|
||||
| `includeCurrentVersion` | `boolean` | `true` | Include the current version of your docs. |
|
||||
|
|
@ -296,18 +296,16 @@ Accepted fields:
|
|||
| `tags` | `Tag[]` | `undefined` | A list of strings or objects of two string fields `label` and `permalink` to tag to your docs. |
|
||||
| `draft` | `boolean` | `false` | Draft documents will only be available during development. |
|
||||
| `unlisted` | `boolean` | `false` | Unlisted documents will be available in both development and production. They will be "hidden" in production, not indexed, excluded from sitemaps, and can only be accessed by users having a direct link. |
|
||||
| `last_update` | `FileChange` | `undefined` | Allows overriding the last updated author and/or date. Date can be any [parsable date string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse). |
|
||||
| `last_update` | `FrontMatterLastUpdate` | `undefined` | Allows overriding the last update author/date. Date can be any [parsable date string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse). |
|
||||
|
||||
```mdx-code-block
|
||||
</APITable>
|
||||
```
|
||||
|
||||
```ts
|
||||
type Tag = string | {label: string; permalink: string};
|
||||
```
|
||||
type FrontMatterLastUpdate = {date?: string; author?: string};
|
||||
|
||||
```ts
|
||||
type FileChange = {date: string; author: string};
|
||||
type Tag = string | {label: string; permalink: string};
|
||||
```
|
||||
|
||||
Example:
|
||||
|
|
|
|||
|
|
@ -195,7 +195,9 @@ export default async function createConfigAsync() {
|
|||
result = result.replaceAll('{/_', '{/*');
|
||||
result = result.replaceAll('_/}', '*/}');
|
||||
|
||||
if (isDev) {
|
||||
const showDevLink = false;
|
||||
|
||||
if (isDev && showDevLink) {
|
||||
const isPartial = path.basename(filePath).startsWith('_');
|
||||
if (!isPartial) {
|
||||
// "vscode://file/${projectPath}${filePath}:${line}:${column}",
|
||||
|
|
@ -441,6 +443,8 @@ export default async function createConfigAsync() {
|
|||
blog: {
|
||||
// routeBasePath: '/',
|
||||
path: 'blog',
|
||||
showLastUpdateAuthor: true,
|
||||
showLastUpdateTime: true,
|
||||
editUrl: ({locale, blogDirPath, blogPath}) => {
|
||||
if (locale !== defaultLocale) {
|
||||
return `https://crowdin.com/project/docusaurus-v2/${locale}`;
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ export function ActiveTeamRow(): JSX.Element {
|
|||
<TeamProfileCardCol
|
||||
name="Clément Couriol"
|
||||
githubUrl="https://github.com/ozakione">
|
||||
<Translate id="team.profile.Yangshun Tay.body">
|
||||
<Translate id="team.profile.Clement Couriol.body">
|
||||
Student from CPE Lyon, France. Passionate web developer who tries to
|
||||
become an expert web developer.
|
||||
</Translate>
|
||||
|
|
|
|||
Loading…
Reference in New Issue