@docusaurus/plugin-content-blog 3.3.2 → 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.
Files changed (62) hide show
  1. package/assets/atom.css +75 -0
  2. package/assets/atom.xsl +92 -0
  3. package/assets/rss.css +75 -0
  4. package/assets/rss.xsl +86 -0
  5. package/lib/authors.d.ts +9 -11
  6. package/lib/authors.js +42 -64
  7. package/lib/authorsMap.d.ts +23 -0
  8. package/lib/authorsMap.js +116 -0
  9. package/lib/authorsProblems.d.ts +21 -0
  10. package/lib/authorsProblems.js +51 -0
  11. package/lib/authorsSocials.d.ts +10 -0
  12. package/lib/authorsSocials.js +48 -0
  13. package/lib/blogUtils.d.ts +7 -12
  14. package/lib/blogUtils.js +44 -34
  15. package/lib/client/contexts.d.ts +33 -0
  16. package/lib/client/contexts.js +54 -0
  17. package/lib/client/index.d.ts +3 -3
  18. package/lib/client/index.js +3 -9
  19. package/lib/client/sidebarUtils.d.ts +21 -0
  20. package/lib/client/sidebarUtils.js +49 -0
  21. package/lib/client/sidebarUtils.test.d.ts +7 -0
  22. package/lib/client/sidebarUtils.test.js +43 -0
  23. package/lib/client/structuredDataUtils.d.ts +10 -0
  24. package/lib/client/structuredDataUtils.js +122 -0
  25. package/lib/feed.d.ts +8 -3
  26. package/lib/feed.js +111 -20
  27. package/lib/frontMatter.d.ts +0 -1
  28. package/lib/frontMatter.js +3 -2
  29. package/lib/index.d.ts +0 -1
  30. package/lib/index.js +132 -105
  31. package/lib/markdownLoader.js +3 -7
  32. package/lib/options.d.ts +4 -1
  33. package/lib/options.js +107 -26
  34. package/lib/props.d.ts +9 -2
  35. package/lib/props.js +23 -3
  36. package/lib/remark/footnoteIDFixer.js +1 -1
  37. package/lib/routes.d.ts +0 -1
  38. package/lib/routes.js +82 -14
  39. package/lib/translations.d.ts +0 -1
  40. package/lib/translations.js +2 -3
  41. package/lib/types.d.ts +1 -8
  42. package/package.json +13 -10
  43. package/src/authors.ts +56 -93
  44. package/src/authorsMap.ts +171 -0
  45. package/src/authorsProblems.ts +72 -0
  46. package/src/authorsSocials.ts +64 -0
  47. package/src/blogUtils.ts +51 -46
  48. package/src/client/contexts.tsx +95 -0
  49. package/src/client/index.tsx +24 -0
  50. package/src/client/sidebarUtils.test.ts +52 -0
  51. package/src/client/sidebarUtils.tsx +85 -0
  52. package/src/client/structuredDataUtils.ts +178 -0
  53. package/src/feed.ts +197 -18
  54. package/src/frontMatter.ts +2 -0
  55. package/src/index.ts +182 -137
  56. package/src/markdownLoader.ts +3 -7
  57. package/src/options.ts +132 -32
  58. package/src/plugin-content-blog.d.ts +252 -113
  59. package/src/props.ts +41 -1
  60. package/src/routes.ts +102 -12
  61. package/src/types.ts +1 -6
  62. package/src/client/index.ts +0 -20
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) Facebook, Inc. and its affiliates.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.reportAuthorsProblems = reportAuthorsProblems;
10
+ exports.reportInlineAuthors = reportInlineAuthors;
11
+ exports.reportDuplicateAuthors = reportDuplicateAuthors;
12
+ const tslib_1 = require("tslib");
13
+ const lodash_1 = tslib_1.__importDefault(require("lodash"));
14
+ const logger_1 = tslib_1.__importDefault(require("@docusaurus/logger"));
15
+ function reportAuthorsProblems(params) {
16
+ reportInlineAuthors(params);
17
+ reportDuplicateAuthors(params);
18
+ }
19
+ function reportInlineAuthors({ authors, blogSourceRelative, options: { onInlineAuthors, authorsMapPath }, }) {
20
+ if (onInlineAuthors === 'ignore') {
21
+ return;
22
+ }
23
+ const inlineAuthors = authors.filter((author) => !author.key);
24
+ if (inlineAuthors.length > 0) {
25
+ logger_1.default.report(onInlineAuthors)(logger_1.default.interpolate `Some blog authors used in path=${blogSourceRelative} are not defined in path=${authorsMapPath}:
26
+ - ${inlineAuthors.map(authorToString).join('\n- ')}
27
+
28
+ Note that we recommend to declare authors once in a path=${authorsMapPath} file and reference them by key in blog posts front matter to avoid author info duplication.
29
+ But if you want to allow inline blog authors, you can disable this message by setting onInlineAuthors: 'ignore' in your blog plugin options.
30
+ More info at url=${'https://docusaurus.io/docs/blog'}
31
+ `);
32
+ }
33
+ }
34
+ function reportDuplicateAuthors({ authors, blogSourceRelative, }) {
35
+ const duplicateAuthors = (0, lodash_1.default)(authors)
36
+ // for now we only check for predefined authors duplicates
37
+ .filter((author) => !!author.key)
38
+ .groupBy((author) => author.key)
39
+ .pickBy((authorsByKey) => authorsByKey.length > 1)
40
+ // We only keep the "test" of all the duplicate groups
41
+ // The first author of a group is not really a duplicate...
42
+ .flatMap(([, ...rest]) => rest)
43
+ .value();
44
+ if (duplicateAuthors.length > 0) {
45
+ throw new Error(logger_1.default.interpolate `Duplicate blog post authors were found in blog post path=${blogSourceRelative} front matter:
46
+ - ${duplicateAuthors.map(authorToString).join('\n- ')}`);
47
+ }
48
+ }
49
+ function authorToString(author) {
50
+ return JSON.stringify(author);
51
+ }
@@ -0,0 +1,10 @@
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
+ import { Joi } from '@docusaurus/utils-validation';
8
+ import type { AuthorSocials } from '@docusaurus/plugin-content-blog';
9
+ export declare const AuthorSocialsSchema: Joi.ObjectSchema<AuthorSocials>;
10
+ export declare const normalizeSocials: (socials: AuthorSocials) => AuthorSocials;
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) Facebook, Inc. and its affiliates.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.normalizeSocials = exports.AuthorSocialsSchema = void 0;
10
+ const utils_validation_1 = require("@docusaurus/utils-validation");
11
+ exports.AuthorSocialsSchema = utils_validation_1.Joi.object({
12
+ twitter: utils_validation_1.Joi.string(),
13
+ github: utils_validation_1.Joi.string(),
14
+ linkedin: utils_validation_1.Joi.string(),
15
+ // StackOverflow userIds like '82609' are parsed as numbers by Yaml
16
+ stackoverflow: utils_validation_1.Joi.alternatives()
17
+ .try(utils_validation_1.Joi.number(), utils_validation_1.Joi.string())
18
+ .custom((val) => String(val)),
19
+ x: utils_validation_1.Joi.string(),
20
+ }).unknown();
21
+ const PredefinedPlatformNormalizers = {
22
+ x: (handle) => `https://x.com/${handle}`,
23
+ twitter: (handle) => `https://twitter.com/${handle}`,
24
+ github: (handle) => `https://github.com/${handle}`,
25
+ linkedin: (handle) => `https://www.linkedin.com/in/${handle}/`,
26
+ stackoverflow: (userId) => `https://stackoverflow.com/users/${userId}`,
27
+ };
28
+ function normalizeSocialEntry([platform, value]) {
29
+ const normalizer = PredefinedPlatformNormalizers[platform.toLowerCase()];
30
+ const isAbsoluteUrl = value.startsWith('http://') || value.startsWith('https://');
31
+ if (isAbsoluteUrl) {
32
+ return [platform, value];
33
+ }
34
+ else if (value.includes('/')) {
35
+ throw new Error(`Author socials should be usernames/userIds/handles, or fully qualified HTTP(s) absolute URLs.
36
+ Social platform '${platform}' has illegal value '${value}'`);
37
+ }
38
+ if (normalizer && !isAbsoluteUrl) {
39
+ const normalizedPlatform = platform.toLowerCase();
40
+ const normalizedValue = normalizer(value);
41
+ return [normalizedPlatform, normalizedValue];
42
+ }
43
+ return [platform, value];
44
+ }
45
+ const normalizeSocials = (socials) => {
46
+ return Object.fromEntries(Object.entries(socials).map(normalizeSocialEntry));
47
+ };
48
+ exports.normalizeSocials = normalizeSocials;
@@ -4,14 +4,14 @@
4
4
  * This source code is licensed under the MIT license found in the
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
- /// <reference path="../src/plugin-content-blog.d.ts" />
8
7
  import type { LoadContext } from '@docusaurus/types';
