@docusaurus/plugin-content-blog 3.1.0 → 3.2.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 (50) hide show
  1. package/lib/blogUtils.d.ts +7 -1
  2. package/lib/blogUtils.js +15 -21
  3. package/lib/client/index.d.ts +8 -0
  4. package/lib/client/index.js +15 -0
  5. package/lib/feed.js +11 -5
  6. package/lib/frontMatter.d.ts +0 -6
  7. package/lib/frontMatter.js +3 -2
  8. package/lib/index.js +25 -5
  9. package/lib/options.js +10 -0
  10. package/package.json +26 -11
  11. package/src/blogUtils.ts +31 -27
  12. package/src/client/index.ts +20 -0
  13. package/src/feed.ts +22 -7
  14. package/src/frontMatter.ts +5 -4
  15. package/src/index.ts +42 -5
  16. package/src/options.ts +12 -0
  17. package/src/plugin-content-blog.d.ts +66 -8
  18. package/.docusaurus/DONT-EDIT-THIS-FOLDER +0 -5
  19. package/.docusaurus/client-modules.js +0 -5
  20. package/.docusaurus/codeTranslations.json +0 -1
  21. package/.docusaurus/docusaurus-plugin-content-blog/default/blog-only-aeb.json +0 -10
  22. package/.docusaurus/docusaurus-plugin-content-blog/default/blog-only-archive-245.json +0 -312
  23. package/.docusaurus/docusaurus-plugin-content-blog/default/blog-only-page-2-d48.json +0 -11
  24. package/.docusaurus/docusaurus-plugin-content-blog/default/blog-only-page-3-8b6.json +0 -10
  25. package/.docusaurus/docusaurus-plugin-content-blog/default/blog-only-tags-birthday-c96-list.json +0 -9
  26. package/.docusaurus/docusaurus-plugin-content-blog/default/blog-only-tags-birthday-c96.json +0 -7
  27. package/.docusaurus/docusaurus-plugin-content-blog/default/blog-only-tags-complex-cf3-list.json +0 -9
  28. package/.docusaurus/docusaurus-plugin-content-blog/default/blog-only-tags-complex-cf3.json +0 -7
  29. package/.docusaurus/docusaurus-plugin-content-blog/default/blog-only-tags-date-c24-list.json +0 -9
  30. package/.docusaurus/docusaurus-plugin-content-blog/default/blog-only-tags-date-c24.json +0 -7
  31. package/.docusaurus/docusaurus-plugin-content-blog/default/blog-only-tags-tags-344.json +0 -17
  32. package/.docusaurus/docusaurus-plugin-content-blog/default/blog-post-list-prop-default.json +0 -30
  33. package/.docusaurus/docusaurus-plugin-content-blog/default/plugin-route-context-module-100.json +0 -4
  34. package/.docusaurus/docusaurus-plugin-content-blog/default/site-src-tests-fixtures-website-blog-2018-12-14-happy-first-birthday-slash-md-d1e.json +0 -46
  35. package/.docusaurus/docusaurus-plugin-content-blog/default/site-src-tests-fixtures-website-blog-blog-with-links-mdx-fe5.json +0 -22
  36. package/.docusaurus/docusaurus-plugin-content-blog/default/site-src-tests-fixtures-website-blog-complex-slug-md-314.json +0 -40
  37. package/.docusaurus/docusaurus-plugin-content-blog/default/site-src-tests-fixtures-website-blog-date-matter-md-191.json +0 -33
  38. package/.docusaurus/docusaurus-plugin-content-blog/default/site-src-tests-fixtures-website-blog-heading-as-title-md-10c.json +0 -25
  39. package/.docusaurus/docusaurus-plugin-content-blog/default/site-src-tests-fixtures-website-blog-mdx-blog-post-mdx-025.json +0 -26
  40. package/.docusaurus/docusaurus-plugin-content-blog/default/site-src-tests-fixtures-website-blog-mdx-require-blog-post-mdx-4ba.json +0 -26
  41. package/.docusaurus/docusaurus-plugin-content-blog/default/site-src-tests-fixtures-website-blog-simple-slug-md-324.json +0 -36
  42. package/.docusaurus/docusaurus-plugin-content-blog/default/site-src-tests-fixtures-website-blog-unlisted-md-5cb.json +0 -18
  43. package/.docusaurus/docusaurus-theme-search-algolia/default/plugin-route-context-module-100.json +0 -4
  44. package/.docusaurus/docusaurus.config.mjs +0 -246
  45. package/.docusaurus/globalData.json +0 -1
  46. package/.docusaurus/i18n.json +0 -17
  47. package/.docusaurus/registry.js +0 -38
  48. package/.docusaurus/routes.js +0 -99
  49. package/.docusaurus/routesChunkNames.json +0 -194
  50. package/.docusaurus/site-metadata.json +0 -24
