mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-28 09:47:48 +02:00
feat(blog): add LastUpdateAuthor & LastUpdateTime (#9912)
Co-authored-by: OzakIOne <OzakIOne@users.noreply.github.com> Co-authored-by: sebastien <lorber.sebastien@gmail.com>
This commit is contained in:
parent
5453bc517d
commit
32fa7e0a8d
40 changed files with 833 additions and 359 deletions
2
.eslintrc.js
vendored
2
.eslintrc.js
vendored
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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...).
|
||||
*/
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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} : {}),
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
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...
|
7
packages/docusaurus-utils/src/__tests__/__fixtures__/simple-site/hello.md
generated
Normal file
7
packages/docusaurus-utils/src/__tests__/__fixtures__/simple-site/hello.md
generated
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
226
packages/docusaurus-utils/src/__tests__/lastUpdateUtils.test.ts
Normal file
226
packages/docusaurus-utils/src/__tests__/lastUpdateUtils.test.ts
Normal file
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -117,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';
|
||||
|
|
132
packages/docusaurus-utils/src/lastUpdateUtils.ts
Normal file
132
packages/docusaurus-utils/src/lastUpdateUtils.ts
Normal file
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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}`;
|
||||
|
|
Loading…
Add table
Reference in a new issue