Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(blog): add LastUpdateAuthor & LastUpdateTime #9912

Merged
merged 45 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
5ba18f3
feat(blog): add LastUpdateAuthor & LastUpdateTime
OzakIOne Mar 5, 2024
5967a6f
display if values exists
OzakIOne Mar 5, 2024
dc305e3
wip shared code
OzakIOne Mar 5, 2024
6f098d8
wip shared code
OzakIOne Mar 5, 2024
8b19677
wip share code & footer
OzakIOne Mar 6, 2024
4db29f9
Merge branch 'main' into ozaki/blogPostLastUpdate
OzakIOne Mar 6, 2024
fb2a3b6
wip tests
OzakIOne Mar 6, 2024
7130546
refactor: apply lint autofix
OzakIOne Mar 6, 2024
2965359
update type & doc
OzakIOne Mar 6, 2024
6331811
wip share code
OzakIOne Mar 6, 2024
3a88750
use throw error
OzakIOne Mar 6, 2024
87fcdf0
seo itemprop datemodified
OzakIOne Mar 6, 2024
5dc4c04
wip: git track
OzakIOne Mar 7, 2024
e0ac624
refactor: tests & shared code
OzakIOne Mar 7, 2024
7bb0310
refactor: apply lint autofix
OzakIOne Mar 7, 2024
ba1d743
refactor: review comments
OzakIOne Mar 8, 2024
b0a22d6
move tests
OzakIOne Mar 8, 2024
92ef13f
wip review
OzakIOne Mar 11, 2024
f6df255
remove snapshot for inline test
OzakIOne Mar 11, 2024
7636376
wip footer shared code
OzakIOne Mar 11, 2024
cc70ae1
EditMetaRow css
OzakIOne Mar 11, 2024
cf8a9ad
wip fix margin right mobile view
OzakIOne Mar 12, 2024
173d922
fix type definition
OzakIOne Mar 12, 2024
4602164
remove doc plugin hint from error
OzakIOne Mar 12, 2024
40d5163
css review
OzakIOne Mar 12, 2024
6754b74
refactor: apply lint autofix
OzakIOne Mar 12, 2024
a2b9a1e
fix typo
OzakIOne Mar 12, 2024
64644c4
refactor: apply lint autofix
OzakIOne Mar 12, 2024
9e0c233
avoid git diff
OzakIOne Mar 12, 2024
e72b315
constant & optional chaining undefined lint error
OzakIOne Mar 12, 2024
c60ae69
add frontMatterLastUpdateSchema test
OzakIOne Mar 12, 2024
6169d68
wip structured data
OzakIOne Mar 14, 2024
9e5f53e
return empty string instead of undefined
OzakIOne Mar 14, 2024
b5f5617
refactor: apply lint autofix
OzakIOne Mar 14, 2024
7801a60
fix typo
OzakIOne Mar 14, 2024
fad3994
wip style to fix argos
OzakIOne Mar 14, 2024
8485d21
refactor review
OzakIOne Mar 14, 2024
bd59b1b
refactor: apply lint autofix
OzakIOne Mar 14, 2024
0d5a109
fix readLastUpdateData algorithm bug
slorber Mar 14, 2024
daf88ab
Merge remote-tracking branch 'origin/ozaki/blogPostLastUpdate' into o…
slorber Mar 14, 2024
a794d06
add missing structured data type
slorber Mar 14, 2024
7b8cabb
fix blog tests
slorber Mar 14, 2024
1d28fa4
docs: update blog according to doc
OzakIOne Mar 14, 2024
718c7c3
minor docs changes
slorber Mar 14, 2024
cb7a7ee
Refactor a bit theme code and CSS
slorber Mar 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions packages/docusaurus-plugin-content-blog/src/blogUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,16 @@ import {
} from '@docusaurus/utils';
import {validateBlogPostFrontMatter} from './frontMatter';
import {type AuthorsMap, getAuthorsMap, getBlogPostAuthors} from './authors';
import {getFileLastUpdate} from './lastUpdate';
import type {LoadContext, ParseFrontMatter} from '@docusaurus/types';
import type {
PluginOptions,
ReadingTimeFunction,
BlogPost,
BlogTags,
BlogPaginated,
FileChange,
LastUpdateData,
} from '@docusaurus/plugin-content-blog';
import type {BlogContentPaths, BlogMarkdownLoaderOptions} from './types';

Expand All @@ -51,6 +54,52 @@ export function getSourceToPermalink(blogPosts: BlogPost[]): {
);
}

type LastUpdateOptions = Pick<
PluginOptions,
'showLastUpdateAuthor' | 'showLastUpdateTime'
>;