9
- import type { PluginOptions, BlogPost, BlogTags, BlogPaginated } from '@docusaurus/plugin-content-blog';
10
- import type { BlogContentPaths, BlogMarkdownLoaderOptions } from './types';
8
+ import type { AuthorsMap, PluginOptions, BlogPost, BlogTags, BlogPaginated } from '@docusaurus/plugin-content-blog';
9
+ import type { BlogContentPaths } from './types';
11
10
  export declare function truncate(fileString: string, truncateMarker: RegExp): string;
12
- export declare function getSourceToPermalink(blogPosts: BlogPost[]): {
13
- [aliasedPath: string]: string;
14
- };
11
+ export declare function reportUntruncatedBlogPosts({ blogPosts, onUntruncatedBlogPosts, }: {
12
+ blogPosts: BlogPost[];
13
+ onUntruncatedBlogPosts: PluginOptions['onUntruncatedBlogPosts'];
14
+ }): void;
15
15
  export declare function paginateBlogPosts({ blogPosts, basePageUrl, blogTitle, blogDescription, postsPerPageOption, pageBasePath, }: {
16
16
  blogPosts: BlogPost[];
17
17
  basePageUrl: string;
@@ -34,12 +34,7 @@ type ParsedBlogFileName = {
34
34
  slug: string;
35
35
  };
36
36
  export declare function parseBlogFileName(blogSourceRelative: string): ParsedBlogFileName;
37
- export declare function generateBlogPosts(contentPaths: BlogContentPaths, context: LoadContext, options: PluginOptions): Promise<BlogPost[]>;
38
- export type LinkifyParams = {
39
- filePath: string;
40
- fileString: string;
41
- } & Pick<BlogMarkdownLoaderOptions, 'sourceToPermalink' | 'siteDir' | 'contentPaths' | 'onBrokenMarkdownLink'>;
42
- export declare function linkify({ filePath, contentPaths, fileString, siteDir, sourceToPermalink, onBrokenMarkdownLink, }: LinkifyParams): string;
37
+ export declare function generateBlogPosts(contentPaths: BlogContentPaths, context: LoadContext, options: PluginOptions, authorsMap?: AuthorsMap): Promise<BlogPost[]>;
43
38
  export declare function applyProcessBlogPosts({ blogPosts, processBlogPosts, }: {
44
39
  blogPosts: BlogPost[];
45
40
  processBlogPosts: PluginOptions['processBlogPosts'];
package/lib/blogUtils.js CHANGED
@@ -6,7 +6,14 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.applyProcessBlogPosts = exports.linkify = exports.generateBlogPosts = exports.parseBlogFileName = exports.getBlogTags = exports.shouldBeListed = exports.paginateBlogPosts = exports.getSourceToPermalink = exports.truncate = void 0;
9
+ exports.truncate = truncate;
10
+ exports.reportUntruncatedBlogPosts = reportUntruncatedBlogPosts;
11
+ exports.paginateBlogPosts = paginateBlogPosts;
12
+ exports.shouldBeListed = shouldBeListed;
13
+ exports.getBlogTags = getBlogTags;
14
+ exports.parseBlogFileName = parseBlogFileName;
15
+ exports.generateBlogPosts = generateBlogPosts;
16
+ exports.applyProcessBlogPosts = applyProcessBlogPosts;
10
17
  const tslib_1 = require("tslib");
11
18
  const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
12
19
  const path_1 = tslib_1.__importDefault(require("path"));
@@ -14,20 +21,30 @@ const lodash_1 = tslib_1.__importDefault(require("lodash"));
14
21
  const logger_1 = tslib_1.__importDefault(require("@docusaurus/logger"));
15
22
  const reading_time_1 = tslib_1.__importDefault(require("reading-time"));
16
23
  const utils_1 = require("@docusaurus/utils");
24
+ const utils_validation_1 = require("@docusaurus/utils-validation");
17
25
  const frontMatter_1 = require("./frontMatter");
18
26
  const authors_1 = require("./authors");
27
+ const authorsProblems_1 = require("./authorsProblems");
19
28
  function truncate(fileString, truncateMarker) {
20
29
  return fileString.split(truncateMarker, 1).shift();
21
30
  }
22
- exports.truncate = truncate;
23
- function getSourceToPermalink(blogPosts) {
24
- return Object.fromEntries(blogPosts.map(({ metadata: { source, permalink } }) => [source, permalink]));
31
+ function reportUntruncatedBlogPosts({ blogPosts, onUntruncatedBlogPosts, }) {
32
+ const untruncatedBlogPosts = blogPosts.filter((p) => !p.metadata.hasTruncateMarker);
33
+ if (onUntruncatedBlogPosts !== 'ignore' && untruncatedBlogPosts.length > 0) {
34
+ const message = logger_1.default.interpolate `Docusaurus found blog posts without truncation markers:
35
+ - ${untruncatedBlogPosts
36
+ .map((p) => logger_1.default.path((0, utils_1.aliasedSitePathToRelativePath)(p.metadata.source)))
37
+ .join('\n- ')}
38
+
39
+ We recommend using truncation markers (code=${`<!-- truncate -->`} or code=${`{/* truncate */}`}) in blog posts to create shorter previews on blog paginated lists.
40
+ Tip: turn this security off with the code=${`onUntruncatedBlogPosts: 'ignore'`} blog plugin option.`;
41
+ logger_1.default.report(onUntruncatedBlogPosts)(message);
42
+ }
25
43
  }
26
- exports.getSourceToPermalink = getSourceToPermalink;
27
44
  function paginateBlogPosts({ blogPosts, basePageUrl, blogTitle, blogDescription, postsPerPageOption, pageBasePath, }) {
28
45
  const totalCount = blogPosts.length;
29
46
  const postsPerPage = postsPerPageOption === 'ALL' ? totalCount : postsPerPageOption;
30
- const numberOfPages = Math.ceil(totalCount / postsPerPage);
47
+ const numberOfPages = Math.max(1, Math.ceil(totalCount / postsPerPage));
31
48
  const pages = [];
32
49
  function permalink(page) {
33
50
  return page > 0
@@ -54,11 +71,9 @@ function paginateBlogPosts({ blogPosts, basePageUrl, blogTitle, blogDescription,
54
71
  }
55
72
  return pages;
56
73
  }
57
- exports.paginateBlogPosts = paginateBlogPosts;
58
74
  function shouldBeListed(blogPost) {
59
75
  return !blogPost.metadata.unlisted;
60
76
  }
61
- exports.shouldBeListed = shouldBeListed;
62
77
  function getBlogTags({ blogPosts, ...params }) {
63
78
  const groups = (0, utils_1.groupTaggedItems)(blogPosts, (blogPost) => blogPost.metadata.tags);
64
79
  return lodash_1.default.mapValues(groups, ({ tag, items: tagBlogPosts }) => {
@@ -67,9 +82,11 @@ function getBlogTags({ blogPosts, ...params }) {
67
82
  isUnlisted: (item) => item.metadata.unlisted,
68
83
  });
69
84
  return {
85
+ inline: tag.inline,
70
86
  label: tag.label,
71
- items: tagVisibility.listedItems.map((item) => item.id),
72
87
  permalink: tag.permalink,
88
+ description: tag.description,
89
+ items: tagVisibility.listedItems.map((item) => item.id),
73
90
  pages: paginateBlogPosts({
74
91
  blogPosts: tagVisibility.listedItems,
75
92
  basePageUrl: tag.permalink,
@@ -79,7 +96,6 @@ function getBlogTags({ blogPosts, ...params }) {
79
96
  };
80
97
  });
81
98
  }
82
- exports.getBlogTags = getBlogTags;
83
99
  const DATE_FILENAME_REGEX = /^(?<folder>.*)(?<date>\d{4}[-/]\d{1,2}[-/]\d{1,2})[-/]?(?<text>.*?)(?:\/index)?.mdx?$/;
84
100
  function parseBlogFileName(blogSourceRelative) {
85
101
  const dateFilenameMatch = blogSourceRelative.match(DATE_FILENAME_REGEX);
@@ -95,7 +111,6 @@ function parseBlogFileName(blogSourceRelative) {
95
111
  const slug = `/${text}`;
96
112
  return { date: undefined, text, slug };
97
113
  }
98
- exports.parseBlogFileName = parseBlogFileName;
99
114
  async function parseBlogPostMarkdownFile({ filePath, parseFrontMatter, }) {
100
115
  const fileContent = await fs_extra_1.default.readFile(filePath, 'utf-8');
101
116
  try {
@@ -116,7 +131,7 @@ async function parseBlogPostMarkdownFile({ filePath, parseFrontMatter, }) {
116
131
  }
117
132
  }
118
133
  const defaultReadingTime = ({ content, options }) => (0, reading_time_1.default)(content, options).minutes;
119
- async function processBlogSourceFile(blogSourceRelative, contentPaths, context, options, authorsMap) {
134
+ async function processBlogSourceFile(blogSourceRelative, contentPaths, context, options, tagsFile, authorsMap) {
120
135
  const { siteConfig: { baseUrl, markdown: { parseFrontMatter }, }, siteDir, i18n, } = context;
121
136
  const { routeBasePath, tagsBasePath: tagsRouteBasePath, truncateMarker, showReadingTime, editUrl, } = options;
122
137
  // Lookup in localized folder in priority
@@ -190,12 +205,24 @@ async function processBlogSourceFile(blogSourceRelative, contentPaths, context,
190
205
  }
191
206
  return undefined;
192
207
  }
193
- const tagsBasePath = (0, utils_1.normalizeUrl)([
208
+ const tagsBaseRoutePath = (0, utils_1.normalizeUrl)([
194
209
  baseUrl,
195
210
  routeBasePath,
196
211
  tagsRouteBasePath,
197
212
  ]);
198
213
  const authors = (0, authors_1.getBlogPostAuthors)({ authorsMap, frontMatter, baseUrl });
214
+ (0, authorsProblems_1.reportAuthorsProblems)({
215
+ authors,
216
+ blogSourceRelative,
217
+ options,
218
+ });
219
+ const tags = (0, utils_1.normalizeTags)({
220
+ options,
221
+ source: blogSourceRelative,
222
+ frontMatterTags: frontMatter.tags,
223
+ tagsBaseRoutePath,
224
+ tagsFile,
225
+ });
199
226
  return {
200
227
  id: slug,
201
228
  metadata: {
@@ -205,7 +232,7 @@ async function processBlogSourceFile(blogSourceRelative, contentPaths, context,
205
232
  title,
206
233
  description,
207
234
  date,
208
- tags: (0, utils_1.normalizeFrontMatterTags)(tagsBasePath, frontMatter.tags),
235
+ tags,
209
236
  readingTime: showReadingTime
210
237
  ? options.readingTime({
211
238
  content,
@@ -223,7 +250,7 @@ async function processBlogSourceFile(blogSourceRelative, contentPaths, context,
223
250
  content,
224
251
  };
225
252
  }
226
- async function generateBlogPosts(contentPaths, context, options) {
253
+ async function generateBlogPosts(contentPaths, context, options, authorsMap) {
227
254
  const { include, exclude } = options;
228
255
  if (!(await fs_extra_1.default.pathExists(contentPaths.contentPath))) {
229
256
  return [];
@@ -232,13 +259,10 @@ async function generateBlogPosts(contentPaths, context, options) {
232
259
  cwd: contentPaths.contentPath,
233
260
  ignore: exclude,
234
261
  });
235
- const authorsMap = await (0, authors_1.getAuthorsMap)({
236
- contentPaths,
237
- authorsMapPath: options.authorsMapPath,
238
- });
262
+ const tagsFile = await (0, utils_validation_1.getTagsFile)({ contentPaths, tags: options.tags });
239
263
  async function doProcessBlogSourceFile(blogSourceFile) {
240
264
  try {
241
- return await processBlogSourceFile(blogSourceFile, contentPaths, context, options, authorsMap);
265
+ return await processBlogSourceFile(blogSourceFile, contentPaths, context, options, tagsFile, authorsMap);
242
266
  }
243
267
  catch (err) {
244
268
  throw new Error(`Processing of blog source file path=${blogSourceFile} failed.`, { cause: err });
@@ -251,19 +275,6 @@ async function generateBlogPosts(contentPaths, context, options) {
251
275
  }
252
276
  return blogPosts;
253
277
  }
254
- exports.generateBlogPosts = generateBlogPosts;
255
- function linkify({ filePath, contentPaths, fileString, siteDir, sourceToPermalink, onBrokenMarkdownLink, }) {
256
- const { newContent, brokenMarkdownLinks } = (0, utils_1.replaceMarkdownLinks)({
257
- siteDir,
258
- fileString,
259
- filePath,
260
- contentPaths,
261
- sourceToPermalink,
262
- });
263
- brokenMarkdownLinks.forEach((l) => onBrokenMarkdownLink(l));
264
- return newContent;
265
- }
266
- exports.linkify = linkify;
267
278
  async function applyProcessBlogPosts({ blogPosts, processBlogPosts, }) {
268
279
  const processedBlogPosts = await processBlogPosts({ blogPosts });
269
280
  if (Array.isArray(processedBlogPosts)) {
@@ -271,4 +282,3 @@ async function applyProcessBlogPosts({ blogPosts, processBlogPosts, }) {
271
282
  }
272
283
  return blogPosts;
273
284
  }
274
- exports.applyProcessBlogPosts = applyProcessBlogPosts;
@@ -0,0 +1,33 @@
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
+ import { type ReactNode } from 'react';
8
+ import type { PropBlogPostContent, BlogMetadata } from '@docusaurus/plugin-content-blog';
9
+ export declare function useBlogMetadata(): BlogMetadata;
10
+ /**
11
+ * The React context value returned by the `useBlogPost()` hook.
12
+ * It contains useful data related to the currently browsed blog post.
13
+ */
14
+ export type BlogPostContextValue = Pick<PropBlogPostContent, 'metadata' | 'frontMatter' | 'assets' | 'toc'> & {
15
+ readonly isBlogPostPage: boolean;
16
+ };
17
+ /**
18
+ * This is a very thin layer around the `content` received from the MDX loader.
19
+ * It provides metadata about the blog post to the children tree.
20
+ */
21
+ export declare function BlogPostProvider({ children, content, isBlogPostPage, }: {
22
+ children: ReactNode;
23
+ content: PropBlogPostContent;
24
+ isBlogPostPage?: boolean;
25
+ }): JSX.Element;
26
+ /**
27
+ * Returns the data of the currently browsed blog post. Gives access to
28
+ * front matter, metadata, TOC, etc.
29
+ * When swizzling a low-level component (e.g. the "Edit this page" link)
30
+ * and you need some extra metadata, you don't have to drill the props
31
+ * all the way through the component tree: simply use this hook instead.
32
+ */
33
+ export declare function useBlogPost(): BlogPostContextValue;
@@ -0,0 +1,54 @@
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
+ import React, { useMemo, useContext } from 'react';
8
+ import { ReactContextError } from '@docusaurus/theme-common/internal';
9
+ import useRouteContext from '@docusaurus/useRouteContext';
10
+ export function useBlogMetadata() {
11
+ const routeContext = useRouteContext();
12
+ const blogMetadata = routeContext?.data?.blogMetadata;
13
+ if (!blogMetadata) {
14
+ throw new Error("useBlogMetadata() can't be called on the current route because the blog metadata could not be found in route context");
15
+ }
16
+ return blogMetadata;
17
+ }
18
+ const Context = React.createContext(null);
19
+ /**
20
+ * Note: we don't use `PropBlogPostContent` as context value on purpose.
21
+ * Metadata is currently stored inside the MDX component, but we may want to
22
+ * change that in the future.
23
+ */
24
+ function useContextValue({ content, isBlogPostPage, }) {
25
+ return useMemo(() => ({
26
+ metadata: content.metadata,
27
+ frontMatter: content.frontMatter,
28
+ assets: content.assets,
29
+ toc: content.toc,
30
+ isBlogPostPage,
31
+ }), [content, isBlogPostPage]);
32
+ }
33
+ /**
34
+ * This is a very thin layer around the `content` received from the MDX loader.
35
+ * It provides metadata about the blog post to the children tree.
36
+ */
37
+ export function BlogPostProvider({ children, content, isBlogPostPage = false, }) {
38
+ const contextValue = useContextValue({ content, isBlogPostPage });
39
+ return <Context.Provider value={contextValue}>{children}</Context.Provider>;
40
+ }
41
+ /**
42
+ * Returns the data of the currently browsed blog post. Gives access to
43
+ * front matter, metadata, TOC, etc.
44
+ * When swizzling a low-level component (e.g. the "Edit this page" link)
45
+ * and you need some extra metadata, you don't have to drill the props
46
+ * all the way through the component tree: simply use this hook instead.
47
+ */
48
+ export function useBlogPost() {
49
+ const blogPost = useContext(Context);
50
+ if (blogPost === null) {
51
+ throw new ReactContextError('BlogPostProvider');
52
+ }
53
+ return blogPost;
54
+ }
@@ -4,6 +4,6 @@
4
4
  * This source code is licensed under the MIT license found in the
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
- /// <reference path="../../src/plugin-content-blog.d.ts" />
8
- import type { BlogMetadata } from '@docusaurus/plugin-content-blog';
9
- export declare function useBlogMetadata(): BlogMetadata;
7
+ export { BlogPostProvider, type BlogPostContextValue, useBlogPost, useBlogMetadata, } from './contexts';
8
+ export { useBlogListPageStructuredData, useBlogPostStructuredData, } from './structuredDataUtils';
9
+ export { BlogSidebarItemList, groupBlogSidebarItemsByYear, useVisibleBlogSidebarItems, } from './sidebarUtils';
@@ -4,12 +4,6 @@
4
4
  * This source code is licensed under the MIT license found in the
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
- import useRouteContext from '@docusaurus/useRouteContext';
8
- export function useBlogMetadata() {
9
- const routeContext = useRouteContext();
10
- const blogMetadata = routeContext?.data?.blogMetadata;
11
- if (!blogMetadata) {
12
- throw new Error("useBlogMetadata() can't be called on the current route because the blog metadata could not be found in route context");
13
- }
14
- return blogMetadata;
15
- }
7
+ export { BlogPostProvider, useBlogPost, useBlogMetadata, } from './contexts';
8
+ export { useBlogListPageStructuredData, useBlogPostStructuredData, } from './structuredDataUtils';
9
+ export { BlogSidebarItemList, groupBlogSidebarItemsByYear, useVisibleBlogSidebarItems, } from './sidebarUtils';
@@ -0,0 +1,21 @@
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
+ import { type ReactNode } from 'react';
8
+ import type { BlogSidebarItem } from '@docusaurus/plugin-content-blog';
9
+ /**
10
+ * Return the visible blog sidebar items to display.
11
+ * Unlisted items are filtered.
12
+ */
13
+ export declare function useVisibleBlogSidebarItems(items: BlogSidebarItem[]): BlogSidebarItem[];
14
+ export declare function groupBlogSidebarItemsByYear(items: BlogSidebarItem[]): [string, BlogSidebarItem[]][];
15
+ export declare function BlogSidebarItemList({ items, ulClassName, liClassName, linkClassName, linkActiveClassName, }: {
16
+ items: BlogSidebarItem[];
17
+ ulClassName?: string;
18
+ liClassName?: string;
19
+ linkClassName?: string;
20
+ linkActiveClassName?: string;
21
+ }): ReactNode;
@@ -0,0 +1,49 @@
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
+ import React, { useMemo } from 'react';
8
+ import { useLocation } from '@docusaurus/router';
9
+ import Link from '@docusaurus/Link';
10
+ import { groupBy } from '@docusaurus/theme-common';
11
+ import { isSamePath } from '@docusaurus/theme-common/internal';
12
+ function isVisible(item, pathname) {
13
+ if (item.unlisted && !isSamePath(item.permalink, pathname)) {
14
+ return false;
15
+ }
16
+ return true;
17
+ }
18
+ /**
19
+ * Return the visible blog sidebar items to display.
20
+ * Unlisted items are filtered.
21
+ */
22
+ export function useVisibleBlogSidebarItems(items) {
23
+ const { pathname } = useLocation();
24
+ return useMemo(() => items.filter((item) => isVisible(item, pathname)), [items, pathname]);
25
+ }
26
+ export function groupBlogSidebarItemsByYear(items) {
27
+ const groupedByYear = groupBy(items, (item) => {
28
+ return `${new Date(item.date).getFullYear()}`;
29
+ });
30
+ // "as" is safe here
31
+ // see https://github.com/microsoft/TypeScript/pull/56805#issuecomment-2196526425
32
+ const entries = Object.entries(groupedByYear);
33
+ // We have to use entries because of https://x.com/sebastienlorber/status/1806371668614369486
34
+ // Objects with string/number keys are automatically sorted asc...
35
+ // Even if keys are strings like "2024"
36
+ // We want descending order for years
37
+ // Alternative: using Map.groupBy (not affected by this "reordering")
38
+ entries.reverse();
39
+ return entries;
40
+ }
41
+ export function BlogSidebarItemList({ items, ulClassName, liClassName, linkClassName, linkActiveClassName, }) {
42
+ return (<ul className={ulClassName}>
43
+ {items.map((item) => (<li key={item.permalink} className={liClassName}>
44
+ <Link isNavLink to={item.permalink} className={linkClassName} activeClassName={linkActiveClassName}>
45
+ {item.title}
46
+ </Link>
47
+ </li>))}
48
+ </ul>);
49
+ }
@@ -0,0 +1,7 @@
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
+ export {};
@@ -0,0 +1,43 @@
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
+ import { groupBlogSidebarItemsByYear } from './sidebarUtils';
8
+ describe('groupBlogSidebarItemsByYear', () => {
9
+ const post1 = {
10
+ title: 'post1',
11
+ permalink: '/post1',
12
+ date: '2024-10-03',
13
+ unlisted: false,
14
+ };
15
+ const post2 = {
16
+ title: 'post2',
17
+ permalink: '/post2',
18
+ date: '2024-05-02',
19
+ unlisted: false,
20
+ };
21
+ const post3 = {
22
+ title: 'post3',
23
+ permalink: '/post3',
24
+ date: '2022-11-18',
25
+ unlisted: false,
26
+ };
27
+ it('can group items by year', () => {
28
+ const items = [post1, post2, post3];
29
+ const entries = groupBlogSidebarItemsByYear(items);
30
+ expect(entries).toEqual([
31
+ ['2024', [post1, post2]],
32
+ ['2022', [post3]],
33
+ ]);
34
+ });
35
+ it('always returns result in descending chronological order', () => {
36
+ const items = [post3, post1, post2];
37
+ const entries = groupBlogSidebarItemsByYear(items);
38
+ expect(entries).toEqual([
39
+ ['2024', [post1, post2]],
40
+ ['2022', [post3]],
41
+ ]);
42
+ });
43
+ });
@@ -0,0 +1,10 @@
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
+ import type { Props as BlogListPageStructuredDataProps } from '@theme/BlogListPage/StructuredData';
8
+ import type { Blog, BlogPosting, WithContext } from 'schema-dts';
9
+ export declare function useBlogListPageStructuredData(props: BlogListPageStructuredDataProps): WithContext<Blog>;
10
+ export declare function useBlogPostStructuredData(): WithContext<BlogPosting>;