@@ -11,12 +11,13 @@ export declare function truncate(fileString: string, truncateMarker: RegExp): st
11
11
  export declare function getSourceToPermalink(blogPosts: BlogPost[]): {
12
12
  [aliasedPath: string]: string;
13
13
  };
14
- export declare function paginateBlogPosts({ blogPosts, basePageUrl, blogTitle, blogDescription, postsPerPageOption, }: {
14
+ export declare function paginateBlogPosts({ blogPosts, basePageUrl, blogTitle, blogDescription, postsPerPageOption, pageBasePath, }: {
15
15
  blogPosts: BlogPost[];
16
16
  basePageUrl: string;
17
17
  blogTitle: string;
18
18
  blogDescription: string;
19
19
  postsPerPageOption: number | 'ALL';
20
+ pageBasePath: string;
20
21
  }): BlogPaginated[];
21
22
  export declare function shouldBeListed(blogPost: BlogPost): boolean;
22
23
  export declare function getBlogTags({ blogPosts, ...params }: {
@@ -24,6 +25,7 @@ export declare function getBlogTags({ blogPosts, ...params }: {
24
25
  blogTitle: string;
25
26
  blogDescription: string;
26
27
  postsPerPageOption: number | 'ALL';
28
+ pageBasePath: string;
27
29
  }): BlogTags;
28
30
  type ParsedBlogFileName = {
29
31
  date: Date | undefined;
@@ -37,4 +39,8 @@ export type LinkifyParams = {
37
39
  fileString: string;
38
40
  } & Pick<BlogMarkdownLoaderOptions, 'sourceToPermalink' | 'siteDir' | 'contentPaths' | 'onBrokenMarkdownLink'>;
39
41
  export declare function linkify({ filePath, contentPaths, fileString, siteDir, sourceToPermalink, onBrokenMarkdownLink, }: LinkifyParams): string;
42
+ export declare function applyProcessBlogPosts({ blogPosts, processBlogPosts, }: {
43
+ blogPosts: BlogPost[];
44
+ processBlogPosts: PluginOptions['processBlogPosts'];
45
+ }): Promise<BlogPost[]>;
40
46
  export {};
package/lib/blogUtils.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.linkify = exports.generateBlogPosts = exports.parseBlogFileName = exports.getBlogTags = exports.shouldBeListed = exports.paginateBlogPosts = exports.getSourceToPermalink = exports.truncate = void 0;
9
+ exports.applyProcessBlogPosts = exports.linkify = exports.generateBlogPosts = exports.parseBlogFileName = exports.getBlogTags = exports.shouldBeListed = exports.paginateBlogPosts = exports.getSourceToPermalink = exports.truncate = void 0;
10
10
  const tslib_1 = require("tslib");
11
11
  const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
12
12
  const path_1 = tslib_1.__importDefault(require("path"));
@@ -24,14 +24,14 @@ function getSourceToPermalink(blogPosts) {
24
24
  return Object.fromEntries(blogPosts.map(({ metadata: { source, permalink } }) => [source, permalink]));
25
25
  }
26
26
  exports.getSourceToPermalink = getSourceToPermalink;
27
- function paginateBlogPosts({ blogPosts, basePageUrl, blogTitle, blogDescription, postsPerPageOption, }) {
27
+ function paginateBlogPosts({ blogPosts, basePageUrl, blogTitle, blogDescription, postsPerPageOption, pageBasePath, }) {
28
28
  const totalCount = blogPosts.length;
29
29
  const postsPerPage = postsPerPageOption === 'ALL' ? totalCount : postsPerPageOption;
30
30
  const numberOfPages = Math.ceil(totalCount / postsPerPage);
31
31
  const pages = [];
32
32
  function permalink(page) {
33
33
  return page > 0
34
- ? (0, utils_1.normalizeUrl)([basePageUrl, `page/${page + 1}`])
34
+ ? (0, utils_1.normalizeUrl)([basePageUrl, pageBasePath, `${page + 1}`])
35
35
  : basePageUrl;
36
36
  }
37
37
  for (let page = 0; page < numberOfPages; page += 1) {
@@ -96,21 +96,6 @@ function parseBlogFileName(blogSourceRelative) {
96
96
  return { date: undefined, text, slug };
97
97
  }
98
98
  exports.parseBlogFileName = parseBlogFileName;
99
- function formatBlogPostDate(locale, date, calendar) {
100
- try {
101
- return new Intl.DateTimeFormat(locale, {
102
- day: 'numeric',
103
- month: 'long',
104
- year: 'numeric',
105
- timeZone: 'UTC',
106
- calendar,
107
- }).format(date);
108
- }
109
- catch (err) {
110
- logger_1.default.error `Can't format blog post date "${String(date)}"`;
111
- throw err;
112
- }
113
- }
114
99
  async function parseBlogPostMarkdownFile({ filePath, parseFrontMatter, }) {
115
100
  const fileContent = await fs_extra_1.default.readFile(filePath, 'utf-8');
116
101
  try {
@@ -142,6 +127,7 @@ async function processBlogSourceFile(blogSourceRelative, contentPaths, context,
142
127
  parseFrontMatter,
143
128
  });
144
129
  const aliasedSource = (0, utils_1.aliasedSitePath)(blogSourceAbsolute, siteDir);
130
+ const lastUpdate = await (0, utils_1.readLastUpdateData)(blogSourceAbsolute, options, frontMatter.last_update);
145
131
  const draft = (0, utils_1.isDraft)({ frontMatter });
146
132
  const unlisted = (0, utils_1.isUnlisted)({ frontMatter });
147
133
  if (draft) {
@@ -165,7 +151,7 @@ async function processBlogSourceFile(blogSourceRelative, contentPaths, context,
165
151
  return parsedBlogFileName.date;
166
152
  }
167
153
  try {
168
- const result = (0, utils_1.getFileCommitDate)(blogSourceAbsolute, {
154
+ const result = await (0, utils_1.getFileCommitDate)(blogSourceAbsolute, {
169
155
  age: 'oldest',
170
156
  includeAuthor: false,
171
157
  });
@@ -177,7 +163,6 @@ async function processBlogSourceFile(blogSourceRelative, contentPaths, context,
177
163
  }
178
164
  }
179
165
  const date = await getDate();
180
- const formattedDate = formatBlogPostDate(i18n.currentLocale, date, i18n.localeConfigs[i18n.currentLocale].calendar);
181
166
  const title = frontMatter.title ?? contentTitle ?? parsedBlogFileName.text;
182
167
  const description = frontMatter.description ?? excerpt ?? '';
183
168
  const slug = frontMatter.slug ?? parsedBlogFileName.slug;
@@ -220,7 +205,6 @@ async function processBlogSourceFile(blogSourceRelative, contentPaths, context,
220
205
  title,
221
206
  description,
222
207
  date,
223
- formattedDate,
224
208
  tags: (0, utils_1.normalizeFrontMatterTags)(tagsBasePath, frontMatter.tags),
225
209
  readingTime: showReadingTime
226
210
  ? options.readingTime({
@@ -233,6 +217,8 @@ async function processBlogSourceFile(blogSourceRelative, contentPaths, context,
233
217
  authors,
234
218
  frontMatter,
235
219
  unlisted,
220
+ lastUpdatedAt: lastUpdate.lastUpdatedAt,
221
+ lastUpdatedBy: lastUpdate.lastUpdatedBy,
236
222
  },
237
223
  content,
238
224
  };
@@ -278,3 +264,11 @@ function linkify({ filePath, contentPaths, fileString, siteDir, sourceToPermalin
278
264
  return newContent;
279
265
  }
280
266
  exports.linkify = linkify;
267
+ async function applyProcessBlogPosts({ blogPosts, processBlogPosts, }) {
268
+ const processedBlogPosts = await processBlogPosts({ blogPosts });
269
+ if (Array.isArray(processedBlogPosts)) {
270
+ return processedBlogPosts;
271
+ }
272
+ return blogPosts;
273
+ }
274
+ exports.applyProcessBlogPosts = applyProcessBlogPosts;
@@ -0,0 +1,8 @@
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 { BlogMetadata } from '@docusaurus/plugin-content-blog';
8
+ export declare function useBlogMetadata(): BlogMetadata;
@@ -0,0 +1,15 @@
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 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
+ }
package/lib/feed.js CHANGED
@@ -21,8 +21,11 @@ async function generateBlogFeed({ blogPosts, options, siteConfig, outDir, locale
21
21
  return null;
22
22
  }
23
23
  const { feedOptions, routeBasePath } = options;
24
- const { url: siteUrl, baseUrl, title, favicon } = siteConfig;
25
- const blogBaseUrl = (0, utils_1.normalizeUrl)([siteUrl, baseUrl, routeBasePath]);
24
+ const { url: siteUrl, baseUrl, title, favicon, trailingSlash } = siteConfig;
25
+ const blogBaseUrl = (0, utils_common_1.applyTrailingSlash)((0, utils_1.normalizeUrl)([siteUrl, baseUrl, routeBasePath]), {
26
+ trailingSlash,
27
+ baseUrl,
28
+ });
26
29
  const blogPostsForFeed = feedOptions.limit === false || feedOptions.limit === null
27
30
  ? blogPosts
28
31
  : blogPosts.slice(0, feedOptions.limit);
@@ -48,15 +51,18 @@ async function generateBlogFeed({ blogPosts, options, siteConfig, outDir, locale
48
51
  return feed;
49
52
  }
50
53
  async function defaultCreateFeedItems({ blogPosts, siteConfig, outDir, }) {
51
- const { url: siteUrl } = siteConfig;
54
+ const { url: siteUrl, baseUrl, trailingSlash } = siteConfig;
52
55
  function toFeedAuthor(author) {
53
56
  return { name: author.name, link: author.url, email: author.email };
54
57
  }
55
58
  return Promise.all(blogPosts.map(async (post) => {
56
59
  const { metadata: { title: metadataTitle, permalink, date, description, authors, tags, }, } = post;
57
- const content = await (0, utils_1.readOutputHTMLFile)(permalink.replace(siteConfig.baseUrl, ''), outDir, siteConfig.trailingSlash);
60
+ const content = await (0, utils_1.readOutputHTMLFile)(permalink.replace(baseUrl, ''), outDir, trailingSlash);
58
61
  const $ = (0, cheerio_1.load)(content);
59
- const blogPostAbsoluteUrl = (0, utils_1.normalizeUrl)([siteUrl, permalink]);
62
+ const blogPostAbsoluteUrl = (0, utils_common_1.applyTrailingSlash)((0, utils_1.normalizeUrl)([siteUrl, permalink]), {
63
+ trailingSlash,
64
+ baseUrl,
65
+ });
60
66
  const toAbsoluteUrl = (src) => String(new URL(src, blogPostAbsoluteUrl));
61
67
  // Make links and image urls absolute
62
68
  // See https://github.com/facebook/docusaurus/issues/9136
@@ -1,9 +1,3 @@
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
1
  import type { BlogPostFrontMatter } from '@docusaurus/plugin-content-blog';
8
2
  export declare function validateBlogPostFrontMatter(frontMatter: {
9
3
  [key: string]: unknown;
@@ -1,12 +1,12 @@
1
1
  "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateBlogPostFrontMatter = void 0;
2
4
  /**
3
5
  * Copyright (c) Facebook, Inc. and its affiliates.
4
6
  *
5
7
  * This source code is licensed under the MIT license found in the
6
8
  * LICENSE file in the root directory of this source tree.
7
9
  */
8
- Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.validateBlogPostFrontMatter = void 0;
10
10
  const utils_validation_1 = require("@docusaurus/utils-validation");
11
11
  const BlogPostFrontMatterAuthorSchema = utils_validation_1.JoiFrontMatter.object({
12
12
  key: utils_validation_1.JoiFrontMatter.string(),
@@ -52,6 +52,7 @@ const BlogFrontMatterSchema = utils_validation_1.JoiFrontMatter.object({
52
52
  keywords: utils_validation_1.JoiFrontMatter.array().items(utils_validation_1.JoiFrontMatter.string().required()),
53
53
  hide_table_of_contents: utils_validation_1.JoiFrontMatter.boolean(),
54
54
  ...utils_validation_1.FrontMatterTOCHeadingLevels,
55
+ last_update: utils_validation_1.FrontMatterLastUpdateSchema,
55
56
  })
56
57
  .messages({
57
58
  'deprecate.error': '{#label} blog frontMatter field is deprecated. Please use {#alternative} instead.',
package/lib/index.js CHANGED
@@ -47,10 +47,14 @@ async function pluginContentBlog(context, options) {
47
47
  },
48
48
  // Fetches blog contents and returns metadata for the necessary routes.
49
49
  async loadContent() {
50
- const { postsPerPage: postsPerPageOption, routeBasePath, tagsBasePath, blogDescription, blogTitle, blogSidebarTitle, } = options;
50
+ const { postsPerPage: postsPerPageOption, routeBasePath, tagsBasePath, blogDescription, blogTitle, blogSidebarTitle, pageBasePath, } = options;
51
51
  const baseBlogUrl = (0, utils_1.normalizeUrl)([baseUrl, routeBasePath]);
52
52
  const blogTagsListPath = (0, utils_1.normalizeUrl)([baseBlogUrl, tagsBasePath]);
53
- const blogPosts = await (0, blogUtils_1.generateBlogPosts)(contentPaths, context, options);
53
+ let blogPosts = await (0, blogUtils_1.generateBlogPosts)(contentPaths, context, options);
54
+ blogPosts = await (0, blogUtils_1.applyProcessBlogPosts)({
55
+ blogPosts,
56
+ processBlogPosts: options.processBlogPosts,
57
+ });
54
58
  const listedBlogPosts = blogPosts.filter(blogUtils_1.shouldBeListed);
55
59
  if (!blogPosts.length) {
56
60
  return {
@@ -59,10 +63,9 @@ async function pluginContentBlog(context, options) {
59
63
  blogListPaginated: [],
60
64
  blogTags: {},
61
65
  blogTagsListPath,
62
- blogTagsPaginated: [],
63
66
  };
64
67
  }
65
- // Colocate next and prev metadata.
68
+ // Collocate next and prev metadata.
66
69
  listedBlogPosts.forEach((blogPost, index) => {
67
70
  const prevItem = index > 0 ? listedBlogPosts[index - 1] : null;
68
71
  if (prevItem) {
@@ -87,12 +90,14 @@ async function pluginContentBlog(context, options) {
87
90
  blogDescription,
88
91
  postsPerPageOption,
89
92
  basePageUrl: baseBlogUrl,
93
+ pageBasePath,
90
94
  });
91
95
  const blogTags = (0, blogUtils_1.getBlogTags)({
92
96
  blogPosts,
93
97
  postsPerPageOption,
94
98
  blogDescription,
95
99
  blogTitle,
100
+ pageBasePath,
96
101
  });
97
102
  return {
98
103
  blogSidebarTitle,
@@ -103,7 +108,7 @@ async function pluginContentBlog(context, options) {
103
108
  };
104
109
  },
105
110
  async contentLoaded({ content: blogContents, actions }) {
106
- const { blogListComponent, blogPostComponent, blogTagsListComponent, blogTagsPostsComponent, blogArchiveComponent, routeBasePath, archiveBasePath, } = options;
111
+ const { blogListComponent, blogPostComponent, blogTagsListComponent, blogTagsPostsComponent, blogArchiveComponent, routeBasePath, archiveBasePath, blogTitle, } = options;
107
112
  const { addRoute, createData } = actions;
108
113
  const { blogSidebarTitle, blogPosts, blogListPaginated, blogTags, blogTagsListPath, } = blogContents;
109
114
  const listedBlogPosts = blogPosts.filter(blogUtils_1.shouldBeListed);
@@ -154,6 +159,17 @@ async function pluginContentBlog(context, options) {
154
159
  unlisted: blogPost.metadata.unlisted,
155
160
  })),
156
161
  }, null, 2));
162
+ const blogMetadata = {
163
+ blogBasePath: (0, utils_1.normalizeUrl)([baseUrl, routeBasePath]),
164
+ blogTitle,
165
+ };
166
+ const blogMetadataPath = await createData(`blogMetadata-${pluginId}.json`, JSON.stringify(blogMetadata, null, 2));
167
+ function createBlogPostRouteMetadata(blogPostMeta) {
168
+ return {
169
+ sourceFilePath: (0, utils_1.aliasedSitePathToRelativePath)(blogPostMeta.source),
170
+ lastUpdatedAt: blogPostMeta.lastUpdatedAt,
171
+ };
172
+ }
157
173
  // Create routes for blog entries.
158
174
  await Promise.all(blogPosts.map(async (blogPost) => {
159
175
  const { id, metadata } = blogPost;
@@ -169,6 +185,10 @@ async function pluginContentBlog(context, options) {
169
185
  sidebar: aliasedSource(sidebarProp),
170
186
  content: metadata.source,
171
187
  },
188
+ metadata: createBlogPostRouteMetadata(metadata),
189
+ context: {
190
+ blogMetadata: aliasedSource(blogMetadataPath),
191
+ },
172
192
  });
173
193
  blogItemsToMetadata[id] = metadata;
174
194
  }));
package/lib/options.js CHANGED
@@ -33,11 +33,15 @@ exports.DEFAULT_OPTIONS = {
33
33
  routeBasePath: 'blog',
34
34
  tagsBasePath: 'tags',
35
35
  archiveBasePath: 'archive',
36
+ pageBasePath: 'page',
36
37
  path: 'blog',
37
38
  editLocalizedFiles: false,
38
39
  authorsMapPath: 'authors.yml',
39
40
  readingTime: ({ content, defaultReadingTime }) => defaultReadingTime({ content }),
40
41
  sortPosts: 'descending',
42
+ showLastUpdateTime: false,
43
+ showLastUpdateAuthor: false,
44
+ processBlogPosts: async () => undefined,
41
45
  };
42
46
  const PluginOptionSchema = utils_validation_1.Joi.object({
43
47
  path: utils_validation_1.Joi.string().default(exports.DEFAULT_OPTIONS.path),
@@ -46,6 +50,7 @@ const PluginOptionSchema = utils_validation_1.Joi.object({
46
50
  .allow(null),
47
51
  routeBasePath: utils_validation_1.RouteBasePathSchema.default(exports.DEFAULT_OPTIONS.routeBasePath),
48
52
  tagsBasePath: utils_validation_1.Joi.string().default(exports.DEFAULT_OPTIONS.tagsBasePath),
53
+ pageBasePath: utils_validation_1.Joi.string().default(exports.DEFAULT_OPTIONS.pageBasePath),
49
54
  include: utils_validation_1.Joi.array().items(utils_validation_1.Joi.string()).default(exports.DEFAULT_OPTIONS.include),
50
55
  exclude: utils_validation_1.Joi.array().items(utils_validation_1.Joi.string()).default(exports.DEFAULT_OPTIONS.exclude),
51
56
  postsPerPage: utils_validation_1.Joi.alternatives()
@@ -101,6 +106,11 @@ const PluginOptionSchema = utils_validation_1.Joi.object({
101
106
  sortPosts: utils_validation_1.Joi.string()
102
107
  .valid('descending', 'ascending')
103
108
  .default(exports.DEFAULT_OPTIONS.sortPosts),
109
+ showLastUpdateTime: utils_validation_1.Joi.bool().default(exports.DEFAULT_OPTIONS.showLastUpdateTime),
110
+ showLastUpdateAuthor: utils_validation_1.Joi.bool().default(exports.DEFAULT_OPTIONS.showLastUpdateAuthor),
111
+ processBlogPosts: utils_validation_1.Joi.function()
112
+ .optional()
113
+ .default(() => exports.DEFAULT_OPTIONS.processBlogPosts),
104
114
  }).default(exports.DEFAULT_OPTIONS);
105
115
  function validateOptions({ validate, options, }) {
106
116
  const validatedOptions = validate(PluginOptionSchema, options);
package/package.json CHANGED
@@ -1,12 +1,24 @@
1
1
  {
2
2
  "name": "@docusaurus/plugin-content-blog",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Blog plugin for Docusaurus.",
5
5
  "main": "lib/index.js",
6
6
  "types": "src/plugin-content-blog.d.ts",
7
+ "exports": {
8
+ "./lib/*": "./lib/*",
9
+ "./src/*": "./src/*",
10
+ "./client": {
11
+ "type": "./lib/client/index.d.ts",
12
+ "default": "./lib/client/index.js"
13
+ },
14
+ ".": {
15
+ "types": "./src/plugin-content-blog.d.ts",
16
+ "default": "./lib/index.js"
17
+ }
18
+ },
7
19
  "scripts": {
8
- "build": "tsc",
9
- "watch": "tsc --watch",
20
+ "build": "tsc --build",
21
+ "watch": "tsc --build --watch",
10
22
  "test:generate-build-snap": "yarn docusaurus build src/__tests__/__fixtures__/website --out-dir build-snap && yarn rimraf src/__tests__/__fixtures__/website/.docusaurus && yarn rimraf src/__tests__/__fixtures__/website/build-snap/assets && git add src/__tests__/__fixtures__/website/build-snap"
11
23
  },
12
24
  "repository": {
@@ -19,13 +31,13 @@
19
31
  },
20
32
  "license": "MIT",
21
33
  "dependencies": {
22
- "@docusaurus/core": "3.1.0",
23
- "@docusaurus/logger": "3.1.0",
24
- "@docusaurus/mdx-loader": "3.1.0",
25
- "@docusaurus/types": "3.1.0",
26
- "@docusaurus/utils": "3.1.0",
27
- "@docusaurus/utils-common": "3.1.0",
28
- "@docusaurus/utils-validation": "3.1.0",
34
+ "@docusaurus/core": "3.2.0",
35
+ "@docusaurus/logger": "3.2.0",
36
+ "@docusaurus/mdx-loader": "3.2.0",
37
+ "@docusaurus/types": "3.2.0",
38
+ "@docusaurus/utils": "3.2.0",
39
+ "@docusaurus/utils-common": "3.2.0",
40
+ "@docusaurus/utils-validation": "3.2.0",
29
41
  "cheerio": "^1.0.0-rc.12",
30
42
  "feed": "^4.2.2",
31
43
  "fs-extra": "^11.1.1",
@@ -44,5 +56,8 @@
44
56
  "engines": {
45
57
  "node": ">=18.0"
46
58
  },
47
- "gitHead": "a5e675821f0e8b70b591fcebf19fd60a70d55548"
59
+ "devDependencies": {
60
+ "@total-typescript/shoehorn": "^0.1.2"
61
+ },
62
+ "gitHead": "5af143651b26b39761361acd96e9c5be7ba0cb25"
48
63
  }
package/src/blogUtils.ts CHANGED
@@ -26,6 +26,7 @@ import {
26
26
  getContentPathList,
27
27
  isUnlisted,
28
28
  isDraft,
29
+ readLastUpdateData,
29
30
  } from '@docusaurus/utils';
30
31
  import {validateBlogPostFrontMatter} from './frontMatter';
31
32
  import {type AuthorsMap, getAuthorsMap, getBlogPostAuthors} from './authors';
@@ -57,12 +58,14 @@ export function paginateBlogPosts({
57
58
  blogTitle,
58
59
  blogDescription,
59
60
  postsPerPageOption,
61
+ pageBasePath,
60
62
  }: {
61
63
  blogPosts: BlogPost[];
62
64
  basePageUrl: string;
63
65
  blogTitle: string;
64
66
  blogDescription: string;
65
67
  postsPerPageOption: number | 'ALL';
68
+ pageBasePath: string;
66
69
  }): BlogPaginated[] {
67
70
  const totalCount = blogPosts.length;
68
71
  const postsPerPage =
@@ -73,7 +76,7 @@ export function paginateBlogPosts({
73
76
 
74
77
  function permalink(page: number) {
75
78
  return page > 0
76
- ? normalizeUrl([basePageUrl, `page/${page + 1}`])
79
+ ? normalizeUrl([basePageUrl, pageBasePath, `${page + 1}`])
77
80
  : basePageUrl;
78
81
  }
79
82
 
@@ -111,6 +114,7 @@ export function getBlogTags({
111
114
  blogTitle: string;
112
115
  blogDescription: string;
113
116
  postsPerPageOption: number | 'ALL';
117
+ pageBasePath: string;
114
118
  }): BlogTags {
115
119
  const groups = groupTaggedItems(
116
120
  blogPosts,
@@ -161,25 +165,6 @@ export function parseBlogFileName(
161
165
  return {date: undefined, text, slug};
162
166
  }
163
167
 
164
- function formatBlogPostDate(
165
- locale: string,
166
- date: Date,
167
- calendar: string,
168
- ): string {
169
- try {
170
- return new Intl.DateTimeFormat(locale, {
171
- day: 'numeric',
172
- month: 'long',
173
- year: 'numeric',
174
- timeZone: 'UTC',
175
- calendar,
176
- }).format(date);
177
- } catch (err) {
178
- logger.error`Can't format blog post date "${String(date)}"`;
179
- throw err;
180
- }
181
- }
182
-
183
168
  async function parseBlogPostMarkdownFile({
184
169
  filePath,
185
170
  parseFrontMatter,
@@ -247,6 +232,12 @@ async function processBlogSourceFile(
247
232
 
248
233
  const aliasedSource = aliasedSitePath(blogSourceAbsolute, siteDir);
249
234
 
235
+ const lastUpdate = await readLastUpdateData(
236
+ blogSourceAbsolute,
237
+ options,
238
+ frontMatter.last_update,
239
+ );
240
+
250
241
  const draft = isDraft({frontMatter});
251
242
  const unlisted = isUnlisted({frontMatter});
252
243
 
@@ -274,10 +265,11 @@ async function processBlogSourceFile(
274
265
  }
275
266
 
276
267
  try {
277
- const result = getFileCommitDate(blogSourceAbsolute, {
268
+ const result = await getFileCommitDate(blogSourceAbsolute, {
278
269
  age: 'oldest',
279
270
  includeAuthor: false,
280
271
  });
272
+
281
273
  return result.date;
282
274
  } catch (err) {
283
275
  logger.warn(err);
@@ -286,11 +278,6 @@ async function processBlogSourceFile(
286
278
  }
287
279
 
288
280
  const date = await getDate();
289
- const formattedDate = formatBlogPostDate(
290
- i18n.currentLocale,
291
- date,
292
- i18n.localeConfigs[i18n.currentLocale]!.calendar,
293
- );
294
281
 
295
282
  const title = frontMatter.title ?? contentTitle ?? parsedBlogFileName.text;
296
283
  const description = frontMatter.description ?? excerpt ?? '';
@@ -345,7 +332,6 @@ async function processBlogSourceFile(
345
332
  title,
346
333
  description,
347
334
  date,
348
- formattedDate,
349
335
  tags: normalizeFrontMatterTags(tagsBasePath, frontMatter.tags),
350
336
  readingTime: showReadingTime
351
337
  ? options.readingTime({
@@ -358,6 +344,8 @@ async function processBlogSourceFile(
358
344
  authors,
359
345
  frontMatter,
360
346
  unlisted,
347
+ lastUpdatedAt: lastUpdate.lastUpdatedAt,
348
+ lastUpdatedBy: lastUpdate.lastUpdatedBy,
361
349
  },
362
350
  content,
363
351
  };
@@ -443,3 +431,19 @@ export function linkify({
443
431
 
444
432
  return newContent;
445
433
  }
434
+
435
+ export async function applyProcessBlogPosts({
436
+ blogPosts,
437
+ processBlogPosts,
438
+ }: {
439
+ blogPosts: BlogPost[];
440
+ processBlogPosts: PluginOptions['processBlogPosts'];
441
+ }): Promise<BlogPost[]> {
442
+ const processedBlogPosts = await processBlogPosts({blogPosts});
443
+
444
+ if (Array.isArray(processedBlogPosts)) {
445
+ return processedBlogPosts;
446
+ }
447
+
448
+ return blogPosts;
449
+ }
@@ -0,0 +1,20 @@
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 useRouteContext from '@docusaurus/useRouteContext';
9
+ import type {BlogMetadata} from '@docusaurus/plugin-content-blog';
10
+
11
+ export function useBlogMetadata(): BlogMetadata {
12
+ const routeContext = useRouteContext();
13
+ const blogMetadata = routeContext?.data?.blogMetadata;
14
+ if (!blogMetadata) {
15
+ throw new Error(
16
+ "useBlogMetadata() can't be called on the current route because the blog metadata could not be found in route context",
17
+ );
18
+ }
19
+ return blogMetadata as BlogMetadata;
20
+ }
package/src/feed.ts CHANGED
@@ -11,7 +11,10 @@ import logger from '@docusaurus/logger';
11
11
  import {Feed, type Author as FeedAuthor} from 'feed';
12
12
  import * as srcset from 'srcset';
13
13
  import {normalizeUrl, readOutputHTMLFile} from '@docusaurus/utils';
14
- import {blogPostContainerID} from '@docusaurus/utils-common';
14
+ import {
15
+ blogPostContainerID,
16
+ applyTrailingSlash,
17
+ } from '@docusaurus/utils-common';
15
18
  import {load as cheerioLoad} from 'cheerio';
16
19
  import type {DocusaurusConfig} from '@docusaurus/types';
17
20
  import type {
@@ -40,8 +43,14 @@ async function generateBlogFeed({
40
43
  }
41
44
 
42
45
  const {feedOptions, routeBasePath} = options;
43
- const {url: siteUrl, baseUrl, title, favicon} = siteConfig;
44
- const blogBaseUrl = normalizeUrl([siteUrl, baseUrl, routeBasePath]);
46
+ const {url: siteUrl, baseUrl, title, favicon, trailingSlash} = siteConfig;
47
+ const blogBaseUrl = applyTrailingSlash(
48
+ normalizeUrl([siteUrl, baseUrl, routeBasePath]),
49
+ {
50
+ trailingSlash,
51
+ baseUrl,
52
+ },
53
+ );
45
54
 
46
55
  const blogPostsForFeed =
47
56
  feedOptions.limit === false || feedOptions.limit === null
@@ -85,7 +94,7 @@ async function defaultCreateFeedItems({
85
94
  siteConfig: DocusaurusConfig;
86
95
  outDir: string;
87
96
  }): Promise<BlogFeedItem[]> {
88
- const {url: siteUrl} = siteConfig;
97
+ const {url: siteUrl, baseUrl, trailingSlash} = siteConfig;
89
98
 
90
99
  function toFeedAuthor(author: Author): FeedAuthor {
91
100
  return {name: author.name, link: author.url, email: author.email};
@@ -105,13 +114,19 @@ async function defaultCreateFeedItems({
105
114
  } = post;
106
115
 
107
116
  const content = await readOutputHTMLFile(
108
- permalink.replace(siteConfig.baseUrl, ''),
117
+ permalink.replace(baseUrl, ''),
109
118
  outDir,
110
- siteConfig.trailingSlash,
119
+ trailingSlash,
111
120
  );
112
121
  const $ = cheerioLoad(content);
113
122
 
114
- const blogPostAbsoluteUrl = normalizeUrl([siteUrl, permalink]);
123
+ const blogPostAbsoluteUrl = applyTrailingSlash(
124
+ normalizeUrl([siteUrl, permalink]),
125
+ {
126
+ trailingSlash,
127
+ baseUrl,
128
+ },
129
+ );
115
130
 
116
131
  const toAbsoluteUrl = (src: string) =>
117
132
  String(new URL(src, blogPostAbsoluteUrl));
@@ -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
-
8
7
  import {
8
+ ContentVisibilitySchema,
9
+ FrontMatterLastUpdateSchema,
10
+ FrontMatterTOCHeadingLevels,
11
+ FrontMatterTagsSchema,
9
12
  JoiFrontMatter as Joi, // Custom instance for front matter
10
13
  URISchema,
11
14
  validateFrontMatter,
12
- FrontMatterTagsSchema,
13
- FrontMatterTOCHeadingLevels,
14
- ContentVisibilitySchema,
15
15
  } from '@docusaurus/utils-validation';
16
16
  import type {BlogPostFrontMatter} from '@docusaurus/plugin-content-blog';
17
17
 
@@ -69,6 +69,7 @@ const BlogFrontMatterSchema = Joi.object<BlogPostFrontMatter>({
69
69
  hide_table_of_contents: Joi.boolean(),
70
70
 
71
71
  ...FrontMatterTOCHeadingLevels,
72
+ last_update: FrontMatterLastUpdateSchema,
72
73
  })
73
74
  .messages({
74
75
  'deprecate.error':