async function readLastUpdateData(
OzakIOne marked this conversation as resolved.
Show resolved Hide resolved
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 function paginateBlogPosts({
blogPosts,
basePageUrl,
Expand Down Expand Up @@ -231,6 +280,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});

Expand Down Expand Up @@ -337,6 +392,8 @@ async function processBlogSourceFile(
authors,
frontMatter,
unlisted,
lastUpdatedAt: lastUpdate.lastUpdatedAt,
lastUpdatedBy: lastUpdate.lastUpdatedBy,
},
content,
};
Expand Down
12 changes: 12 additions & 0 deletions packages/docusaurus-plugin-content-blog/src/frontMatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ const BlogPostFrontMatterAuthorSchema = Joi.object({
const FrontMatterAuthorErrorMessage =
'{{#label}} does not look like a valid blog post author. Please use an author key or an author object (with a key and/or name).';

const FrontMatterLastUpdateErrorMessage =
'{{#label}} does not look like a valid front matter FileChange object. Please use a FileChange object (with an author and/or date).';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is the historical message, but honestly, I doubt users will understand it 😅

Not sure mentioning FileChange is super useful, we just need users to know they should provide a date or an author attribute.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type FrontMatterLastUpdate = {
  author?: string;
  date?: Date | string;
};

As author seems to be only a string I wonder removing author key or an author object (with a key and/or name)

So it would be something like this :

const FrontMatterAuthorErrorMessage =
  '{{#label}} does not look like a valid blog post author. Please use a string (maybe insert example?).';

const FrontMatterLastUpdateErrorMessage =
  '{{#label}} does not look like a valid front matter date. Please use a string or date (maybe insert example?).';

Copy link
Collaborator

@slorber slorber Mar 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As author seems to be only a string I wonder removing author key or an author object (with a key and/or name)

const FrontMatterAuthorErrorMessage =
 '{{#label}} does not look like a valid blog post author. Please use a string (maybe insert example?).';

I think you mislead blog post authors and last update author: those are different concepts

https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog#authors

Blog post authors (not just a single one) can be objects, strings (keys)

  export type BlogPostFrontMatterAuthor = Author & {
    /**
     * Will be normalized into the `imageURL` prop.
     */
    image_url?: string;
    /**
     * References an existing author in the authors map.
     */
    key?: string;
  };

  export type BlogPostFrontMatterAuthors =
    | string
    | BlogPostFrontMatterAuthor
    | (string | BlogPostFrontMatterAuthor)[];

Only the last update author is just a string, so your new validation error message proposal is not good.


const FrontMatterLastUpdateErrorMessage =
  '{{#label}} does not look like a valid front matter date. Please use a string or > date (maybe insert example?).';

Not good either, the last update is not a "date", but an object that can have either (or both) a date attribute (Date | string) and an author (string) attribute.

The message doesn't have to explain the types IMHO, just mention that the overall object shape is wrong and valid attribute names. Yaml will auto-convert strings to Dates depending on the format so mentioning it accept both Date + string is kind of overkill.


const BlogFrontMatterSchema = Joi.object<BlogPostFrontMatter>({
id: Joi.string(),
title: Joi.string().allow(''),
Expand Down Expand Up @@ -69,6 +72,15 @@ const BlogFrontMatterSchema = Joi.object<BlogPostFrontMatter>({
hide_table_of_contents: Joi.boolean(),

...FrontMatterTOCHeadingLevels,
last_update: Joi.object({
author: Joi.string(),
date: Joi.date().raw(),
})
.or('author', 'date')
.messages({
'object.missing': FrontMatterLastUpdateErrorMessage,
'object.base': FrontMatterLastUpdateErrorMessage,
}),
OzakIOne marked this conversation as resolved.
Show resolved Hide resolved
})
.messages({
'deprecate.error':
Expand Down
52 changes: 52 additions & 0 deletions packages/docusaurus-plugin-content-blog/src/lastUpdate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* 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(
OzakIOne marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
6 changes: 6 additions & 0 deletions packages/docusaurus-plugin-content-blog/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export const DEFAULT_OPTIONS: PluginOptions = {
authorsMapPath: 'authors.yml',
readingTime: ({content, defaultReadingTime}) => defaultReadingTime({content}),
sortPosts: 'descending',
showLastUpdateTime: false,
showLastUpdateAuthor: false,
};

const PluginOptionSchema = Joi.object<PluginOptions>({
Expand Down Expand Up @@ -134,6 +136,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,
),
}).default(DEFAULT_OPTIONS);

export function validateOptions({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
[key: string]: unknown;
};

export type FileChange = {
OzakIOne marked this conversation as resolved.
Show resolved Hide resolved
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;
};

/**
* Everything is partial/unnormalized, because front matter is always
* preserved as-is. Default values will be applied when generating metadata
Expand Down Expand Up @@ -156,6 +164,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?: FileChange;
};

export type BlogPostFrontMatterAuthor = Author & {
Expand All @@ -180,7 +190,14 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
| BlogPostFrontMatterAuthor
| (string | BlogPostFrontMatterAuthor)[];

export type BlogPostMetadata = {
export type LastUpdateData = {
OzakIOne marked this conversation as resolved.
Show resolved Hide resolved
/** A timestamp in **seconds**, directly acquired from `git log`. */
lastUpdatedAt?: number;
/** The author's name directly acquired from `git log`. */
lastUpdatedBy?: string;
};

export type BlogPostMetadata = LastUpdateData & {
/** Path to the Markdown source, with `@site` alias. */
readonly source: string;
/**
Expand Down Expand Up @@ -421,6 +438,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;
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from '@docusaurus/theme-common/internal';
import type {Props} from '@theme/BlogPostItem/Header/Info';

import LastUpdated from '@theme/LastUpdated';
import styles from './styles.module.css';

// Very simple pluralization: probably good enough for now
Expand Down Expand Up @@ -60,7 +61,8 @@ export default function BlogPostItemHeaderInfo({
className,
}: Props): JSX.Element {
const {metadata} = useBlogPost();
const {date, readingTime} = metadata;
const {date, readingTime, lastUpdatedAt, lastUpdatedBy} = metadata;
console.log('metadata:', metadata);

const dateTimeFormat = useDateTimeFormat({
day: 'numeric',
Expand All @@ -79,6 +81,13 @@ export default function BlogPostItemHeaderInfo({
<>
<Spacer />
<ReadingTime readingTime={readingTime} />
<Spacer />
{(lastUpdatedAt || lastUpdatedBy) && (
<LastUpdated
lastUpdatedAt={lastUpdatedAt}
lastUpdatedBy={lastUpdatedBy}
/>
)}
OzakIOne marked this conversation as resolved.
Show resolved Hide resolved
</>
)}
</div>
Expand Down
2 changes: 2 additions & 0 deletions website/docusaurus.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,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}`;
Expand Down
Loading