@docusaurus/plugin-content-blog 3.4.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/atom.css +75 -0
- package/assets/atom.xsl +92 -0
- package/assets/rss.css +75 -0
- package/assets/rss.xsl +86 -0
- package/lib/authors.d.ts +9 -11
- package/lib/authors.js +42 -64
- package/lib/authorsMap.d.ts +23 -0
- package/lib/authorsMap.js +116 -0
- package/lib/authorsProblems.d.ts +21 -0
- package/lib/authorsProblems.js +51 -0
- package/lib/authorsSocials.d.ts +10 -0
- package/lib/authorsSocials.js +48 -0
- package/lib/blogUtils.d.ts +6 -3
- package/lib/blogUtils.js +29 -14
- package/lib/client/contexts.d.ts +33 -0
- package/lib/client/contexts.js +54 -0
- package/lib/client/index.d.ts +3 -3
- package/lib/client/index.js +3 -9
- package/lib/client/sidebarUtils.d.ts +21 -0
- package/lib/client/sidebarUtils.js +49 -0
- package/lib/client/sidebarUtils.test.d.ts +7 -0
- package/lib/client/sidebarUtils.test.js +43 -0
- package/lib/client/structuredDataUtils.d.ts +10 -0
- package/lib/client/structuredDataUtils.js +122 -0
- package/lib/feed.d.ts +3 -2
- package/lib/feed.js +69 -21
- package/lib/frontMatter.d.ts +0 -1
- package/lib/frontMatter.js +3 -2
- package/lib/index.d.ts +0 -1
- package/lib/index.js +23 -4
- package/lib/markdownLoader.js +1 -1
- package/lib/options.d.ts +4 -1
- package/lib/options.js +98 -26
- package/lib/props.d.ts +9 -2
- package/lib/props.js +21 -3
- package/lib/remark/footnoteIDFixer.js +1 -1
- package/lib/routes.d.ts +0 -1
- package/lib/routes.js +82 -14
- package/lib/translations.d.ts +0 -1
- package/lib/translations.js +2 -3
- package/package.json +13 -10
- package/src/authors.ts +56 -93
- package/src/authorsMap.ts +171 -0
- package/src/authorsProblems.ts +72 -0
- package/src/authorsSocials.ts +64 -0
- package/src/blogUtils.ts +34 -7
- package/src/client/contexts.tsx +95 -0
- package/src/client/index.tsx +24 -0
- package/src/client/sidebarUtils.test.ts +52 -0
- package/src/client/sidebarUtils.tsx +85 -0
- package/src/client/structuredDataUtils.ts +178 -0
- package/src/feed.ts +140 -17
- package/src/frontMatter.ts +2 -0
- package/src/index.ts +31 -1
- package/src/options.ts +123 -32
- package/src/plugin-content-blog.d.ts +150 -12
- package/src/props.ts +39 -1
- package/src/routes.ts +102 -12
- package/src/client/index.ts +0 -20
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {Joi} from '@docusaurus/utils-validation';
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
AuthorSocials,
|
|
12
|
+
SocialPlatformKey,
|
|
13
|
+
} from '@docusaurus/plugin-content-blog';
|
|
14
|
+
|
|
15
|
+
export const AuthorSocialsSchema = Joi.object<AuthorSocials>({
|
|
16
|
+
twitter: Joi.string(),
|
|
17
|
+
github: Joi.string(),
|
|
18
|
+
linkedin: Joi.string(),
|
|
19
|
+
// StackOverflow userIds like '82609' are parsed as numbers by Yaml
|
|
20
|
+
stackoverflow: Joi.alternatives()
|
|
21
|
+
.try(Joi.number(), Joi.string())
|
|
22
|
+
.custom((val) => String(val)),
|
|
23
|
+
x: Joi.string(),
|
|
24
|
+
}).unknown();
|
|
25
|
+
|
|
26
|
+
type PredefinedPlatformNormalizer = (value: string) => string;
|
|
27
|
+
|
|
28
|
+
const PredefinedPlatformNormalizers: Record<
|
|
29
|
+
SocialPlatformKey | string,
|
|
30
|
+
PredefinedPlatformNormalizer
|
|
31
|
+
> = {
|
|
32
|
+
x: (handle: string) => `https://x.com/${handle}`,
|
|
33
|
+
twitter: (handle: string) => `https://twitter.com/${handle}`,
|
|
34
|
+
github: (handle: string) => `https://github.com/${handle}`,
|
|
35
|
+
linkedin: (handle: string) => `https://www.linkedin.com/in/${handle}/`,
|
|
36
|
+
stackoverflow: (userId: string) =>
|
|
37
|
+
`https://stackoverflow.com/users/${userId}`,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type SocialEntry = [string, string];
|
|
41
|
+
|
|
42
|
+
function normalizeSocialEntry([platform, value]: SocialEntry): SocialEntry {
|
|
43
|
+
const normalizer = PredefinedPlatformNormalizers[platform.toLowerCase()];
|
|
44
|
+
const isAbsoluteUrl =
|
|
45
|
+
value.startsWith('http://') || value.startsWith('https://');
|
|
46
|
+
if (isAbsoluteUrl) {
|
|
47
|
+
return [platform, value];
|
|
48
|
+
} else if (value.includes('/')) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Author socials should be usernames/userIds/handles, or fully qualified HTTP(s) absolute URLs.
|
|
51
|
+
Social platform '${platform}' has illegal value '${value}'`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (normalizer && !isAbsoluteUrl) {
|
|
55
|
+
const normalizedPlatform = platform.toLowerCase();
|
|
56
|
+
const normalizedValue = normalizer(value);
|
|
57
|
+
return [normalizedPlatform as SocialPlatformKey, normalizedValue];
|
|
58
|
+
}
|
|
59
|
+
return [platform, value];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const normalizeSocials = (socials: AuthorSocials): AuthorSocials => {
|
|
63
|
+
return Object.fromEntries(Object.entries(socials).map(normalizeSocialEntry));
|
|
64
|
+
};
|
package/src/blogUtils.ts
CHANGED
|
@@ -26,13 +26,16 @@ import {
|
|
|
26
26
|
isDraft,
|
|
27
27
|
readLastUpdateData,
|
|
28
28
|
normalizeTags,
|
|
29
|
+
aliasedSitePathToRelativePath,
|
|
29
30
|
} from '@docusaurus/utils';
|
|
30
31
|
import {getTagsFile} from '@docusaurus/utils-validation';
|
|
31
32
|
import {validateBlogPostFrontMatter} from './frontMatter';
|
|
32
|
-
import {
|
|
33
|
+
import {getBlogPostAuthors} from './authors';
|
|
34
|
+
import {reportAuthorsProblems} from './authorsProblems';
|
|
33
35
|
import type {TagsFile} from '@docusaurus/utils';
|
|
34
36
|
import type {LoadContext, ParseFrontMatter} from '@docusaurus/types';
|
|
35
37
|
import type {
|
|
38
|
+
AuthorsMap,
|
|
36
39
|
PluginOptions,
|
|
37
40
|
ReadingTimeFunction,
|
|
38
41
|
BlogPost,
|
|
@@ -45,6 +48,28 @@ export function truncate(fileString: string, truncateMarker: RegExp): string {
|
|
|
45
48
|
return fileString.split(truncateMarker, 1).shift()!;
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
export function reportUntruncatedBlogPosts({
|
|
52
|
+
blogPosts,
|
|
53
|
+
onUntruncatedBlogPosts,
|
|
54
|
+
}: {
|
|
55
|
+
blogPosts: BlogPost[];
|
|
56
|
+
onUntruncatedBlogPosts: PluginOptions['onUntruncatedBlogPosts'];
|
|
57
|
+
}): void {
|
|
58
|
+
const untruncatedBlogPosts = blogPosts.filter(
|
|
59
|
+
(p) => !p.metadata.hasTruncateMarker,
|
|
60
|
+
);
|
|
61
|
+
if (onUntruncatedBlogPosts !== 'ignore' && untruncatedBlogPosts.length > 0) {
|
|
62
|
+
const message = logger.interpolate`Docusaurus found blog posts without truncation markers:
|
|
63
|
+
- ${untruncatedBlogPosts
|
|
64
|
+
.map((p) => logger.path(aliasedSitePathToRelativePath(p.metadata.source)))
|
|
65
|
+
.join('\n- ')}
|
|
66
|
+
|
|
67
|
+
We recommend using truncation markers (code=${`<!-- truncate -->`} or code=${`{/* truncate */}`}) in blog posts to create shorter previews on blog paginated lists.
|
|
68
|
+
Tip: turn this security off with the code=${`onUntruncatedBlogPosts: 'ignore'`} blog plugin option.`;
|
|
69
|
+
logger.report(onUntruncatedBlogPosts)(message);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
48
73
|
export function paginateBlogPosts({
|
|
49
74
|
blogPosts,
|
|
50
75
|
basePageUrl,
|
|
@@ -63,7 +88,7 @@ export function paginateBlogPosts({
|
|
|
63
88
|
const totalCount = blogPosts.length;
|
|
64
89
|
const postsPerPage =
|
|
65
90
|
postsPerPageOption === 'ALL' ? totalCount : postsPerPageOption;
|
|
66
|
-
const numberOfPages = Math.ceil(totalCount / postsPerPage);
|
|
91
|
+
const numberOfPages = Math.max(1, Math.ceil(totalCount / postsPerPage));
|
|
67
92
|
|
|
68
93
|
const pages: BlogPaginated[] = [];
|
|
69
94
|
|
|
@@ -317,7 +342,13 @@ async function processBlogSourceFile(
|
|
|
317
342
|
routeBasePath,
|
|
318
343
|
tagsRouteBasePath,
|
|
319
344
|
]);
|
|
345
|
+
|
|
320
346
|
const authors = getBlogPostAuthors({authorsMap, frontMatter, baseUrl});
|
|
347
|
+
reportAuthorsProblems({
|
|
348
|
+
authors,
|
|
349
|
+
blogSourceRelative,
|
|
350
|
+
options,
|
|
351
|
+
});
|
|
321
352
|
|
|
322
353
|
const tags = normalizeTags({
|
|
323
354
|
options,
|
|
@@ -359,6 +390,7 @@ export async function generateBlogPosts(
|
|
|
359
390
|
contentPaths: BlogContentPaths,
|
|
360
391
|
context: LoadContext,
|
|
361
392
|
options: PluginOptions,
|
|
393
|
+
authorsMap?: AuthorsMap,
|
|
362
394
|
): Promise<BlogPost[]> {
|
|
363
395
|
const {include, exclude} = options;
|
|
364
396
|
|
|
@@ -371,11 +403,6 @@ export async function generateBlogPosts(
|
|
|
371
403
|
ignore: exclude,
|
|
372
404
|
});
|
|
373
405
|
|
|
374
|
-
const authorsMap = await getAuthorsMap({
|
|
375
|
-
contentPaths,
|
|
376
|
-
authorsMapPath: options.authorsMapPath,
|
|
377
|
-
});
|
|
378
|
-
|
|
379
406
|
const tagsFile = await getTagsFile({contentPaths, tags: options.tags});
|
|
380
407
|
|
|
381
408
|
async function doProcessBlogSourceFile(blogSourceFile: string) {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, {useMemo, type ReactNode, useContext} from 'react';
|
|
9
|
+
import {ReactContextError} from '@docusaurus/theme-common/internal';
|
|
10
|
+
import useRouteContext from '@docusaurus/useRouteContext';
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
PropBlogPostContent,
|
|
14
|
+
BlogMetadata,
|
|
15
|
+
} from '@docusaurus/plugin-content-blog';
|
|
16
|
+
|
|
17
|
+
export function useBlogMetadata(): BlogMetadata {
|
|
18
|
+
const routeContext = useRouteContext();
|
|
19
|
+
const blogMetadata = routeContext?.data?.blogMetadata;
|
|
20
|
+
if (!blogMetadata) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
"useBlogMetadata() can't be called on the current route because the blog metadata could not be found in route context",
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return blogMetadata as BlogMetadata;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The React context value returned by the `useBlogPost()` hook.
|
|
30
|
+
* It contains useful data related to the currently browsed blog post.
|
|
31
|
+
*/
|
|
32
|
+
export type BlogPostContextValue = Pick<
|
|
33
|
+
PropBlogPostContent,
|
|
34
|
+
'metadata' | 'frontMatter' | 'assets' | 'toc'
|
|
35
|
+
> & {
|
|
36
|
+
readonly isBlogPostPage: boolean;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const Context = React.createContext<BlogPostContextValue | null>(null);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Note: we don't use `PropBlogPostContent` as context value on purpose.
|
|
43
|
+
* Metadata is currently stored inside the MDX component, but we may want to
|
|
44
|
+
* change that in the future.
|
|
45
|
+
*/
|
|
46
|
+
function useContextValue({
|
|
47
|
+
content,
|
|
48
|
+
isBlogPostPage,
|
|
49
|
+
}: {
|
|
50
|
+
content: PropBlogPostContent;
|
|
51
|
+
isBlogPostPage: boolean;
|
|
52
|
+
}): BlogPostContextValue {
|
|
53
|
+
return useMemo(
|
|
54
|
+
() => ({
|
|
55
|
+
metadata: content.metadata,
|
|
56
|
+
frontMatter: content.frontMatter,
|
|
57
|
+
assets: content.assets,
|
|
58
|
+
toc: content.toc,
|
|
59
|
+
isBlogPostPage,
|
|
60
|
+
}),
|
|
61
|
+
[content, isBlogPostPage],
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* This is a very thin layer around the `content` received from the MDX loader.
|
|
67
|
+
* It provides metadata about the blog post to the children tree.
|
|
68
|
+
*/
|
|
69
|
+
export function BlogPostProvider({
|
|
70
|
+
children,
|
|
71
|
+
content,
|
|
72
|
+
isBlogPostPage = false,
|
|
73
|
+
}: {
|
|
74
|
+
children: ReactNode;
|
|
75
|
+
content: PropBlogPostContent;
|
|
76
|
+
isBlogPostPage?: boolean;
|
|
77
|
+
}): JSX.Element {
|
|
78
|
+
const contextValue = useContextValue({content, isBlogPostPage});
|
|
79
|
+
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Returns the data of the currently browsed blog post. Gives access to
|
|
84
|
+
* front matter, metadata, TOC, etc.
|
|
85
|
+
* When swizzling a low-level component (e.g. the "Edit this page" link)
|
|
86
|
+
* and you need some extra metadata, you don't have to drill the props
|
|
87
|
+
* all the way through the component tree: simply use this hook instead.
|
|
88
|
+
*/
|
|
89
|
+
export function useBlogPost(): BlogPostContextValue {
|
|
90
|
+
const blogPost = useContext(Context);
|
|
91
|
+
if (blogPost === null) {
|
|
92
|
+
throw new ReactContextError('BlogPostProvider');
|
|
93
|
+
}
|
|
94
|
+
return blogPost;
|
|
95
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
BlogPostProvider,
|
|
10
|
+
type BlogPostContextValue,
|
|
11
|
+
useBlogPost,
|
|
12
|
+
useBlogMetadata,
|
|
13
|
+
} from './contexts';
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
useBlogListPageStructuredData,
|
|
17
|
+
useBlogPostStructuredData,
|
|
18
|
+
} from './structuredDataUtils';
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
BlogSidebarItemList,
|
|
22
|
+
groupBlogSidebarItemsByYear,
|
|
23
|
+
useVisibleBlogSidebarItems,
|
|
24
|
+
} from './sidebarUtils';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {groupBlogSidebarItemsByYear} from './sidebarUtils';
|
|
9
|
+
import type {BlogSidebarItem} from '@docusaurus/plugin-content-blog';
|
|
10
|
+
|
|
11
|
+
describe('groupBlogSidebarItemsByYear', () => {
|
|
12
|
+
const post1: BlogSidebarItem = {
|
|
13
|
+
title: 'post1',
|
|
14
|
+
permalink: '/post1',
|
|
15
|
+
date: '2024-10-03',
|
|
16
|
+
unlisted: false,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const post2: BlogSidebarItem = {
|
|
20
|
+
title: 'post2',
|
|
21
|
+
permalink: '/post2',
|
|
22
|
+
date: '2024-05-02',
|
|
23
|
+
unlisted: false,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const post3: BlogSidebarItem = {
|
|
27
|
+
title: 'post3',
|
|
28
|
+
permalink: '/post3',
|
|
29
|
+
date: '2022-11-18',
|
|
30
|
+
unlisted: false,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
it('can group items by year', () => {
|
|
34
|
+
const items: BlogSidebarItem[] = [post1, post2, post3];
|
|
35
|
+
const entries = groupBlogSidebarItemsByYear(items);
|
|
36
|
+
|
|
37
|
+
expect(entries).toEqual([
|
|
38
|
+
['2024', [post1, post2]],
|
|
39
|
+
['2022', [post3]],
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('always returns result in descending chronological order', () => {
|
|
44
|
+
const items: BlogSidebarItem[] = [post3, post1, post2];
|
|
45
|
+
const entries = groupBlogSidebarItemsByYear(items);
|
|
46
|
+
|
|
47
|
+
expect(entries).toEqual([
|
|
48
|
+
['2024', [post1, post2]],
|
|
49
|
+
['2022', [post3]],
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, {type ReactNode, useMemo} from 'react';
|
|
9
|
+
import {useLocation} from '@docusaurus/router';
|
|
10
|
+
import Link from '@docusaurus/Link';
|
|
11
|
+
import {groupBy} from '@docusaurus/theme-common';
|
|
12
|
+
import {isSamePath} from '@docusaurus/theme-common/internal';
|
|
13
|
+
import type {BlogSidebarItem} from '@docusaurus/plugin-content-blog';
|
|
14
|
+
|
|
15
|
+
function isVisible(item: BlogSidebarItem, pathname: string): boolean {
|
|
16
|
+
if (item.unlisted && !isSamePath(item.permalink, pathname)) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Return the visible blog sidebar items to display.
|
|
24
|
+
* Unlisted items are filtered.
|
|
25
|
+
*/
|
|
26
|
+
export function useVisibleBlogSidebarItems(
|
|
27
|
+
items: BlogSidebarItem[],
|
|
28
|
+
): BlogSidebarItem[] {
|
|
29
|
+
const {pathname} = useLocation();
|
|
30
|
+
return useMemo(
|
|
31
|
+
() => items.filter((item) => isVisible(item, pathname)),
|
|
32
|
+
[items, pathname],
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function groupBlogSidebarItemsByYear(
|
|
37
|
+
items: BlogSidebarItem[],
|
|
38
|
+
): [string, BlogSidebarItem[]][] {
|
|
39
|
+
const groupedByYear = groupBy(items, (item) => {
|
|
40
|
+
return `${new Date(item.date).getFullYear()}`;
|
|
41
|
+
});
|
|
42
|
+
// "as" is safe here
|
|
43
|
+
// see https://github.com/microsoft/TypeScript/pull/56805#issuecomment-2196526425
|
|
44
|
+
const entries = Object.entries(groupedByYear) as [
|
|
45
|
+
string,
|
|
46
|
+
BlogSidebarItem[],
|
|
47
|
+
][];
|
|
48
|
+
// We have to use entries because of https://x.com/sebastienlorber/status/1806371668614369486
|
|
49
|
+
// Objects with string/number keys are automatically sorted asc...
|
|
50
|
+
// Even if keys are strings like "2024"
|
|
51
|
+
// We want descending order for years
|
|
52
|
+
// Alternative: using Map.groupBy (not affected by this "reordering")
|
|
53
|
+
entries.reverse();
|
|
54
|
+
return entries;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function BlogSidebarItemList({
|
|
58
|
+
items,
|
|
59
|
+
ulClassName,
|
|
60
|
+
liClassName,
|
|
61
|
+
linkClassName,
|
|
62
|
+
linkActiveClassName,
|
|
63
|
+
}: {
|
|
64
|
+
items: BlogSidebarItem[];
|
|
65
|
+
ulClassName?: string;
|
|
66
|
+
liClassName?: string;
|
|
67
|
+
linkClassName?: string;
|
|
68
|
+
linkActiveClassName?: string;
|
|
69
|
+
}): ReactNode {
|
|
70
|
+
return (
|
|
71
|
+
<ul className={ulClassName}>
|
|
72
|
+
{items.map((item) => (
|
|
73
|
+
<li key={item.permalink} className={liClassName}>
|
|
74
|
+
<Link
|
|
75
|
+
isNavLink
|
|
76
|
+
to={item.permalink}
|
|
77
|
+
className={linkClassName}
|
|
78
|
+
activeClassName={linkActiveClassName}>
|
|
79
|
+
{item.title}
|
|
80
|
+
</Link>
|
|
81
|
+
</li>
|
|
82
|
+
))}
|
|
83
|
+
</ul>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {useBaseUrlUtils, type BaseUrlUtils} from '@docusaurus/useBaseUrl';
|
|
9
|
+
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|
10
|
+
import {useBlogMetadata} from '@docusaurus/plugin-content-blog/client';
|
|
11
|
+
import type {Props as BlogListPageStructuredDataProps} from '@theme/BlogListPage/StructuredData';
|
|
12
|
+
import {useBlogPost} from './contexts';
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
Blog,
|
|
16
|
+
BlogPosting,
|
|
17
|
+
WithContext,
|
|
18
|
+
Person,
|
|
19
|
+
ImageObject,
|
|
20
|
+
} from 'schema-dts';
|
|
21
|
+
import type {
|
|
22
|
+
Author,
|
|
23
|
+
PropBlogPostContent,
|
|
24
|
+
} from '@docusaurus/plugin-content-blog';
|
|
25
|
+
import type {DocusaurusConfig} from '@docusaurus/types';
|
|
26
|
+
|
|
27
|
+
const convertDate = (dateMs: number) => new Date(dateMs).toISOString();
|
|
28
|
+
|
|
29
|
+
function getBlogPost(
|
|
30
|
+
blogPostContent: PropBlogPostContent,
|
|
31
|
+
siteConfig: DocusaurusConfig,
|
|
32
|
+
withBaseUrl: BaseUrlUtils['withBaseUrl'],
|
|
33
|
+
): BlogPosting {
|
|
34
|
+
const {assets, frontMatter, metadata} = blogPostContent;
|
|
35
|
+
const {date, title, description, lastUpdatedAt} = metadata;
|
|
36
|
+
|
|
37
|
+
const image = assets.image ?? frontMatter.image;
|
|
38
|
+
const keywords = frontMatter.keywords ?? [];
|
|
39
|
+
|
|
40
|
+
const blogUrl = `${siteConfig.url}${metadata.permalink}`;
|
|
41
|
+
|
|
42
|
+
const dateModified = lastUpdatedAt ? convertDate(lastUpdatedAt) : undefined;
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
'@type': 'BlogPosting',
|
|
46
|
+
'@id': blogUrl,
|
|
47
|
+
mainEntityOfPage: blogUrl,
|
|
48
|
+
url: blogUrl,
|
|
49
|
+
headline: title,
|
|
50
|
+
name: title,
|
|
51
|
+
description,
|
|
52
|
+
datePublished: date,
|
|
53
|
+
...(dateModified ? {dateModified} : {}),
|
|
54
|
+
...getAuthor(metadata.authors),
|
|
55
|
+
...getImage(image, withBaseUrl, title),
|
|
56
|
+
...(keywords ? {keywords} : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getAuthor(authors: Author[]) {
|
|
61
|
+
const authorsStructuredData = authors.map(createPersonStructuredData);
|
|
62
|
+
return {
|
|
63
|
+
author:
|
|
64
|
+
authorsStructuredData.length === 1
|
|
65
|
+
? authorsStructuredData[0]
|
|
66
|
+
: authorsStructuredData,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getImage(
|
|
71
|
+
image: string | undefined,
|
|
72
|
+
withBaseUrl: BaseUrlUtils['withBaseUrl'],
|
|
73
|
+
title: string,
|
|
74
|
+
) {
|
|
75
|
+
return image
|
|
76
|
+
? {
|
|
77
|
+
image: createImageStructuredData({
|
|
78
|
+
imageUrl: withBaseUrl(image, {absolute: true}),
|
|
79
|
+
caption: `title image for the blog post: ${title}`,
|
|
80
|
+
}),
|
|
81
|
+
}
|
|
82
|
+
: {};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function useBlogListPageStructuredData(
|
|
86
|
+
props: BlogListPageStructuredDataProps,
|
|
87
|
+
): WithContext<Blog> {
|
|
88
|
+
const {siteConfig} = useDocusaurusContext();
|
|
89
|
+
const {withBaseUrl} = useBaseUrlUtils();
|
|
90
|
+
|
|
91
|
+
const {
|
|
92
|
+
metadata: {blogDescription, blogTitle, permalink},
|
|
93
|
+
} = props;
|
|
94
|
+
|
|
95
|
+
const url = `${siteConfig.url}${permalink}`;
|
|
96
|
+
|
|
97
|
+
// details on structured data support: https://schema.org/Blog
|
|
98
|
+
return {
|
|
99
|
+
'@context': 'https://schema.org',
|
|
100
|
+
'@type': 'Blog',
|
|
101
|
+
'@id': url,
|
|
102
|
+
mainEntityOfPage: url,
|
|
103
|
+
headline: blogTitle,
|
|
104
|
+
description: blogDescription,
|
|
105
|
+
blogPost: props.items.map((blogItem) =>
|
|
106
|
+
getBlogPost(blogItem.content, siteConfig, withBaseUrl),
|
|
107
|
+
),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function useBlogPostStructuredData(): WithContext<BlogPosting> {
|
|
112
|
+
const blogMetadata = useBlogMetadata();
|
|
113
|
+
const {assets, metadata} = useBlogPost();
|
|
114
|
+
const {siteConfig} = useDocusaurusContext();
|
|
115
|
+
const {withBaseUrl} = useBaseUrlUtils();
|
|
116
|
+
|
|
117
|
+
const {date, title, description, frontMatter, lastUpdatedAt} = metadata;
|
|
118
|
+
|
|
119
|
+
const image = assets.image ?? frontMatter.image;
|
|
120
|
+
const keywords = frontMatter.keywords ?? [];
|
|
121
|
+
|
|
122
|
+
const dateModified = lastUpdatedAt ? convertDate(lastUpdatedAt) : undefined;
|
|
123
|
+
|
|
124
|
+
const url = `${siteConfig.url}${metadata.permalink}`;
|
|
125
|
+
|
|
126
|
+
// details on structured data support: https://schema.org/BlogPosting
|
|
127
|
+
// BlogPosting is one of the structured data types that Google explicitly
|
|
128
|
+
// supports: https://developers.google.com/search/docs/appearance/structured-data/article#structured-data-type-definitions
|
|
129
|
+
return {
|
|
130
|
+
'@context': 'https://schema.org',
|
|
131
|
+
'@type': 'BlogPosting',
|
|
132
|
+
'@id': url,
|
|
133
|
+
mainEntityOfPage: url,
|
|
134
|
+
url,
|
|
135
|
+
headline: title,
|
|
136
|
+
name: title,
|
|
137
|
+
description,
|
|
138
|
+
datePublished: date,
|
|
139
|
+
...(dateModified ? {dateModified} : {}),
|
|
140
|
+
...getAuthor(metadata.authors),
|
|
141
|
+
...getImage(image, withBaseUrl, title),
|
|
142
|
+
...(keywords ? {keywords} : {}),
|
|
143
|
+
isPartOf: {
|
|
144
|
+
'@type': 'Blog',
|
|
145
|
+
'@id': `${siteConfig.url}${blogMetadata.blogBasePath}`,
|
|
146
|
+
name: blogMetadata.blogTitle,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** @returns A {@link https://schema.org/Person} constructed from the {@link Author} */
|
|
152
|
+
function createPersonStructuredData(author: Author): Person {
|
|
153
|
+
return {
|
|
154
|
+
'@type': 'Person',
|
|
155
|
+
...(author.name ? {name: author.name} : {}),
|
|
156
|
+
...(author.title ? {description: author.title} : {}),
|
|
157
|
+
...(author.url ? {url: author.url} : {}),
|
|
158
|
+
...(author.email ? {email: author.email} : {}),
|
|
159
|
+
...(author.imageURL ? {image: author.imageURL} : {}),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** @returns A {@link https://schema.org/ImageObject} */
|
|
164
|
+
function createImageStructuredData({
|
|
165
|
+
imageUrl,
|
|
166
|
+
caption,
|
|
167
|
+
}: {
|
|
168
|
+
imageUrl: string;
|
|
169
|
+
caption: string;
|
|
170
|
+
}): ImageObject {
|
|
171
|
+
return {
|
|
172
|
+
'@type': 'ImageObject',
|
|
173
|
+
'@id': imageUrl,
|
|
174
|
+
url: imageUrl,
|
|
175
|
+
contentUrl: imageUrl,
|
|
176
|
+
caption,
|
|
177
|
+
};
|
|
178
|
+
}
|