@docusaurus/plugin-content-blog 2.0.0-beta.15 → 2.0.0-beta.16

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/lib/authors.js CHANGED
@@ -52,9 +52,9 @@ function getFrontMatterAuthorLegacy(frontMatter) {
52
52
  function normalizeFrontMatterAuthors(frontMatterAuthors = []) {
53
53
  function normalizeAuthor(authorInput) {
54
54
  if (typeof authorInput === 'string') {
55
- // Technically, we could allow users to provide an author's name here
56
- // IMHO it's better to only support keys here
57
- // Reason: a typo in a key would fallback to becoming a name and may end-up un-noticed
55
+ // Technically, we could allow users to provide an author's name here, but
56
+ // we only support keys, otherwise, a typo in a key would fallback to
57
+ // becoming a name and may end up unnoticed
58
58
  return { key: authorInput };
59
59
  }
60
60
  return authorInput;
@@ -97,7 +97,8 @@ function getBlogPostAuthors(params) {
97
97
  const authorLegacy = getFrontMatterAuthorLegacy(params.frontMatter);
98
98
  const authors = getFrontMatterAuthors(params);
99
99
  if (authorLegacy) {
100
- // Technically, we could allow mixing legacy/authors front matter, but do we really want to?
100
+ // Technically, we could allow mixing legacy/authors front matter, but do we
101
+ // really want to?
101
102
  if (authors.length > 0) {
102
103
  throw new Error(`To declare blog post authors, use the 'authors' front matter in priority.
103
104
  Don't mix 'authors' with other existing 'author_*' front matter. Choose one or the other, not both at the same time.`);
@@ -4,12 +4,24 @@
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 type { BlogPost, BlogContentPaths, BlogMarkdownLoaderOptions, BlogTags } from './types';
7
+ import type { BlogPost, BlogContentPaths, BlogMarkdownLoaderOptions, BlogTags, BlogPaginated } from './types';
8
8
  import type { LoadContext } from '@docusaurus/types';
9
9
  import type { PluginOptions } from '@docusaurus/plugin-content-blog';
10
10
  export declare function truncate(fileString: string, truncateMarker: RegExp): string;
11
11
  export declare function getSourceToPermalink(blogPosts: BlogPost[]): Record<string, string>;
12
- export declare function getBlogTags(blogPosts: BlogPost[]): BlogTags;
12
+ export declare function paginateBlogPosts({ blogPosts, basePageUrl, blogTitle, blogDescription, postsPerPageOption, }: {
13
+ blogPosts: BlogPost[];
14
+ basePageUrl: string;
15
+ blogTitle: string;
16
+ blogDescription: string;
17
+ postsPerPageOption: number | 'ALL';
18
+ }): BlogPaginated[];
19
+ export declare function getBlogTags({ blogPosts, ...params }: {
20
+ blogPosts: BlogPost[];
21
+ blogTitle: string;
22
+ blogDescription: string;
23
+ postsPerPageOption: number | 'ALL';
24
+ }): BlogTags;
13
25
  declare type ParsedBlogFileName = {
14
26
  date: Date | undefined;
15
27
  text: string;
package/lib/blogUtils.js CHANGED
@@ -6,12 +6,12 @@
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.getSourceToPermalink = exports.truncate = void 0;
9
+ exports.linkify = exports.generateBlogPosts = exports.parseBlogFileName = exports.getBlogTags = exports.paginateBlogPosts = exports.getSourceToPermalink = exports.truncate = void 0;
10
10
  const tslib_1 = require("tslib");
11
11
  const fs_extra_1 = (0, tslib_1.__importDefault)(require("fs-extra"));
12
12
  const path_1 = (0, tslib_1.__importDefault)(require("path"));
13
13
  const reading_time_1 = (0, tslib_1.__importDefault)(require("reading-time"));
14
- const lodash_1 = require("lodash");
14
+ const lodash_1 = (0, tslib_1.__importDefault)(require("lodash"));
15
15
  const utils_1 = require("@docusaurus/utils");
16
16
  const blogFrontMatter_1 = require("./blogFrontMatter");
17
17
  const authors_1 = require("./authors");
@@ -21,19 +21,53 @@ function truncate(fileString, truncateMarker) {
21
21
  }
22
22
  exports.truncate = truncate;
23
23
  function getSourceToPermalink(blogPosts) {
24
- return (0, lodash_1.mapValues)((0, lodash_1.keyBy)(blogPosts, (item) => item.metadata.source), (v) => v.metadata.permalink);
24
+ return Object.fromEntries(blogPosts.map(({ metadata: { source, permalink } }) => [source, permalink]));
25
25
  }
26
26
  exports.getSourceToPermalink = getSourceToPermalink;
27
- function getBlogTags(blogPosts) {
27
+ function paginateBlogPosts({ blogPosts, basePageUrl, blogTitle, blogDescription, postsPerPageOption, }) {
28
+ const totalCount = blogPosts.length;
29
+ const postsPerPage = postsPerPageOption === 'ALL' ? totalCount : postsPerPageOption;
30
+ const numberOfPages = Math.ceil(totalCount / postsPerPage);
31
+ const pages = [];
32
+ function permalink(page) {
33
+ return page > 0 ? `${basePageUrl}/page/${page + 1}` : basePageUrl;
34
+ }
35
+ for (let page = 0; page < numberOfPages; page += 1) {
36
+ pages.push({
37
+ items: blogPosts
38
+ .slice(page * postsPerPage, (page + 1) * postsPerPage)
39
+ .map((item) => item.id),
40
+ metadata: {
41
+ permalink: permalink(page),
42
+ page: page + 1,
43
+ postsPerPage,
44
+ totalPages: numberOfPages,
45
+ totalCount,
46
+ previousPage: page !== 0 ? permalink(page - 1) : null,
47
+ nextPage: page < numberOfPages - 1 ? permalink(page + 1) : null,
48
+ blogDescription,
49
+ blogTitle,
50
+ },
51
+ });
52
+ }
53
+ return pages;
54
+ }
55
+ exports.paginateBlogPosts = paginateBlogPosts;
56
+ function getBlogTags({ blogPosts, ...params }) {
28
57
  const groups = (0, utils_1.groupTaggedItems)(blogPosts, (blogPost) => blogPost.metadata.tags);
29
- return (0, lodash_1.mapValues)(groups, (group) => ({
30
- name: group.tag.label,
31
- items: group.items.map((item) => item.id),
32
- permalink: group.tag.permalink,
58
+ return lodash_1.default.mapValues(groups, ({ tag, items: tagBlogPosts }) => ({
59
+ name: tag.label,
60
+ items: tagBlogPosts.map((item) => item.id),
61
+ permalink: tag.permalink,
62
+ pages: paginateBlogPosts({
63
+ blogPosts: tagBlogPosts,
64
+ basePageUrl: tag.permalink,
65
+ ...params,
66
+ }),
33
67
  }));
34
68
  }
35
69
  exports.getBlogTags = getBlogTags;
36
- const DATE_FILENAME_REGEX = /^(?<folder>.*)(?<date>\d{4}[-/]\d{1,2}[-/]\d{1,2})[-/]?(?<text>.*?)(\/index)?.mdx?$/;
70
+ const DATE_FILENAME_REGEX = /^(?<folder>.*)(?<date>\d{4}[-/]\d{1,2}[-/]\d{1,2})[-/]?(?<text>.*?)(?:\/index)?.mdx?$/;
37
71
  function parseBlogFileName(blogSourceRelative) {
38
72
  const dateFilenameMatch = blogSourceRelative.match(DATE_FILENAME_REGEX);
39
73
  if (dateFilenameMatch) {
@@ -44,11 +78,9 @@ function parseBlogFileName(blogSourceRelative) {
44
78
  const slug = `/${slugDate}/${folder}${text}`;
45
79
  return { date, text, slug };
46
80
  }
47
- else {
48
- const text = blogSourceRelative.replace(/(\/index)?\.mdx?$/, '');
49
- const slug = `/${text}`;
50
- return { date: undefined, text, slug };
51
- }
81
+ const text = blogSourceRelative.replace(/(?:\/index)?\.mdx?$/, '');
82
+ const slug = `/${text}`;
83
+ return { date: undefined, text, slug };
52
84
  }
53
85
  exports.parseBlogFileName = parseBlogFileName;
54
86
  function formatBlogPostDate(locale, date) {
@@ -60,8 +92,9 @@ function formatBlogPostDate(locale, date) {
60
92
  timeZone: 'UTC',
61
93
  }).format(date);
62
94
  }
63
- catch (e) {
64
- throw new Error(`Can't format blog post date "${date}"`);
95
+ catch (err) {
96
+ logger_1.default.error `Can't format blog post date "${String(date)}"`;
97
+ throw err;
65
98
  }
66
99
  }
67
100
  async function parseBlogPostMarkdownFile(blogSourceAbsolute) {
@@ -75,8 +108,9 @@ async function parseBlogPostMarkdownFile(blogSourceAbsolute) {
75
108
  frontMatter: (0, blogFrontMatter_1.validateBlogPostFrontMatter)(result.frontMatter),
76
109
  };
77
110
  }
78
- catch (e) {
79
- throw new Error(`Error while parsing blog post file ${blogSourceAbsolute}: "${e.message}".`);
111
+ catch (err) {
112
+ logger_1.default.error `Error while parsing blog post file path=${blogSourceAbsolute}.`;
113
+ throw err;
80
114
  }
81
115
  }
82
116
  const defaultReadingTime = ({ content, options }) => (0, reading_time_1.default)(content, options).minutes;
@@ -109,8 +143,17 @@ async function processBlogSourceFile(blogSourceRelative, contentPaths, context,
109
143
  else if (parsedBlogFileName.date) {
110
144
  return parsedBlogFileName.date;
111
145
  }
112
- // Fallback to file create time
113
- return (await fs_extra_1.default.stat(blogSourceAbsolute)).birthtime;
146
+ try {
147
+ const result = (0, utils_1.getFileCommitDate)(blogSourceAbsolute, {
148
+ age: 'oldest',
149
+ includeAuthor: false,
150
+ });
151
+ return result.date;
152
+ }
153
+ catch (err) {
154
+ logger_1.default.error(err);
155
+ return (await fs_extra_1.default.stat(blogSourceAbsolute)).birthtime;
156
+ }
114
157
  }
115
158
  const date = await getDate();
116
159
  const formattedDate = formatBlogPostDate(i18n.currentLocale, date);
@@ -174,7 +217,7 @@ async function processBlogSourceFile(blogSourceRelative, contentPaths, context,
174
217
  }
175
218
  async function generateBlogPosts(contentPaths, context, options) {
176
219
  const { include, exclude } = options;
177
- if (!fs_extra_1.default.existsSync(contentPaths.contentPath)) {
220
+ if (!(await fs_extra_1.default.pathExists(contentPaths.contentPath))) {
178
221
  return [];
179
222
  }
180
223
  const blogSourceFiles = await (0, utils_1.Globby)(include, {
@@ -189,9 +232,9 @@ async function generateBlogPosts(contentPaths, context, options) {
189
232
  try {
190
233
  return await processBlogSourceFile(blogSourceFile, contentPaths, context, options, authorsMap);
191
234
  }
192
- catch (e) {
193
- logger_1.default.error `Processing of blog source file failed for path path=${blogSourceFile}.`;
194
- throw e;
235
+ catch (err) {
236
+ logger_1.default.error `Processing of blog source file path=${blogSourceFile} failed.`;
237
+ throw err;
195
238
  }
196
239
  }))).filter(Boolean);
197
240
  blogPosts.sort((a, b) => b.metadata.date.getTime() - a.metadata.date.getTime());
package/lib/index.js CHANGED
@@ -62,6 +62,7 @@ async function pluginContentBlog(context, options) {
62
62
  blogListPaginated: [],
63
63
  blogTags: {},
64
64
  blogTagsListPath: null,
65
+ blogTagsPaginated: [],
65
66
  };
66
67
  }
67
68
  // Colocate next and prev metadata.
@@ -81,39 +82,20 @@ async function pluginContentBlog(context, options) {
81
82
  };
82
83
  }
83
84
  });
84
- // Blog pagination routes.
85
- // Example: `/blog`, `/blog/page/1`, `/blog/page/2`
86
- const totalCount = blogPosts.length;
87
- const postsPerPage = postsPerPageOption === 'ALL' ? totalCount : postsPerPageOption;
88
- const numberOfPages = Math.ceil(totalCount / postsPerPage);
89
85
  const baseBlogUrl = (0, utils_1.normalizeUrl)([baseUrl, routeBasePath]);
90
- const blogListPaginated = [];
91
- function blogPaginationPermalink(page) {
92
- return page > 0
93
- ? (0, utils_1.normalizeUrl)([baseBlogUrl, `page/${page + 1}`])
94
- : baseBlogUrl;
95
- }
96
- for (let page = 0; page < numberOfPages; page += 1) {
97
- blogListPaginated.push({
98
- metadata: {
99
- permalink: blogPaginationPermalink(page),
100
- page: page + 1,
101
- postsPerPage,
102
- totalPages: numberOfPages,
103
- totalCount,
104
- previousPage: page !== 0 ? blogPaginationPermalink(page - 1) : null,
105
- nextPage: page < numberOfPages - 1
106
- ? blogPaginationPermalink(page + 1)
107
- : null,
108
- blogDescription,
109
- blogTitle,
110
- },
111
- items: blogPosts
112
- .slice(page * postsPerPage, (page + 1) * postsPerPage)
113
- .map((item) => item.id),
114
- });
115
- }
116
- const blogTags = (0, blogUtils_1.getBlogTags)(blogPosts);
86
+ const blogListPaginated = (0, blogUtils_1.paginateBlogPosts)({
87
+ blogPosts,
88
+ blogTitle,
89
+ blogDescription,
90
+ postsPerPageOption,
91
+ basePageUrl: baseBlogUrl,
92
+ });
93
+ const blogTags = (0, blogUtils_1.getBlogTags)({
94
+ blogPosts,
95
+ postsPerPageOption,
96
+ blogDescription,
97
+ blogTitle,
98
+ });
117
99
  const tagsPath = (0, utils_1.normalizeUrl)([baseBlogUrl, tagsBasePath]);
118
100
  const blogTagsListPath = Object.keys(blogTags).length > 0 ? tagsPath : null;
119
101
  return {
@@ -128,7 +110,7 @@ async function pluginContentBlog(context, options) {
128
110
  if (!blogContents) {
129
111
  return;
130
112
  }
131
- const { blogListComponent, blogPostComponent, blogTagsListComponent, blogTagsPostsComponent, routeBasePath, archiveBasePath, } = options;
113
+ const { blogListComponent, blogPostComponent, blogTagsListComponent, blogTagsPostsComponent, blogArchiveComponent, routeBasePath, archiveBasePath, } = options;
132
114
  const { addRoute, createData } = actions;
133
115
  const { blogSidebarTitle, blogPosts, blogListPaginated, blogTags, blogTagsListPath, } = blogContents;
134
116
  const blogItemsToMetadata = {};
@@ -145,7 +127,7 @@ async function pluginContentBlog(context, options) {
145
127
  const archiveProp = await createData(`${(0, utils_1.docuHash)(archiveUrl)}.json`, JSON.stringify({ blogPosts }, null, 2));
146
128
  addRoute({
147
129
  path: archiveUrl,
148
- component: '@theme/BlogArchivePage',
130
+ component: blogArchiveComponent,
149
131
  exact: true,
150
132
  modules: {
151
133
  archive: aliasedSource(archiveProp),
@@ -193,7 +175,8 @@ async function pluginContentBlog(context, options) {
193
175
  modules: {
194
176
  sidebar: aliasedSource(sidebarProp),
195
177
  items: items.map((postID) =>
196
- // To tell routes.js this is an import and not a nested object to recurse.
178
+ // To tell routes.js this is an import and not a nested object
179
+ // to recurse.
197
180
  ({
198
181
  content: {
199
182
  __import: true,
@@ -211,40 +194,46 @@ async function pluginContentBlog(context, options) {
211
194
  if (blogTagsListPath === null) {
212
195
  return;
213
196
  }
214
- const tagsModule = {};
215
- await Promise.all(Object.keys(blogTags).map(async (tag) => {
216
- const { name, items, permalink } = blogTags[tag];
217
- // Refactor all this, see docs implementation
218
- tagsModule[tag] = {
197
+ const tagsModule = Object.fromEntries(Object.entries(blogTags).map(([tagKey, tag]) => {
198
+ const tagModule = {
219
199
  allTagsPath: blogTagsListPath,
220
- slug: tag,
221
- name,
222
- count: items.length,
223
- permalink,
200
+ slug: tagKey,
201
+ name: tag.name,
202
+ count: tag.items.length,
203
+ permalink: tag.permalink,
224
204
  };
225
- const tagsMetadataPath = await createData(`${(0, utils_1.docuHash)(permalink)}.json`, JSON.stringify(tagsModule[tag], null, 2));
226
- addRoute({
227
- path: permalink,
228
- component: blogTagsPostsComponent,
229
- exact: true,
230
- modules: {
231
- sidebar: aliasedSource(sidebarProp),
232
- items: items.map((postID) => {
233
- const metadata = blogItemsToMetadata[postID];
234
- return {
235
- content: {
236
- __import: true,
237
- path: metadata.source,
238
- query: {
239
- truncated: true,
240
- },
241
- },
242
- };
243
- }),
244
- metadata: aliasedSource(tagsMetadataPath),
245
- },
246
- });
205
+ return [tag.name, tagModule];
247
206
  }));
207
+ async function createTagRoutes(tag) {
208
+ await Promise.all(tag.pages.map(async (blogPaginated) => {
209
+ const { metadata, items } = blogPaginated;
210
+ const tagsMetadataPath = await createData(`${(0, utils_1.docuHash)(metadata.permalink)}.json`, JSON.stringify(tagsModule[tag.name], null, 2));
211
+ const listMetadataPath = await createData(`${(0, utils_1.docuHash)(metadata.permalink)}-list.json`, JSON.stringify(metadata, null, 2));
212
+ addRoute({
213
+ path: metadata.permalink,
214
+ component: blogTagsPostsComponent,
215
+ exact: true,
216
+ modules: {
217
+ sidebar: aliasedSource(sidebarProp),
218
+ items: items.map((postID) => {
219
+ const blogPostMetadata = blogItemsToMetadata[postID];
220
+ return {
221
+ content: {
222
+ __import: true,
223
+ path: blogPostMetadata.source,
224
+ query: {
225
+ truncated: true,
226
+ },
227
+ },
228
+ };
229
+ }),
230
+ metadata: aliasedSource(tagsMetadataPath),
231
+ listMetadata: aliasedSource(listMetadataPath),
232
+ },
233
+ });
234
+ }));
235
+ }
236
+ await Promise.all(Object.values(blogTags).map(createTagRoutes));
248
237
  // Only create /tags page if there are tags.
249
238
  if (Object.keys(blogTags).length > 0) {
250
239
  const tagsListPath = await createData(`${(0, utils_1.docuHash)(`${blogTagsListPath}-tags`)}.json`, JSON.stringify(tagsModule, null, 2));
@@ -286,7 +275,7 @@ async function pluginContentBlog(context, options) {
286
275
  module: {
287
276
  rules: [
288
277
  {
289
- test: /(\.mdx?)$/,
278
+ test: /\.mdx?$/i,
290
279
  include: contentDirs
291
280
  // Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970
292
281
  .map(utils_1.addTrailingPathSeparator),
@@ -311,7 +300,8 @@ async function pluginContentBlog(context, options) {
311
300
  // For blog posts a title in markdown is always removed
312
301
  // Blog posts title are rendered separately
313
302
  removeContentTitle: true,
314
- // Assets allow to convert some relative images paths to require() calls
303
+ // Assets allow to convert some relative images paths to
304
+ // require() calls
315
305
  createAssets: ({ frontMatter, metadata, }) => ({
316
306
  image: frontMatter.image,
317
307
  authorsImageUrls: metadata.authors.map((author) => author.imageURL),
@@ -14,7 +14,7 @@ exports.DEFAULT_OPTIONS = {
14
14
  beforeDefaultRehypePlugins: [],
15
15
  beforeDefaultRemarkPlugins: [],
16
16
  admonitions: {},
17
- truncateMarker: /<!--\s*(truncate)\s*-->/,
17
+ truncateMarker: /<!--\s*truncate\s*-->/,
18
18
  rehypePlugins: [],
19
19
  remarkPlugins: [],
20
20
  showReadingTime: true,
@@ -22,6 +22,7 @@ exports.DEFAULT_OPTIONS = {
22
22
  blogTagsListComponent: '@theme/BlogTagsListPage',
23
23
  blogPostComponent: '@theme/BlogPostPage',
24
24
  blogListComponent: '@theme/BlogListPage',
25
+ blogArchiveComponent: '@theme/BlogArchivePage',
25
26
  blogDescription: 'Blog',
26
27
  blogTitle: 'Blog',
27
28
  blogSidebarCount: 5,
@@ -57,6 +58,7 @@ exports.PluginOptionSchema = utils_validation_1.Joi.object({
57
58
  blogPostComponent: utils_validation_1.Joi.string().default(exports.DEFAULT_OPTIONS.blogPostComponent),
58
59
  blogTagsListComponent: utils_validation_1.Joi.string().default(exports.DEFAULT_OPTIONS.blogTagsListComponent),
59
60
  blogTagsPostsComponent: utils_validation_1.Joi.string().default(exports.DEFAULT_OPTIONS.blogTagsPostsComponent),
61
+ blogArchiveComponent: utils_validation_1.Joi.string().default(exports.DEFAULT_OPTIONS.blogArchiveComponent),
60
62
  blogTitle: utils_validation_1.Joi.string().allow('').default(exports.DEFAULT_OPTIONS.blogTitle),
61
63
  blogDescription: utils_validation_1.Joi.string()
62
64
  .allow('')
package/lib/types.d.ts CHANGED
@@ -16,12 +16,13 @@ export interface BlogContent {
16
16
  blogTagsListPath: string | null;
17
17
  }
18
18
  export interface BlogTags {
19
- [key: string]: BlogTag;
19
+ [tagKey: string]: BlogTag;
20
20
  }
21
21
  export interface BlogTag {
22
22
  name: string;
23
23
  items: string[];
24
24
  permalink: string;
25
+ pages: BlogPaginated[];
25
26
  }
26
27
  export interface BlogPost {
27
28
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docusaurus/plugin-content-blog",
3
- "version": "2.0.0-beta.15",
3
+ "version": "2.0.0-beta.16",
4
4
  "description": "Blog plugin for Docusaurus.",
5
5
  "main": "lib/index.js",
6
6
  "types": "src/plugin-content-blog.d.ts",
@@ -18,24 +18,24 @@
18
18
  },
19
19
  "license": "MIT",
20
20
  "dependencies": {
21
- "@docusaurus/core": "2.0.0-beta.15",
22
- "@docusaurus/logger": "2.0.0-beta.15",
23
- "@docusaurus/mdx-loader": "2.0.0-beta.15",
24
- "@docusaurus/utils": "2.0.0-beta.15",
25
- "@docusaurus/utils-common": "2.0.0-beta.15",
26
- "@docusaurus/utils-validation": "2.0.0-beta.15",
21
+ "@docusaurus/core": "2.0.0-beta.16",
22
+ "@docusaurus/logger": "2.0.0-beta.16",
23
+ "@docusaurus/mdx-loader": "2.0.0-beta.16",
24
+ "@docusaurus/utils": "2.0.0-beta.16",
25
+ "@docusaurus/utils-common": "2.0.0-beta.16",
26
+ "@docusaurus/utils-validation": "2.0.0-beta.16",
27
27
  "cheerio": "^1.0.0-rc.10",
28
28
  "feed": "^4.2.2",
29
- "fs-extra": "^10.0.0",
30
- "lodash": "^4.17.20",
29
+ "fs-extra": "^10.0.1",
30
+ "lodash": "^4.17.21",
31
31
  "reading-time": "^1.5.0",
32
32
  "remark-admonitions": "^1.2.1",
33
33
  "tslib": "^2.3.1",
34
34
  "utility-types": "^3.10.0",
35
- "webpack": "^5.61.0"
35
+ "webpack": "^5.69.1"
36
36
  },
37
37
  "devDependencies": {
38
- "@docusaurus/types": "2.0.0-beta.15",
38
+ "@docusaurus/types": "2.0.0-beta.16",
39
39
  "escape-string-regexp": "^4.0.0"
40
40
  },
41
41
  "peerDependencies": {
@@ -45,5 +45,5 @@
45
45
  "engines": {
46
46
  "node": ">=14"
47
47
  },
48
- "gitHead": "6cfad16436c07d8d11e5c2e1486dc59afd483e33"
48
+ "gitHead": "eb43c4d4f95a4fb97dc9bb9dc615413e0dc2e1e7"
49
49
  }
package/src/authors.ts CHANGED
@@ -83,9 +83,9 @@ function normalizeFrontMatterAuthors(
83
83
  authorInput: string | BlogPostFrontMatterAuthor,
84
84
  ): BlogPostFrontMatterAuthor {
85
85
  if (typeof authorInput === 'string') {
86
- // Technically, we could allow users to provide an author's name here
87
- // IMHO it's better to only support keys here
88
- // Reason: a typo in a key would fallback to becoming a name and may end-up un-noticed
86
+ // Technically, we could allow users to provide an author's name here, but
87
+ // we only support keys, otherwise, a typo in a key would fallback to
88
+ // becoming a name and may end up unnoticed
89
89
  return {key: authorInput};
90
90
  }
91
91
  return authorInput;
@@ -137,7 +137,8 @@ export function getBlogPostAuthors(params: AuthorsParam): Author[] {
137
137
  const authors = getFrontMatterAuthors(params);
138
138
 
139
139
  if (authorLegacy) {
140
- // Technically, we could allow mixing legacy/authors front matter, but do we really want to?
140
+ // Technically, we could allow mixing legacy/authors front matter, but do we
141
+ // really want to?
141
142
  if (authors.length > 0) {
142
143
  throw new Error(
143
144
  `To declare blog post authors, use the 'authors' front matter in priority.
package/src/blogUtils.ts CHANGED
@@ -8,12 +8,13 @@
8
8
  import fs from 'fs-extra';
9
9
  import path from 'path';
10
10
  import readingTime from 'reading-time';
11
- import {keyBy, mapValues} from 'lodash';
11
+ import _ from 'lodash';
12
12
  import type {
13
13
  BlogPost,
14
14
  BlogContentPaths,
15
15
  BlogMarkdownLoaderOptions,
16
16
  BlogTags,
17
+ BlogPaginated,
17
18
  } from './types';
18
19
  import {
19
20
  parseMarkdownString,
@@ -26,6 +27,7 @@ import {
26
27
  Globby,
27
28
  normalizeFrontMatterTags,
28
29
  groupTaggedItems,
30
+ getFileCommitDate,
29
31
  getContentPathList,
30
32
  } from '@docusaurus/utils';
31
33
  import type {LoadContext} from '@docusaurus/types';
@@ -44,26 +46,85 @@ export function truncate(fileString: string, truncateMarker: RegExp): string {
44
46
  export function getSourceToPermalink(
45
47
  blogPosts: BlogPost[],
46
48
  ): Record<string, string> {
47
- return mapValues(
48
- keyBy(blogPosts, (item) => item.metadata.source),
49
- (v) => v.metadata.permalink,
49
+ return Object.fromEntries(
50
+ blogPosts.map(({metadata: {source, permalink}}) => [source, permalink]),
50
51
  );
51
52
  }
52
53
 
53
- export function getBlogTags(blogPosts: BlogPost[]): BlogTags {
54
+ export function paginateBlogPosts({
55
+ blogPosts,
56
+ basePageUrl,
57
+ blogTitle,
58
+ blogDescription,
59
+ postsPerPageOption,
60
+ }: {
61
+ blogPosts: BlogPost[];
62
+ basePageUrl: string;
63
+ blogTitle: string;
64
+ blogDescription: string;
65
+ postsPerPageOption: number | 'ALL';
66
+ }): BlogPaginated[] {
67
+ const totalCount = blogPosts.length;
68
+ const postsPerPage =
69
+ postsPerPageOption === 'ALL' ? totalCount : postsPerPageOption;
70
+ const numberOfPages = Math.ceil(totalCount / postsPerPage);
71
+
72
+ const pages: BlogPaginated[] = [];
73
+
74
+ function permalink(page: number) {
75
+ return page > 0 ? `${basePageUrl}/page/${page + 1}` : basePageUrl;
76
+ }
77
+
78
+ for (let page = 0; page < numberOfPages; page += 1) {
79
+ pages.push({
80
+ items: blogPosts
81
+ .slice(page * postsPerPage, (page + 1) * postsPerPage)
82
+ .map((item) => item.id),
83
+ metadata: {
84
+ permalink: permalink(page),
85
+ page: page + 1,
86
+ postsPerPage,
87
+ totalPages: numberOfPages,
88
+ totalCount,
89
+ previousPage: page !== 0 ? permalink(page - 1) : null,
90
+ nextPage: page < numberOfPages - 1 ? permalink(page + 1) : null,
91
+ blogDescription,
92
+ blogTitle,
93
+ },
94
+ });
95
+ }
96
+
97
+ return pages;
98
+ }
99
+
100
+ export function getBlogTags({
101
+ blogPosts,
102
+ ...params
103
+ }: {
104
+ blogPosts: BlogPost[];
105
+ blogTitle: string;
106
+ blogDescription: string;
107
+ postsPerPageOption: number | 'ALL';
108
+ }): BlogTags {
54
109
  const groups = groupTaggedItems(
55
110
  blogPosts,
56
111
  (blogPost) => blogPost.metadata.tags,
57
112
  );
58
- return mapValues(groups, (group) => ({
59
- name: group.tag.label,
60
- items: group.items.map((item) => item.id),
61
- permalink: group.tag.permalink,
113
+
114
+ return _.mapValues(groups, ({tag, items: tagBlogPosts}) => ({
115
+ name: tag.label,
116
+ items: tagBlogPosts.map((item) => item.id),
117
+ permalink: tag.permalink,
118
+ pages: paginateBlogPosts({
119
+ blogPosts: tagBlogPosts,
120
+ basePageUrl: tag.permalink,
121
+ ...params,
122
+ }),
62
123
  }));
63
124
  }
64
125
 
65
126
  const DATE_FILENAME_REGEX =
66
- /^(?<folder>.*)(?<date>\d{4}[-/]\d{1,2}[-/]\d{1,2})[-/]?(?<text>.*?)(\/index)?.mdx?$/;
127
+ /^(?<folder>.*)(?<date>\d{4}[-/]\d{1,2}[-/]\d{1,2})[-/]?(?<text>.*?)(?:\/index)?.mdx?$/;
67
128
 
68
129
  type ParsedBlogFileName = {
69
130
  date: Date | undefined;
@@ -82,11 +143,10 @@ export function parseBlogFileName(
82
143
  const slugDate = dateString.replace(/-/g, '/');
83
144
  const slug = `/${slugDate}/${folder}${text}`;
84
145
  return {date, text, slug};
85
- } else {
86
- const text = blogSourceRelative.replace(/(\/index)?\.mdx?$/, '');
87
- const slug = `/${text}`;
88
- return {date: undefined, text, slug};
89
146
  }
147
+ const text = blogSourceRelative.replace(/(?:\/index)?\.mdx?$/, '');
148
+ const slug = `/${text}`;
149
+ return {date: undefined, text, slug};
90
150
  }
91
151
 
92
152
  function formatBlogPostDate(locale: string, date: Date): string {
@@ -97,8 +157,9 @@ function formatBlogPostDate(locale: string, date: Date): string {
97
157
  year: 'numeric',
98
158
  timeZone: 'UTC',
99
159
  }).format(date);
100
- } catch (e) {
101
- throw new Error(`Can't format blog post date "${date}"`);
160
+ } catch (err) {
161
+ logger.error`Can't format blog post date "${String(date)}"`;
162
+ throw err;
102
163
  }
103
164
  }
104
165
 
@@ -112,12 +173,9 @@ async function parseBlogPostMarkdownFile(blogSourceAbsolute: string) {
112
173
  ...result,
113
174
  frontMatter: validateBlogPostFrontMatter(result.frontMatter),
114
175
  };
115
- } catch (e) {
116
- throw new Error(
117
- `Error while parsing blog post file ${blogSourceAbsolute}: "${
118
- (e as Error).message
119
- }".`,
120
- );
176
+ } catch (err) {
177
+ logger.error`Error while parsing blog post file path=${blogSourceAbsolute}.`;
178
+ throw err;
121
179
  }
122
180
  }
123
181
 
@@ -179,8 +237,17 @@ async function processBlogSourceFile(
179
237
  } else if (parsedBlogFileName.date) {
180
238
  return parsedBlogFileName.date;
181
239
  }
182
- // Fallback to file create time
183
- return (await fs.stat(blogSourceAbsolute)).birthtime;
240
+
241
+ try {
242
+ const result = getFileCommitDate(blogSourceAbsolute, {
243
+ age: 'oldest',
244
+ includeAuthor: false,
245
+ });
246
+ return result.date;
247
+ } catch (err) {
248
+ logger.error(err);
249
+ return (await fs.stat(blogSourceAbsolute)).birthtime;
250
+ }
184
251
  }
185
252
 
186
253
  const date = await getDate();
@@ -263,7 +330,7 @@ export async function generateBlogPosts(
263
330
  ): Promise<BlogPost[]> {
264
331
  const {include, exclude} = options;
265
332
 
266
- if (!fs.existsSync(contentPaths.contentPath)) {
333
+ if (!(await fs.pathExists(contentPaths.contentPath))) {
267
334
  return [];
268
335
  }
269
336
 
@@ -288,9 +355,9 @@ export async function generateBlogPosts(
288
355
  options,
289
356
  authorsMap,
290
357
  );
291
- } catch (e) {
292
- logger.error`Processing of blog source file failed for path path=${blogSourceFile}.`;
293
- throw e;
358
+ } catch (err) {
359
+ logger.error`Processing of blog source file path=${blogSourceFile} failed.`;
360
+ throw err;
294
361
  }
295
362
  }),
296
363
  )
package/src/index.ts CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  import {translateContent, getTranslationFiles} from './translations';
24
24
 
25
25
  import type {
26
+ BlogTag,
26
27
  BlogTags,
27
28
  BlogContent,
28
29
  BlogItemsToMetadata,
@@ -31,21 +32,21 @@ import type {
31
32
  BlogContentPaths,
32
33
  BlogMarkdownLoaderOptions,
33
34
  MetaData,
35
+ TagModule,
34
36
  } from './types';
35
37
  import {PluginOptionSchema} from './pluginOptionSchema';
36
38
  import type {
37
39
  LoadContext,
38
- ConfigureWebpackUtils,
39
40
  Plugin,
40
41
  HtmlTags,
41
42
  OptionValidationContext,
42
43
  ValidationResult,
43
44
  } from '@docusaurus/types';
44
- import type {Configuration} from 'webpack';
45
45
  import {
46
46
  generateBlogPosts,
47
47
  getSourceToPermalink,
48
48
  getBlogTags,
49
+ paginateBlogPosts,
49
50
  } from './blogUtils';
50
51
  import {createBlogFeedFiles} from './feed';
51
52
  import type {
@@ -134,6 +135,7 @@ export default async function pluginContentBlog(
134
135
  blogListPaginated: [],
135
136
  blogTags: {},
136
137
  blogTagsListPath: null,
138
+ blogTagsPaginated: [],
137
139
  };
138
140
  }
139
141
 
@@ -157,45 +159,22 @@ export default async function pluginContentBlog(
157
159
  }
158
160
  });
159
161
 
160
- // Blog pagination routes.
161
- // Example: `/blog`, `/blog/page/1`, `/blog/page/2`
162
- const totalCount = blogPosts.length;
163
- const postsPerPage =
164
- postsPerPageOption === 'ALL' ? totalCount : postsPerPageOption;
165
- const numberOfPages = Math.ceil(totalCount / postsPerPage);
166
162
  const baseBlogUrl = normalizeUrl([baseUrl, routeBasePath]);
167
163
 
168
- const blogListPaginated: BlogPaginated[] = [];
169
-
170
- function blogPaginationPermalink(page: number) {
171
- return page > 0
172
- ? normalizeUrl([baseBlogUrl, `page/${page + 1}`])
173
- : baseBlogUrl;
174
- }
175
-
176
- for (let page = 0; page < numberOfPages; page += 1) {
177
- blogListPaginated.push({
178
- metadata: {
179
- permalink: blogPaginationPermalink(page),
180
- page: page + 1,
181
- postsPerPage,
182
- totalPages: numberOfPages,
183
- totalCount,
184
- previousPage: page !== 0 ? blogPaginationPermalink(page - 1) : null,
185
- nextPage:
186
- page < numberOfPages - 1
187
- ? blogPaginationPermalink(page + 1)
188
- : null,
189
- blogDescription,
190
- blogTitle,
191
- },
192
- items: blogPosts
193
- .slice(page * postsPerPage, (page + 1) * postsPerPage)
194
- .map((item) => item.id),
195
- });
196
- }
164
+ const blogListPaginated: BlogPaginated[] = paginateBlogPosts({
165
+ blogPosts,
166
+ blogTitle,
167
+ blogDescription,
168
+ postsPerPageOption,
169
+ basePageUrl: baseBlogUrl,
170
+ });
197
171
 
198
- const blogTags: BlogTags = getBlogTags(blogPosts);
172
+ const blogTags: BlogTags = getBlogTags({
173
+ blogPosts,
174
+ postsPerPageOption,
175
+ blogDescription,
176
+ blogTitle,
177
+ });
199
178
 
200
179
  const tagsPath = normalizeUrl([baseBlogUrl, tagsBasePath]);
201
180
 
@@ -221,6 +200,7 @@ export default async function pluginContentBlog(
221
200
  blogPostComponent,
222
201
  blogTagsListComponent,
223
202
  blogTagsPostsComponent,
203
+ blogArchiveComponent,
224
204
  routeBasePath,
225
205
  archiveBasePath,
226
206
  } = options;
@@ -254,7 +234,7 @@ export default async function pluginContentBlog(
254
234
  );
255
235
  addRoute({
256
236
  path: archiveUrl,
257
- component: '@theme/BlogArchivePage',
237
+ component: blogArchiveComponent,
258
238
  exact: true,
259
239
  modules: {
260
240
  archive: aliasedSource(archiveProp),
@@ -322,7 +302,8 @@ export default async function pluginContentBlog(
322
302
  modules: {
323
303
  sidebar: aliasedSource(sidebarProp),
324
304
  items: items.map((postID) =>
325
- // To tell routes.js this is an import and not a nested object to recurse.
305
+ // To tell routes.js this is an import and not a nested object
306
+ // to recurse.
326
307
  ({
327
308
  content: {
328
309
  __import: true,
@@ -344,50 +325,61 @@ export default async function pluginContentBlog(
344
325
  return;
345
326
  }
346
327
 
347
- const tagsModule: TagsModule = {};
348
-
349
- await Promise.all(
350
- Object.keys(blogTags).map(async (tag) => {
351
- const {name, items, permalink} = blogTags[tag];
352
-
353
- // Refactor all this, see docs implementation
354
- tagsModule[tag] = {
328
+ const tagsModule: TagsModule = Object.fromEntries(
329
+ Object.entries(blogTags).map(([tagKey, tag]) => {
330
+ const tagModule: TagModule = {
355
331
  allTagsPath: blogTagsListPath,
356
- slug: tag,
357
- name,
358
- count: items.length,
359
- permalink,
332
+ slug: tagKey,
333
+ name: tag.name,
334
+ count: tag.items.length,
335
+ permalink: tag.permalink,
360
336
  };
361
-
362
- const tagsMetadataPath = await createData(
363
- `${docuHash(permalink)}.json`,
364
- JSON.stringify(tagsModule[tag], null, 2),
365
- );
366
-
367
- addRoute({
368
- path: permalink,
369
- component: blogTagsPostsComponent,
370
- exact: true,
371
- modules: {
372
- sidebar: aliasedSource(sidebarProp),
373
- items: items.map((postID) => {
374
- const metadata = blogItemsToMetadata[postID];
375
- return {
376
- content: {
377
- __import: true,
378
- path: metadata.source,
379
- query: {
380
- truncated: true,
381
- },
382
- },
383
- };
384
- }),
385
- metadata: aliasedSource(tagsMetadataPath),
386
- },
387
- });
337
+ return [tag.name, tagModule];
388
338
  }),
389
339
  );
390
340
 
341
+ async function createTagRoutes(tag: BlogTag): Promise<void> {
342
+ await Promise.all(
343
+ tag.pages.map(async (blogPaginated) => {
344
+ const {metadata, items} = blogPaginated;
345
+ const tagsMetadataPath = await createData(
346
+ `${docuHash(metadata.permalink)}.json`,
347
+ JSON.stringify(tagsModule[tag.name], null, 2),
348
+ );
349
+
350
+ const listMetadataPath = await createData(
351
+ `${docuHash(metadata.permalink)}-list.json`,
352
+ JSON.stringify(metadata, null, 2),
353
+ );
354
+
355
+ addRoute({
356
+ path: metadata.permalink,
357
+ component: blogTagsPostsComponent,
358
+ exact: true,
359
+ modules: {
360
+ sidebar: aliasedSource(sidebarProp),
361
+ items: items.map((postID) => {
362
+ const blogPostMetadata = blogItemsToMetadata[postID];
363
+ return {
364
+ content: {
365
+ __import: true,
366
+ path: blogPostMetadata.source,
367
+ query: {
368
+ truncated: true,
369
+ },
370
+ },
371
+ };
372
+ }),
373
+ metadata: aliasedSource(tagsMetadataPath),
374
+ listMetadata: aliasedSource(listMetadataPath),
375
+ },
376
+ });
377
+ }),
378
+ );
379
+ }
380
+
381
+ await Promise.all(Object.values(blogTags).map(createTagRoutes));
382
+
391
383
  // Only create /tags page if there are tags.
392
384
  if (Object.keys(blogTags).length > 0) {
393
385
  const tagsListPath = await createData(
@@ -411,12 +403,7 @@ export default async function pluginContentBlog(
411
403
  return translateContent(content, translationFiles);
412
404
  },
413
405
 
414
- configureWebpack(
415
- _config: Configuration,
416
- isServer: boolean,
417
- {getJSLoader}: ConfigureWebpackUtils,
418
- content,
419
- ) {
406
+ configureWebpack(_config, isServer, {getJSLoader}, content) {
420
407
  const {
421
408
  rehypePlugins,
422
409
  remarkPlugins,
@@ -451,7 +438,7 @@ export default async function pluginContentBlog(
451
438
  module: {
452
439
  rules: [
453
440
  {
454
- test: /(\.mdx?)$/,
441
+ test: /\.mdx?$/i,
455
442
  include: contentDirs
456
443
  // Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970
457
444
  .map(addTrailingPathSeparator),
@@ -485,7 +472,8 @@ export default async function pluginContentBlog(
485
472
  // Blog posts title are rendered separately
486
473
  removeContentTitle: true,
487
474
 
488
- // Assets allow to convert some relative images paths to require() calls
475
+ // Assets allow to convert some relative images paths to
476
+ // require() calls
489
477
  createAssets: ({
490
478
  frontMatter,
491
479
  metadata,
@@ -120,6 +120,7 @@ declare module '@docusaurus/plugin-content-blog' {
120
120
  blogPostComponent: string;
121
121
  blogTagsListComponent: string;
122
122
  blogTagsPostsComponent: string;
123
+ blogArchiveComponent: string;
123
124
  blogTitle: string;
124
125
  blogDescription: string;
125
126
  blogSidebarCount: number | 'ALL';
@@ -147,21 +148,6 @@ declare module '@docusaurus/plugin-content-blog' {
147
148
  >;
148
149
  }
149
150
 
150
- declare module '@theme/BlogSidebar' {
151
- export type BlogSidebarItem = {title: string; permalink: string};
152
- export type BlogSidebar = {
153
- title: string;
154
- items: BlogSidebarItem[];
155
- };
156
-
157
- export interface Props {
158
- readonly sidebar: BlogSidebar;
159
- }
160
-
161
- const BlogSidebar: (props: Props) => JSX.Element;
162
- export default BlogSidebar;
163
- }
164
-
165
151
  declare module '@theme/BlogPostPage' {
166
152
  import type {BlogSidebar} from '@theme/BlogSidebar';
167
153
  import type {TOCItem} from '@docusaurus/types';
@@ -205,8 +191,7 @@ declare module '@theme/BlogPostPage' {
205
191
  readonly content: Content;
206
192
  }
207
193
 
208
- const BlogPostPage: (props: Props) => JSX.Element;
209
- export default BlogPostPage;
194
+ export default function BlogPostPage(props: Props): JSX.Element;
210
195
  }
211
196
 
212
197
  declare module '@theme/BlogListPage' {
@@ -231,8 +216,7 @@ declare module '@theme/BlogListPage' {
231
216
  readonly items: readonly {readonly content: Content}[];
232
217
  }
233
218
 
234
- const BlogListPage: (props: Props) => JSX.Element;
235
- export default BlogListPage;
219
+ export default function BlogListPage(props: Props): JSX.Element;
236
220
  }
237
221
 
238
222
  declare module '@theme/BlogTagsListPage' {
@@ -251,23 +235,23 @@ declare module '@theme/BlogTagsListPage' {
251
235
  readonly tags: Readonly<Record<string, Tag>>;
252
236
  }
253
237
 
254
- const BlogTagsListPage: (props: Props) => JSX.Element;
255
- export default BlogTagsListPage;
238
+ export default function BlogTagsListPage(props: Props): JSX.Element;
256
239
  }
257
240
 
258
241
  declare module '@theme/BlogTagsPostsPage' {
259
242
  import type {BlogSidebar} from '@theme/BlogSidebar';
260
243
  import type {Tag} from '@theme/BlogTagsListPage';
261
244
  import type {Content} from '@theme/BlogPostPage';
245
+ import type {Metadata} from '@theme/BlogListPage';
262
246
 
263
247
  export interface Props {
264
248
  readonly sidebar: BlogSidebar;
265
249
  readonly metadata: Tag;
250
+ readonly listMetadata: Metadata;
266
251
  readonly items: readonly {readonly content: Content}[];
267
252
  }
268
253
 
269
- const BlogTagsPostsPage: (props: Props) => JSX.Element;
270
- export default BlogTagsPostsPage;
254
+ export default function BlogTagsPostsPage(props: Props): JSX.Element;
271
255
  }
272
256
 
273
257
  declare module '@theme/BlogArchivePage' {
@@ -20,7 +20,7 @@ export const DEFAULT_OPTIONS: PluginOptions = {
20
20
  beforeDefaultRehypePlugins: [],
21
21
  beforeDefaultRemarkPlugins: [],
22
22
  admonitions: {},
23
- truncateMarker: /<!--\s*(truncate)\s*-->/,
23
+ truncateMarker: /<!--\s*truncate\s*-->/,
24
24
  rehypePlugins: [],
25
25
  remarkPlugins: [],
26
26
  showReadingTime: true,
@@ -28,6 +28,7 @@ export const DEFAULT_OPTIONS: PluginOptions = {
28
28
  blogTagsListComponent: '@theme/BlogTagsListPage',
29
29
  blogPostComponent: '@theme/BlogPostPage',
30
30
  blogListComponent: '@theme/BlogListPage',
31
+ blogArchiveComponent: '@theme/BlogArchivePage',
31
32
  blogDescription: 'Blog',
32
33
  blogTitle: 'Blog',
33
34
  blogSidebarCount: 5,
@@ -68,6 +69,9 @@ export const PluginOptionSchema = Joi.object<PluginOptions>({
68
69
  blogTagsPostsComponent: Joi.string().default(
69
70
  DEFAULT_OPTIONS.blogTagsPostsComponent,
70
71
  ),
72
+ blogArchiveComponent: Joi.string().default(
73
+ DEFAULT_OPTIONS.blogArchiveComponent,
74
+ ),
71
75
  blogTitle: Joi.string().allow('').default(DEFAULT_OPTIONS.blogTitle),
72
76
  blogDescription: Joi.string()
73
77
  .allow('')
package/src/types.ts CHANGED
@@ -26,13 +26,18 @@ export interface BlogContent {
26
26
  }
27
27
 
28
28
  export interface BlogTags {
29
- [key: string]: BlogTag;
29
+ // TODO, the key is the tag slug/permalink
30
+ // This is due to legacy frontmatter: tags:
31
+ // [{label: "xyz", permalink: "/1"}, {label: "xyz", permalink: "/2"}]
32
+ // Soon we should forbid declaring permalink through frontmatter
33
+ [tagKey: string]: BlogTag;
30
34
  }
31
35
 
32
36
  export interface BlogTag {
33
37
  name: string;
34
- items: string[];
38
+ items: string[]; // blog post permalinks
35
39
  permalink: string;
40
+ pages: BlogPaginated[];
36
41
  }
37
42
 
38
43
  export interface BlogPost {
@@ -55,7 +60,7 @@ export interface BlogPaginatedMetadata {
55
60
 
56
61
  export interface BlogPaginated {
57
62
  metadata: BlogPaginatedMetadata;
58
- items: string[];
63
+ items: string[]; // blog post permalinks
59
64
  }
60
65
 
61
66
  export interface MetaData {