@docusaurus/plugin-content-blog 2.0.0-beta.18 → 2.0.0-beta.19

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.
@@ -4,9 +4,9 @@
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, BlogPaginated } from './types';
7
+ import type { BlogContentPaths, BlogMarkdownLoaderOptions } from './types';
8
8
  import type { LoadContext } from '@docusaurus/types';
9
- import type { PluginOptions } from '@docusaurus/plugin-content-blog';
9
+ import type { PluginOptions, BlogPost, BlogTags, BlogPaginated } from '@docusaurus/plugin-content-blog';
10
10
  export declare function truncate(fileString: string, truncateMarker: RegExp): string;
11
11
  export declare function getSourceToPermalink(blogPosts: BlogPost[]): {
12
12
  [aliasedPath: string]: string;
package/lib/blogUtils.js CHANGED
@@ -58,7 +58,7 @@ exports.paginateBlogPosts = paginateBlogPosts;
58
58
  function getBlogTags({ blogPosts, ...params }) {
59
59
  const groups = (0, utils_1.groupTaggedItems)(blogPosts, (blogPost) => blogPost.metadata.tags);
60
60
  return lodash_1.default.mapValues(groups, ({ tag, items: tagBlogPosts }) => ({
61
- name: tag.label,
61
+ label: tag.label,
62
62
  items: tagBlogPosts.map((item) => item.id),
63
63
  permalink: tag.permalink,
64
64
  pages: paginateBlogPosts({
@@ -85,13 +85,14 @@ function parseBlogFileName(blogSourceRelative) {
85
85
  return { date: undefined, text, slug };
86
86
  }
87
87
  exports.parseBlogFileName = parseBlogFileName;
88
- function formatBlogPostDate(locale, date) {
88
+ function formatBlogPostDate(locale, date, calendar) {
89
89
  try {
90
90
  return new Intl.DateTimeFormat(locale, {
91
91
  day: 'numeric',
92
92
  month: 'long',
93
93
  year: 'numeric',
94
94
  timeZone: 'UTC',
95
+ calendar,
95
96
  }).format(date);
96
97
  }
97
98
  catch (err) {
@@ -157,7 +158,7 @@ async function processBlogSourceFile(blogSourceRelative, contentPaths, context,
157
158
  }
158
159
  }
159
160
  const date = await getDate();
160
- const formattedDate = formatBlogPostDate(i18n.currentLocale, date);
161
+ const formattedDate = formatBlogPostDate(i18n.currentLocale, date, i18n.localeConfigs[i18n.currentLocale].calendar);
161
162
  const title = frontMatter.title ?? contentTitle ?? parsedBlogFileName.text;
162
163
  const description = frontMatter.description ?? excerpt ?? '';
163
164
  const slug = frontMatter.slug || parsedBlogFileName.slug;
package/lib/feed.d.ts CHANGED
@@ -4,9 +4,8 @@
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 } from './types';
8
7
  import type { DocusaurusConfig } from '@docusaurus/types';
9
- import type { PluginOptions } from '@docusaurus/plugin-content-blog';
8
+ import type { PluginOptions, BlogPost } from '@docusaurus/plugin-content-blog';
10
9
  export declare function createBlogFeedFiles({ blogPosts, options, siteConfig, outDir, locale, }: {
11
10
  blogPosts: BlogPost[];
12
11
  options: PluginOptions;
package/lib/feed.js CHANGED
@@ -35,7 +35,7 @@ async function generateBlogFeed({ blogPosts, options, siteConfig, outDir, locale
35
35
  function toFeedAuthor(author) {
36
36
  return { name: author.name, link: author.url, email: author.email };
37
37
  }
38
- await (0, utils_1.mapAsyncSequential)(blogPosts, async (post) => {
38
+ await Promise.all(blogPosts.map(async (post) => {
39
39
  const { id, metadata: { title: metadataTitle, permalink, date, description, authors, tags, }, } = post;
40
40
  const content = await (0, utils_1.readOutputHTMLFile)(permalink.replace(siteConfig.baseUrl, ''), outDir, siteConfig.trailingSlash);
41
41
  const $ = (0, cheerio_1.load)(content);
@@ -55,8 +55,8 @@ async function generateBlogFeed({ blogPosts, options, siteConfig, outDir, locale
55
55
  if (feedItemAuthors.length > 0) {
56
56
  feedItem.author = feedItemAuthors;
57
57
  }
58
- feed.addItem(feedItem);
59
- });
58
+ return feedItem;
59
+ })).then((items) => items.forEach(feed.addItem));
60
60
  return feed;
61
61
  }
62
62
  async function createBlogFeedFile({ feed, feedType, generatePath, }) {
@@ -73,7 +73,7 @@ async function createBlogFeedFile({ feed, feedType, generatePath, }) {
73
73
  }
74
74
  })();
75
75
  try {
76
- await fs_extra_1.default.outputFile((0, utils_1.posixPath)(path_1.default.join(generatePath, feedPath)), feedContent);
76
+ await fs_extra_1.default.outputFile(path_1.default.join(generatePath, feedPath), feedContent);
77
77
  }
78
78
  catch (err) {
79
79
  throw new Error(`Generating ${feedType} feed failed: ${err}.`);
package/lib/index.d.ts CHANGED
@@ -4,8 +4,7 @@
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 { BlogContent } from './types';
8
7
  import type { LoadContext, Plugin } from '@docusaurus/types';
9
- import type { PluginOptions } from '@docusaurus/plugin-content-blog';
8
+ import type { PluginOptions, BlogContent } from '@docusaurus/plugin-content-blog';
10
9
  export default function pluginContentBlog(context: LoadContext, options: PluginOptions): Promise<Plugin<BlogContent>>;
11
10
  export { validateOptions } from './options';
package/lib/index.js CHANGED
@@ -10,6 +10,7 @@ exports.validateOptions = void 0;
10
10
  const tslib_1 = require("tslib");
11
11
  const path_1 = tslib_1.__importDefault(require("path"));
12
12
  const remark_admonitions_1 = tslib_1.__importDefault(require("remark-admonitions"));
13
+ const footnoteIDFixer_1 = tslib_1.__importDefault(require("./remark/footnoteIDFixer"));
13
14
  const utils_1 = require("@docusaurus/utils");
14
15
  const translations_1 = require("./translations");
15
16
  const blogUtils_1 = require("./blogUtils");
@@ -46,12 +47,14 @@ async function pluginContentBlog(context, options) {
46
47
  const contentMarkdownGlobs = (0, utils_1.getContentPathList)(contentPaths).flatMap((contentPath) => include.map((pattern) => `${contentPath}/${pattern}`));
47
48
  return [authorsMapFilePath, ...contentMarkdownGlobs].filter(Boolean);
48
49
  },
49
- async getTranslationFiles() {
50
+ getTranslationFiles() {
50
51
  return (0, translations_1.getTranslationFiles)(options);
51
52
  },
52
53
  // Fetches blog contents and returns metadata for the necessary routes.
53
54
  async loadContent() {
54
55
  const { postsPerPage: postsPerPageOption, routeBasePath, tagsBasePath, blogDescription, blogTitle, blogSidebarTitle, } = options;
56
+ const baseBlogUrl = (0, utils_1.normalizeUrl)([baseUrl, routeBasePath]);
57
+ const blogTagsListPath = (0, utils_1.normalizeUrl)([baseBlogUrl, tagsBasePath]);
55
58
  const blogPosts = await (0, blogUtils_1.generateBlogPosts)(contentPaths, context, options);
56
59
  if (!blogPosts.length) {
57
60
  return {
@@ -59,7 +62,7 @@ async function pluginContentBlog(context, options) {
59
62
  blogPosts: [],
60
63
  blogListPaginated: [],
61
64
  blogTags: {},
62
- blogTagsListPath: null,
65
+ blogTagsListPath,
63
66
  blogTagsPaginated: [],
64
67
  };
65
68
  }
@@ -80,7 +83,6 @@ async function pluginContentBlog(context, options) {
80
83
  };
81
84
  }
82
85
  });
83
- const baseBlogUrl = (0, utils_1.normalizeUrl)([baseUrl, routeBasePath]);
84
86
  const blogListPaginated = (0, blogUtils_1.paginateBlogPosts)({
85
87
  blogPosts,
86
88
  blogTitle,
@@ -94,8 +96,6 @@ async function pluginContentBlog(context, options) {
94
96
  blogDescription,
95
97
  blogTitle,
96
98
  });
97
- const tagsPath = (0, utils_1.normalizeUrl)([baseBlogUrl, tagsBasePath]);
98
- const blogTagsListPath = Object.keys(blogTags).length > 0 ? tagsPath : null;
99
99
  return {
100
100
  blogSidebarTitle,
101
101
  blogPosts,
@@ -121,7 +121,7 @@ async function pluginContentBlog(context, options) {
121
121
  routeBasePath,
122
122
  archiveBasePath,
123
123
  ]);
124
- // creates a blog archive route
124
+ // Create a blog archive route
125
125
  const archiveProp = await createData(`${(0, utils_1.docuHash)(archiveUrl)}.json`, JSON.stringify({ blogPosts }, null, 2));
126
126
  addRoute({
127
127
  path: archiveUrl,
@@ -172,10 +172,7 @@ async function pluginContentBlog(context, options) {
172
172
  exact: true,
173
173
  modules: {
174
174
  sidebar: aliasedSource(sidebarProp),
175
- items: items.map((postID) =>
176
- // To tell routes.js this is an import and not a nested object
177
- // to recurse.
178
- ({
175
+ items: items.map((postID) => ({
179
176
  content: {
180
177
  __import: true,
181
178
  path: blogItemsToMetadata[postID].source,
@@ -188,23 +185,37 @@ async function pluginContentBlog(context, options) {
188
185
  },
189
186
  });
190
187
  }));
191
- // Tags.
192
- if (blogTagsListPath === null) {
188
+ // Tags. This is the last part so we early-return if there are no tags.
189
+ if (Object.keys(blogTags).length === 0) {
193
190
  return;
194
191
  }
195
- const tagsModule = Object.fromEntries(Object.entries(blogTags).map(([, tag]) => {
196
- const tagModule = {
197
- allTagsPath: blogTagsListPath,
198
- name: tag.name,
199
- count: tag.items.length,
192
+ async function createTagsListPage() {
193
+ const tagsProp = Object.values(blogTags).map((tag) => ({
194
+ label: tag.label,
200
195
  permalink: tag.permalink,
201
- };
202
- return [tag.name, tagModule];
203
- }));
204
- async function createTagRoutes(tag) {
196
+ count: tag.items.length,
197
+ }));
198
+ const tagsPropPath = await createData(`${(0, utils_1.docuHash)(`${blogTagsListPath}-tags`)}.json`, JSON.stringify(tagsProp, null, 2));
199
+ addRoute({
200
+ path: blogTagsListPath,
201
+ component: blogTagsListComponent,
202
+ exact: true,
203
+ modules: {
204
+ sidebar: aliasedSource(sidebarProp),
205
+ tags: aliasedSource(tagsPropPath),
206
+ },
207
+ });
208
+ }
209
+ async function createTagPostsListPage(tag) {
205
210
  await Promise.all(tag.pages.map(async (blogPaginated) => {
206
211
  const { metadata, items } = blogPaginated;
207
- const tagsMetadataPath = await createData(`${(0, utils_1.docuHash)(metadata.permalink)}.json`, JSON.stringify(tagsModule[tag.name], null, 2));
212
+ const tagProp = {
213
+ label: tag.label,
214
+ permalink: tag.permalink,
215
+ allTagsPath: blogTagsListPath,
216
+ count: tag.items.length,
217
+ };
218
+ const tagPropPath = await createData(`${(0, utils_1.docuHash)(metadata.permalink)}.json`, JSON.stringify(tagProp, null, 2));
208
219
  const listMetadataPath = await createData(`${(0, utils_1.docuHash)(metadata.permalink)}-list.json`, JSON.stringify(metadata, null, 2));
209
220
  addRoute({
210
221
  path: metadata.permalink,
@@ -224,26 +235,14 @@ async function pluginContentBlog(context, options) {
224
235
  },
225
236
  };
226
237
  }),
227
- metadata: aliasedSource(tagsMetadataPath),
238
+ tag: aliasedSource(tagPropPath),
228
239
  listMetadata: aliasedSource(listMetadataPath),
229
240
  },
230
241
  });
231
242
  }));
232
243
  }
233
- await Promise.all(Object.values(blogTags).map(createTagRoutes));
234
- // Only create /tags page if there are tags.
235
- if (Object.keys(blogTags).length > 0) {
236
- const tagsListPath = await createData(`${(0, utils_1.docuHash)(`${blogTagsListPath}-tags`)}.json`, JSON.stringify(tagsModule, null, 2));
237
- addRoute({
238
- path: blogTagsListPath,
239
- component: blogTagsListComponent,
240
- exact: true,
241
- modules: {
242
- sidebar: aliasedSource(sidebarProp),
243
- tags: aliasedSource(tagsListPath),
244
- },
245
- });
246
- }
244
+ await createTagsListPage();
245
+ await Promise.all(Object.values(blogTags).map(createTagPostsListPage));
247
246
  },
248
247
  translateContent({ content, translationFiles }) {
249
248
  return (0, translations_1.translateContent)(content, translationFiles);
@@ -283,7 +282,10 @@ async function pluginContentBlog(context, options) {
283
282
  options: {
284
283
  remarkPlugins,
285
284
  rehypePlugins,
286
- beforeDefaultRemarkPlugins,
285
+ beforeDefaultRemarkPlugins: [
286
+ footnoteIDFixer_1.default,
287
+ ...beforeDefaultRemarkPlugins,
288
+ ],
287
289
  beforeDefaultRehypePlugins,
288
290
  staticDirs: siteConfig.staticDirectories.map((dir) => path_1.default.resolve(siteDir, dir)),
289
291
  siteDir,
package/lib/options.js CHANGED
@@ -85,7 +85,7 @@ const PluginOptionSchema = utils_validation_1.Joi.object({
85
85
  .default(exports.DEFAULT_OPTIONS.feedOptions.type),
86
86
  title: utils_validation_1.Joi.string().allow(''),
87
87
  description: utils_validation_1.Joi.string().allow(''),
88
- // only add default value when user actually wants a feed (type is not null)
88
+ // Only add default value when user actually wants a feed (type is not null)
89
89
  copyright: utils_validation_1.Joi.when('type', {
90
90
  is: utils_validation_1.Joi.any().valid(null),
91
91
  then: utils_validation_1.Joi.string().optional(),
@@ -0,0 +1,14 @@
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 { Transformer } from 'unified';
8
+ /**
9
+ * In the blog list view, each post will be compiled separately. However, they
10
+ * may use the same footnote IDs. This leads to duplicated DOM IDs and inability
11
+ * to navigate to footnote references. This plugin fixes it by appending a
12
+ * unique hash to each reference/definition.
13
+ */
14
+ export default function plugin(): Transformer;
@@ -0,0 +1,29 @@
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
+ const tslib_1 = require("tslib");
10
+ const unist_util_visit_1 = tslib_1.__importDefault(require("unist-util-visit"));
11
+ const utils_1 = require("@docusaurus/utils");
12
+ /**
13
+ * In the blog list view, each post will be compiled separately. However, they
14
+ * may use the same footnote IDs. This leads to duplicated DOM IDs and inability
15
+ * to navigate to footnote references. This plugin fixes it by appending a
16
+ * unique hash to each reference/definition.
17
+ */
18
+ function plugin() {
19
+ return (root, vfile) => {
20
+ const suffix = `-${(0, utils_1.simpleHash)(vfile.path, 6)}`;
21
+ (0, unist_util_visit_1.default)(root, 'footnoteReference', (node) => {
22
+ node.identifier += suffix;
23
+ });
24
+ (0, unist_util_visit_1.default)(root, 'footnoteDefinition', (node) => {
25
+ node.identifier += suffix;
26
+ });
27
+ };
28
+ }
29
+ exports.default = plugin;
@@ -4,8 +4,7 @@
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 { BlogContent } from './types';
8
- import type { TranslationFiles } from '@docusaurus/types';
9
- import type { PluginOptions } from '@docusaurus/plugin-content-blog';
10
- export declare function getTranslationFiles(options: PluginOptions): TranslationFiles;
11
- export declare function translateContent(content: BlogContent, translationFiles: TranslationFiles): BlogContent;
7
+ import type { TranslationFile } from '@docusaurus/types';
8
+ import type { PluginOptions, BlogContent } from '@docusaurus/plugin-content-blog';
9
+ export declare function getTranslationFiles(options: PluginOptions): TranslationFile[];
10
+ export declare function translateContent(content: BlogContent, translationFiles: TranslationFile[]): BlogContent;
package/lib/types.d.ts CHANGED
@@ -5,34 +5,7 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
  import type { BrokenMarkdownLink, ContentPaths } from '@docusaurus/utils';
8
- import type { BlogPostMetadata } from '@docusaurus/plugin-content-blog';
9
- import type { Metadata as BlogPaginatedMetadata } from '@theme/BlogListPage';
10
8
  export declare type BlogContentPaths = ContentPaths;
11
- export interface BlogContent {
12
- blogSidebarTitle: string;
13
- blogPosts: BlogPost[];
14
- blogListPaginated: BlogPaginated[];
15
- blogTags: BlogTags;
16
- blogTagsListPath: string | null;
17
- }
18
- export interface BlogTags {
19
- [tagKey: string]: BlogTag;
20
- }
21
- export interface BlogTag {
22
- name: string;
23
- items: string[];
24
- permalink: string;
25
- pages: BlogPaginated[];
26
- }
27
- export interface BlogPost {
28
- id: string;
29
- metadata: BlogPostMetadata;
30
- content: string;
31
- }
32
- export interface BlogPaginated {
33
- metadata: BlogPaginatedMetadata;
34
- items: string[];
35
- }
36
9
  export declare type BlogBrokenMarkdownLink = BrokenMarkdownLink<BlogContentPaths>;
37
10
  export declare type BlogMarkdownLoaderOptions = {
38
11
  siteDir: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docusaurus/plugin-content-blog",
3
- "version": "2.0.0-beta.18",
3
+ "version": "2.0.0-beta.19",
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,25 @@
18
18
  },
19
19
  "license": "MIT",
20
20
  "dependencies": {
21
- "@docusaurus/core": "2.0.0-beta.18",
22
- "@docusaurus/logger": "2.0.0-beta.18",
23
- "@docusaurus/mdx-loader": "2.0.0-beta.18",
24
- "@docusaurus/utils": "2.0.0-beta.18",
25
- "@docusaurus/utils-common": "2.0.0-beta.18",
26
- "@docusaurus/utils-validation": "2.0.0-beta.18",
21
+ "@docusaurus/core": "2.0.0-beta.19",
22
+ "@docusaurus/logger": "2.0.0-beta.19",
23
+ "@docusaurus/mdx-loader": "2.0.0-beta.19",
24
+ "@docusaurus/utils": "2.0.0-beta.19",
25
+ "@docusaurus/utils-common": "2.0.0-beta.19",
26
+ "@docusaurus/utils-validation": "2.0.0-beta.19",
27
27
  "cheerio": "^1.0.0-rc.10",
28
28
  "feed": "^4.2.2",
29
- "fs-extra": "^10.0.1",
29
+ "fs-extra": "^10.1.0",
30
30
  "lodash": "^4.17.21",
31
31
  "reading-time": "^1.5.0",
32
32
  "remark-admonitions": "^1.2.1",
33
- "tslib": "^2.3.1",
33
+ "tslib": "^2.4.0",
34
+ "unist-util-visit": "^2.0.3",
34
35
  "utility-types": "^3.10.0",
35
- "webpack": "^5.70.0"
36
+ "webpack": "^5.72.0"
36
37
  },
37
38
  "devDependencies": {
38
- "@docusaurus/types": "2.0.0-beta.18",
39
+ "@docusaurus/types": "2.0.0-beta.19",
39
40
  "escape-string-regexp": "^4.0.0"
40
41
  },
41
42
  "peerDependencies": {
@@ -45,5 +46,5 @@
45
46
  "engines": {
46
47
  "node": ">=14"
47
48
  },
48
- "gitHead": "1a945d06993d53376e61bed2c942799fe07dc336"
49
+ "gitHead": "a71e60a49cce93c1006ef10c41ac03187f057102"
49
50
  }
package/src/blogUtils.ts CHANGED
@@ -9,13 +9,7 @@ import fs from 'fs-extra';
9
9
  import path from 'path';
10
10
  import readingTime from 'reading-time';
11
11
  import _ from 'lodash';
12
- import type {
13
- BlogPost,
14
- BlogContentPaths,
15
- BlogMarkdownLoaderOptions,
16
- BlogTags,
17
- BlogPaginated,
18
- } from './types';
12
+ import type {BlogContentPaths, BlogMarkdownLoaderOptions} from './types';
19
13
  import {
20
14
  parseMarkdownString,
21
15
  normalizeUrl,
@@ -37,6 +31,9 @@ import logger from '@docusaurus/logger';
37
31
  import type {
38
32
  PluginOptions,
39
33
  ReadingTimeFunction,
34
+ BlogPost,
35
+ BlogTags,
36
+ BlogPaginated,
40
37
  } from '@docusaurus/plugin-content-blog';
41
38
 
42
39
  export function truncate(fileString: string, truncateMarker: RegExp): string {
@@ -114,7 +111,7 @@ export function getBlogTags({
114
111
  );
115
112
 
116
113
  return _.mapValues(groups, ({tag, items: tagBlogPosts}) => ({
117
- name: tag.label,
114
+ label: tag.label,
118
115
  items: tagBlogPosts.map((item) => item.id),
119
116
  permalink: tag.permalink,
120
117
  pages: paginateBlogPosts({
@@ -151,13 +148,18 @@ export function parseBlogFileName(
151
148
  return {date: undefined, text, slug};
152
149
  }
153
150
 
154
- function formatBlogPostDate(locale: string, date: Date): string {
151
+ function formatBlogPostDate(
152
+ locale: string,
153
+ date: Date,
154
+ calendar: string,
155
+ ): string {
155
156
  try {
156
157
  return new Intl.DateTimeFormat(locale, {
157
158
  day: 'numeric',
158
159
  month: 'long',
159
160
  year: 'numeric',
160
161
  timeZone: 'UTC',
162
+ calendar,
161
163
  }).format(date);
162
164
  } catch (err) {
163
165
  logger.error`Can't format blog post date "${String(date)}"`;
@@ -253,7 +255,11 @@ async function processBlogSourceFile(
253
255
  }
254
256
 
255
257
  const date = await getDate();
256
- const formattedDate = formatBlogPostDate(i18n.currentLocale, date);
258
+ const formattedDate = formatBlogPostDate(
259
+ i18n.currentLocale,
260
+ date,
261
+ i18n.localeConfigs[i18n.currentLocale]!.calendar,
262
+ );
257
263
 
258
264
  const title = frontMatter.title ?? contentTitle ?? parsedBlogFileName.text;
259
265
  const description = frontMatter.description ?? excerpt ?? '';
package/src/feed.ts CHANGED
@@ -6,13 +6,7 @@
6
6
  */
7
7
 
8
8
  import {Feed, type Author as FeedAuthor, type Item as FeedItem} from 'feed';
9
- import type {BlogPost} from './types';
10
- import {
11
- normalizeUrl,
12
- posixPath,
13
- mapAsyncSequential,
14
- readOutputHTMLFile,
15
- } from '@docusaurus/utils';
9
+ import {normalizeUrl, readOutputHTMLFile} from '@docusaurus/utils';
16
10
  import {load as cheerioLoad} from 'cheerio';
17
11
  import type {DocusaurusConfig} from '@docusaurus/types';
18
12
  import path from 'path';
@@ -21,6 +15,7 @@ import type {
21
15
  FeedType,
22
16
  PluginOptions,
23
17
  Author,
18
+ BlogPost,
24
19
  } from '@docusaurus/plugin-content-blog';
25
20
  import {blogPostContainerID} from '@docusaurus/utils-common';
26
21
 
@@ -62,46 +57,48 @@ async function generateBlogFeed({
62
57
  return {name: author.name, link: author.url, email: author.email};
63
58
  }
64
59
 
65
- await mapAsyncSequential(blogPosts, async (post) => {
66
- const {
67
- id,
68
- metadata: {
60
+ await Promise.all(
61
+ blogPosts.map(async (post) => {
62
+ const {
63
+ id,
64
+ metadata: {
65
+ title: metadataTitle,
66
+ permalink,
67
+ date,
68
+ description,
69
+ authors,
70
+ tags,
71
+ },
72
+ } = post;
73
+
74
+ const content = await readOutputHTMLFile(
75
+ permalink.replace(siteConfig.baseUrl, ''),
76
+ outDir,
77
+ siteConfig.trailingSlash,
78
+ );
79
+ const $ = cheerioLoad(content);
80
+
81
+ const feedItem: FeedItem = {
69
82
  title: metadataTitle,
70
- permalink,
83
+ id,
84
+ link: normalizeUrl([siteUrl, permalink]),
71
85
  date,
72
86
  description,
73
- authors,
74
- tags,
75
- },
76
- } = post;
77
-
78
- const content = await readOutputHTMLFile(
79
- permalink.replace(siteConfig.baseUrl, ''),
80
- outDir,
81
- siteConfig.trailingSlash,
82
- );
83
- const $ = cheerioLoad(content);
84
-
85
- const feedItem: FeedItem = {
86
- title: metadataTitle,
87
- id,
88
- link: normalizeUrl([siteUrl, permalink]),
89
- date,
90
- description,
91
- // Atom feed demands the "term", while other feeds use "name"
92
- category: tags.map((tag) => ({name: tag.label, term: tag.label})),
93
- content: $(`#${blogPostContainerID}`).html()!,
94
- };
95
-
96
- // json1() method takes the first item of authors array
97
- // it causes an error when authors array is empty
98
- const feedItemAuthors = authors.map(toFeedAuthor);
99
- if (feedItemAuthors.length > 0) {
100
- feedItem.author = feedItemAuthors;
101
- }
102
-
103
- feed.addItem(feedItem);
104
- });
87
+ // Atom feed demands the "term", while other feeds use "name"
88
+ category: tags.map((tag) => ({name: tag.label, term: tag.label})),
89
+ content: $(`#${blogPostContainerID}`).html()!,
90
+ };
91
+
92
+ // json1() method takes the first item of authors array
93
+ // it causes an error when authors array is empty
94
+ const feedItemAuthors = authors.map(toFeedAuthor);
95
+ if (feedItemAuthors.length > 0) {
96
+ feedItem.author = feedItemAuthors;
97
+ }
98
+
99
+ return feedItem;
100
+ }),
101
+ ).then((items) => items.forEach(feed.addItem));
105
102
 
106
103
  return feed;
107
104
  }
@@ -128,10 +125,7 @@ async function createBlogFeedFile({
128
125
  }
129
126
  })();
130
127
  try {
131
- await fs.outputFile(
132
- posixPath(path.join(generatePath, feedPath)),
133
- feedContent,
134
- );
128
+ await fs.outputFile(path.join(generatePath, feedPath), feedContent);
135
129
  } catch (err) {
136
130
  throw new Error(`Generating ${feedType} feed failed: ${err}.`);
137
131
  }
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@
7
7
 
8
8
  import path from 'path';
9
9
  import admonitions from 'remark-admonitions';
10
+ import footnoteIDFixer from './remark/footnoteIDFixer';
10
11
  import {
11
12
  normalizeUrl,
12
13
  docuHash,
@@ -19,17 +20,12 @@ import {
19
20
  getContentPathList,
20
21
  getDataFilePath,
21
22
  DEFAULT_PLUGIN_ID,
23
+ type TagsListItem,
24
+ type TagModule,
22
25
  } from '@docusaurus/utils';
23
26
  import {translateContent, getTranslationFiles} from './translations';
24
27
 
25
- import type {
26
- BlogTag,
27
- BlogTags,
28
- BlogContent,
29
- BlogPaginated,
30
- BlogContentPaths,
31
- BlogMarkdownLoaderOptions,
32
- } from './types';
28
+ import type {BlogContentPaths, BlogMarkdownLoaderOptions} from './types';
33
29
  import type {LoadContext, Plugin, HtmlTags} from '@docusaurus/types';
34
30
  import {
35
31
  generateBlogPosts,
@@ -43,7 +39,10 @@ import type {
43
39
  BlogPostFrontMatter,
44
40
  BlogPostMetadata,
45
41
  Assets,
46
- TagModule,
42
+ BlogTag,
43
+ BlogTags,
44
+ BlogContent,
45
+ BlogPaginated,
47
46
  } from '@docusaurus/plugin-content-blog';
48
47
 
49
48
  export default async function pluginContentBlog(
@@ -102,7 +101,7 @@ export default async function pluginContentBlog(
102
101
  ) as string[];
103
102
  },
104
103
 
105
- async getTranslationFiles() {
104
+ getTranslationFiles() {
106
105
  return getTranslationFiles(options);
107
106
  },
108
107
 
@@ -117,6 +116,8 @@ export default async function pluginContentBlog(
117
116
  blogSidebarTitle,
118
117
  } = options;
119
118
 
119
+ const baseBlogUrl = normalizeUrl([baseUrl, routeBasePath]);
120
+ const blogTagsListPath = normalizeUrl([baseBlogUrl, tagsBasePath]);
120
121
  const blogPosts = await generateBlogPosts(contentPaths, context, options);
121
122
 
122
123
  if (!blogPosts.length) {
@@ -125,7 +126,7 @@ export default async function pluginContentBlog(
125
126
  blogPosts: [],
126
127
  blogListPaginated: [],
127
128
  blogTags: {},
128
- blogTagsListPath: null,
129
+ blogTagsListPath,
129
130
  blogTagsPaginated: [],
130
131
  };
131
132
  }
@@ -150,8 +151,6 @@ export default async function pluginContentBlog(
150
151
  }
151
152
  });
152
153
 
153
- const baseBlogUrl = normalizeUrl([baseUrl, routeBasePath]);
154
-
155
154
  const blogListPaginated: BlogPaginated[] = paginateBlogPosts({
156
155
  blogPosts,
157
156
  blogTitle,
@@ -167,11 +166,6 @@ export default async function pluginContentBlog(
167
166
  blogTitle,
168
167
  });
169
168
 
170
- const tagsPath = normalizeUrl([baseBlogUrl, tagsBasePath]);
171
-
172
- const blogTagsListPath =
173
- Object.keys(blogTags).length > 0 ? tagsPath : null;
174
-
175
169
  return {
176
170
  blogSidebarTitle,
177
171
  blogPosts,
@@ -218,7 +212,7 @@ export default async function pluginContentBlog(
218
212
  routeBasePath,
219
213
  archiveBasePath,
220
214
  ]);
221
- // creates a blog archive route
215
+ // Create a blog archive route
222
216
  const archiveProp = await createData(
223
217
  `${docuHash(archiveUrl)}.json`,
224
218
  JSON.stringify({blogPosts}, null, 2),
@@ -292,49 +286,62 @@ export default async function pluginContentBlog(
292
286
  exact: true,
293
287
  modules: {
294
288
  sidebar: aliasedSource(sidebarProp),
295
- items: items.map((postID) =>
296
- // To tell routes.js this is an import and not a nested object
297
- // to recurse.
298
- ({
299
- content: {
300
- __import: true,
301
- path: blogItemsToMetadata[postID]!.source,
302
- query: {
303
- truncated: true,
304
- },
289
+ items: items.map((postID) => ({
290
+ content: {
291
+ __import: true,
292
+ path: blogItemsToMetadata[postID]!.source,
293
+ query: {
294
+ truncated: true,
305
295
  },
306
- }),
307
- ),
296
+ },
297
+ })),
308
298
  metadata: aliasedSource(pageMetadataPath),
309
299
  },
310
300
  });
311
301
  }),
312
302
  );
313
303
 
314
- // Tags.
315
- if (blogTagsListPath === null) {
304
+ // Tags. This is the last part so we early-return if there are no tags.
305
+ if (Object.keys(blogTags).length === 0) {
316
306
  return;
317
307
  }
318
308
 
319
- const tagsModule: {[tagName: string]: TagModule} = Object.fromEntries(
320
- Object.entries(blogTags).map(([, tag]) => {
321
- const tagModule: TagModule = {
322
- allTagsPath: blogTagsListPath,
323
- name: tag.name,
324
- count: tag.items.length,
325
- permalink: tag.permalink,
326
- };
327
- return [tag.name, tagModule];
328
- }),
329
- );
309
+ async function createTagsListPage() {
310
+ const tagsProp: TagsListItem[] = Object.values(blogTags).map((tag) => ({
311
+ label: tag.label,
312
+ permalink: tag.permalink,
313
+ count: tag.items.length,
314
+ }));
315
+
316
+ const tagsPropPath = await createData(
317
+ `${docuHash(`${blogTagsListPath}-tags`)}.json`,
318
+ JSON.stringify(tagsProp, null, 2),
319
+ );
330
320
 
331
- async function createTagRoutes(tag: BlogTag): Promise<void> {
321
+ addRoute({
322
+ path: blogTagsListPath,
323
+ component: blogTagsListComponent,
324
+ exact: true,
325
+ modules: {
326
+ sidebar: aliasedSource(sidebarProp),
327
+ tags: aliasedSource(tagsPropPath),
328
+ },
329
+ });
330
+ }
331
+
332
+ async function createTagPostsListPage(tag: BlogTag): Promise<void> {
332
333
  await Promise.all(
333
334
  tag.pages.map(async (blogPaginated) => {
334
335
  const {metadata, items} = blogPaginated;
335
- const tagsMetadataPath = await createData(
336
+ const tagProp: TagModule = {
337
+ label: tag.label,
338
+ permalink: tag.permalink,
339
+ allTagsPath: blogTagsListPath,
340
+ count: tag.items.length,
341
+ };
342
+ const tagPropPath = await createData(
336
343
  `${docuHash(metadata.permalink)}.json`,
337
- JSON.stringify(tagsModule[tag.name], null, 2),
344
+ JSON.stringify(tagProp, null, 2),
338
345
  );
339
346
 
340
347
  const listMetadataPath = await createData(
@@ -360,7 +367,7 @@ export default async function pluginContentBlog(
360
367
  },
361
368
  };
362
369
  }),
363
- metadata: aliasedSource(tagsMetadataPath),
370
+ tag: aliasedSource(tagPropPath),
364
371
  listMetadata: aliasedSource(listMetadataPath),
365
372
  },
366
373
  });
@@ -368,25 +375,8 @@ export default async function pluginContentBlog(
368
375
  );
369
376
  }
370
377
 
371
- await Promise.all(Object.values(blogTags).map(createTagRoutes));
372
-
373
- // Only create /tags page if there are tags.
374
- if (Object.keys(blogTags).length > 0) {
375
- const tagsListPath = await createData(
376
- `${docuHash(`${blogTagsListPath}-tags`)}.json`,
377
- JSON.stringify(tagsModule, null, 2),
378
- );
379
-
380
- addRoute({
381
- path: blogTagsListPath,
382
- component: blogTagsListComponent,
383
- exact: true,
384
- modules: {
385
- sidebar: aliasedSource(sidebarProp),
386
- tags: aliasedSource(tagsListPath),
387
- },
388
- });
389
- }
378
+ await createTagsListPage();
379
+ await Promise.all(Object.values(blogTags).map(createTagPostsListPage));
390
380
  },
391
381
 
392
382
  translateContent({content, translationFiles}) {
@@ -439,7 +429,10 @@ export default async function pluginContentBlog(
439
429
  options: {
440
430
  remarkPlugins,
441
431
  rehypePlugins,
442
- beforeDefaultRemarkPlugins,
432
+ beforeDefaultRemarkPlugins: [
433
+ footnoteIDFixer,
434
+ ...beforeDefaultRemarkPlugins,
435
+ ],
443
436
  beforeDefaultRehypePlugins,
444
437
  staticDirs: siteConfig.staticDirectories.map((dir) =>
445
438
  path.resolve(siteDir, dir),
package/src/options.ts CHANGED
@@ -111,7 +111,7 @@ const PluginOptionSchema = Joi.object<PluginOptions>({
111
111
  .default(DEFAULT_OPTIONS.feedOptions.type),
112
112
  title: Joi.string().allow(''),
113
113
  description: Joi.string().allow(''),
114
- // only add default value when user actually wants a feed (type is not null)
114
+ // Only add default value when user actually wants a feed (type is not null)
115
115
  copyright: Joi.when('type', {
116
116
  is: Joi.any().valid(null),
117
117
  then: Joi.string().optional(),
@@ -6,11 +6,12 @@
6
6
  */
7
7
 
8
8
  declare module '@docusaurus/plugin-content-blog' {
9
- import type {RemarkAndRehypePluginOptions} from '@docusaurus/mdx-loader';
10
- import type {FrontMatterTag} from '@docusaurus/utils';
9
+ import type {MDXOptions} from '@docusaurus/mdx-loader';
10
+ import type {FrontMatterTag, Tag} from '@docusaurus/utils';
11
+ import type {Plugin, LoadContext} from '@docusaurus/types';
11
12
  import type {Overwrite} from 'utility-types';
12
13
 
13
- export interface Assets {
14
+ export type Assets = {
14
15
  /**
15
16
  * If `metadata.image` is a collocated image path, this entry will be the
16
17
  * bundler-generated image path. Otherwise, it's empty, and the image URL
@@ -25,13 +26,9 @@ declare module '@docusaurus/plugin-content-blog' {
25
26
  * should be accessed through `authors.imageURL`.
26
27
  */
27
28
  authorsImageUrls: (string | undefined)[];
28
- }
29
+ };
29
30
 
30
- /**
31
- * Unknown keys are allowed, so that we can pass custom fields to authors,
32
- * e.g., `twitter`.
33
- */
34
- export interface Author extends Record<string, unknown> {
31
+ export type Author = {
35
32
  /**
36
33
  * If `name` doesn't exist, an `imageURL` is expected.
37
34
  */
@@ -55,7 +52,12 @@ declare module '@docusaurus/plugin-content-blog' {
55
52
  * to generate a fallback `mailto:` URL.
56
53
  */
57
54
  email?: string;
58
- }
55
+ /**
56
+ * Unknown keys are allowed, so that we can pass custom fields to authors,
57
+ * e.g., `twitter`.
58
+ */
59
+ [key: string]: unknown;
60
+ };
59
61
 
60
62
  /**
61
63
  * Everything is partial/unnormalized, because front matter is always
@@ -81,10 +83,7 @@ declare module '@docusaurus/plugin-content-blog' {
81
83
  * @see {@link BlogPostMetadata.tags}
82
84
  */
83
85
  tags?: FrontMatterTag[];
84
- /**
85
- * Custom slug appended after /<baseUrl>/<routeBasePath>/
86
- * @see {@link BlogPostMetadata.slug}
87
- */
86
+ /** Custom slug appended after `/<baseUrl>/<routeBasePath>/` */
88
87
  slug?: string;
89
88
  /**
90
89
  * Marks the post as draft and excludes it from the production build.
@@ -130,25 +129,18 @@ declare module '@docusaurus/plugin-content-blog' {
130
129
  /** @deprecated v1 legacy */
131
130
  authorImageURL?: string;
132
131
 
133
- /**
134
- * @see {@link BlogPostMetadata.image}
135
- */
132
+ /** Used in the head meta. Should use `assets.image` in priority. */
136
133
  image?: string;
137
- /**
138
- * Used in the head meta
139
- */
134
+ /** Used in the head meta. */
140
135
  keywords?: string[];
141
- /**
142
- * Hide the right TOC
143
- */
136
+ /** Hide the right TOC. */
144
137
  hide_table_of_contents?: boolean;
145
138
  /**
146
- * Minimum TOC heading level
139
+ * Minimum TOC heading level. Must be between 2 and 6 and lower or equal to
140
+ * the max value.
147
141
  */
148
142
  toc_min_heading_level?: number;
149
- /**
150
- * Maximum TOC heading level
151
- */
143
+ /** Maximum TOC heading level. Must be between 2 and 6. */
152
144
  toc_max_heading_level?: number;
153
145
  };
154
146
 
@@ -175,9 +167,7 @@ declare module '@docusaurus/plugin-content-blog' {
175
167
  | (string | BlogPostFrontMatterAuthor)[];
176
168
 
177
169
  export type BlogPostMetadata = {
178
- /**
179
- * Path to the Markdown source, with `@site` alias.
180
- */
170
+ /** Path to the Markdown source, with `@site` alias. */
181
171
  readonly source: string;
182
172
  /**
183
173
  * Used to generate the page h1 heading, tab title, and pagination title.
@@ -193,9 +183,7 @@ declare module '@docusaurus/plugin-content-blog' {
193
183
  * render the date regardless of the existence of `Intl.DateTimeFormat`.
194
184
  */
195
185
  readonly formattedDate: string;
196
- /**
197
- * Full link including base URL.
198
- */
186
+ /** Full link including base URL. */
199
187
  readonly permalink: string;
200
188
  /**
201
189
  * Description used in the meta. Could be an empty string (empty content)
@@ -229,17 +217,10 @@ declare module '@docusaurus/plugin-content-blog' {
229
217
  * `assets.authorsImageUrls` on client side.
230
218
  */
231
219
  readonly authors: Author[];
232
- /**
233
- * Front matter, as-is.
234
- */
220
+ /** Front matter, as-is. */
235
221
  readonly frontMatter: BlogPostFrontMatter & {[key: string]: unknown};
236
- /**
237
- * Tags, normalized.
238
- */
239
- readonly tags: readonly {
240
- readonly label: string;
241
- readonly permalink: string;
242
- }[];
222
+ /** Tags, normalized. */
223
+ readonly tags: Tag[];
243
224
  };
244
225
  /**
245
226
  * @returns The edit URL that's directly plugged into metadata.
@@ -250,17 +231,11 @@ declare module '@docusaurus/plugin-content-blog' {
250
231
  * site path. Usually the same as `options.path` but can be localized
251
232
  */
252
233
  blogDirPath: string;
253
- /**
254
- * Path to this post file, relative to `blogDirPath`
255
- */
234
+ /** Path to this post file, relative to `blogDirPath`. */
256
235
  blogPath: string;
257
- /**
258
- * @see {@link BlogPostMetadata.permalink}
259
- */
236
+ /** @see {@link BlogPostMetadata.permalink} */
260
237
  permalink: string;
261
- /**
262
- * Locale name.
263
- */
238
+ /** Locale name. */
264
239
  locale: string;
265
240
  }) => string | undefined;
266
241
 
@@ -325,7 +300,7 @@ declare module '@docusaurus/plugin-content-blog' {
325
300
  /**
326
301
  * Plugin options after normalization.
327
302
  */
328
- export type PluginOptions = RemarkAndRehypePluginOptions & {
303
+ export type PluginOptions = MDXOptions & {
329
304
  /** Plugin ID. */
330
305
  id?: string;
331
306
  /**
@@ -432,25 +407,70 @@ declare module '@docusaurus/plugin-content-blog' {
432
407
  }
433
408
  >;
434
409
 
435
- export type TagModule = {
436
- /** Permalink of the tag's own page. */
437
- permalink: string;
438
- /** Name of the tag. */
439
- name: string;
440
- /** Number of posts with this tag. */
441
- count: number;
442
- /** The tags list page. */
443
- allTagsPath: string;
444
- };
445
-
446
410
  export type BlogSidebar = {
447
411
  title: string;
448
412
  items: {title: string; permalink: string}[];
449
413
  };
414
+
415
+ export type BlogContent = {
416
+ blogSidebarTitle: string;
417
+ blogPosts: BlogPost[];
418
+ blogListPaginated: BlogPaginated[];
419
+ blogTags: BlogTags;
420
+ blogTagsListPath: string;
421
+ };
422
+
423
+ export type BlogTags = {
424
+ [permalink: string]: BlogTag;
425
+ };
426
+
427
+ export type BlogTag = Tag & {
428
+ /** Blog post permalinks. */
429
+ items: string[];
430
+ pages: BlogPaginated[];
431
+ };
432
+
433
+ export type BlogPost = {
434
+ id: string;
435
+ metadata: BlogPostMetadata;
436
+ content: string;
437
+ };
438
+
439
+ export type BlogPaginatedMetadata = {
440
+ /** Title of the entire blog. */
441
+ readonly blogTitle: string;
442
+ /** Blog description. */
443
+ readonly blogDescription: string;
444
+ /** Permalink to the next list page. */
445
+ readonly nextPage?: string;
446
+ /** Permalink of the current page. */
447
+ readonly permalink: string;
448
+ /** Permalink to the previous list page. */
449
+ readonly previousPage?: string;
450
+ /** Index of the current page, 1-based. */
451
+ readonly page: number;
452
+ /** Posts displayed on each list page. */
453
+ readonly postsPerPage: number;
454
+ /** Total number of posts in the entire blog. */
455
+ readonly totalCount: number;
456
+ /** Total number of list pages. */
457
+ readonly totalPages: number;
458
+ };
459
+
460
+ export type BlogPaginated = {
461
+ metadata: BlogPaginatedMetadata;
462
+ /** Blog post permalinks. */
463
+ items: string[];
464
+ };
465
+
466
+ export default function pluginContentBlog(
467
+ context: LoadContext,
468
+ options: PluginOptions,
469
+ ): Promise<Plugin<BlogContent>>;
450
470
  }
451
471
 
452
472
  declare module '@theme/BlogPostPage' {
453
- import type {TOCItem} from '@docusaurus/types';
473
+ import type {LoadedMDXContent} from '@docusaurus/mdx-loader';
454
474
  import type {
455
475
  BlogPostFrontMatter,
456
476
  BlogPostMetadata,
@@ -469,24 +489,7 @@ declare module '@theme/BlogPostPage' {
469
489
  }
470
490
  >;
471
491
 
472
- export type Content = {
473
- // TODO remove this. `metadata.frontMatter` is preferred because it can be
474
- // accessed in enhanced plugins
475
- /** Same as `metadata.frontMatter` */
476
- readonly frontMatter: FrontMatter;
477
- /**
478
- * Usually image assets that may be collocated like `./img/thumbnail.png`.
479
- * The loader would also bundle these assets and the client should use these
480
- * in priority.
481
- */
482
- readonly assets: Assets;
483
- /** Metadata of the post. */
484
- readonly metadata: Metadata;
485
- /** A list of TOC items (headings). */
486
- readonly toc: readonly TOCItem[];
487
- /** Renders the actual MDX content. */
488
- (): JSX.Element;
489
- };
492
+ export type Content = LoadedMDXContent<FrontMatter, Metadata, Assets>;
490
493
 
491
494
  export interface Props {
492
495
  /** Blog sidebar. */
@@ -500,34 +503,16 @@ declare module '@theme/BlogPostPage' {
500
503
 
501
504
  declare module '@theme/BlogListPage' {
502
505
  import type {Content} from '@theme/BlogPostPage';
503
- import type {BlogSidebar} from '@docusaurus/plugin-content-blog';
504
-
505
- export type Metadata = {
506
- /** Title of the entire blog. */
507
- readonly blogTitle: string;
508
- /** Blog description. */
509
- readonly blogDescription: string;
510
- /** Permalink to the next list page. */
511
- readonly nextPage?: string;
512
- /** Permalink of the current page. */
513
- readonly permalink: string;
514
- /** Permalink to the previous list page. */
515
- readonly previousPage?: string;
516
- /** Index of the current page, 1-based. */
517
- readonly page: number;
518
- /** Posts displayed on each list page. */
519
- readonly postsPerPage: number;
520
- /** Total number of posts in the entire blog. */
521
- readonly totalCount: number;
522
- /** Total number of list pages. */
523
- readonly totalPages: number;
524
- };
506
+ import type {
507
+ BlogSidebar,
508
+ BlogPaginatedMetadata,
509
+ } from '@docusaurus/plugin-content-blog';
525
510
 
526
511
  export interface Props {
527
512
  /** Blog sidebar. */
528
513
  readonly sidebar: BlogSidebar;
529
514
  /** Metadata of the current listing page. */
530
- readonly metadata: Metadata;
515
+ readonly metadata: BlogPaginatedMetadata;
531
516
  /**
532
517
  * Array of blog posts included on this page. Every post's metadata is also
533
518
  * available.
@@ -539,30 +524,34 @@ declare module '@theme/BlogListPage' {
539
524
  }
540
525
 
541
526
  declare module '@theme/BlogTagsListPage' {
542
- import type {BlogSidebar, TagModule} from '@docusaurus/plugin-content-blog';
527
+ import type {BlogSidebar} from '@docusaurus/plugin-content-blog';
528
+ import type {TagsListItem} from '@docusaurus/utils';
543
529
 
544
530
  export interface Props {
545
531
  /** Blog sidebar. */
546
532
  readonly sidebar: BlogSidebar;
547
- /** A map from tag names to the full tag module. */
548
- readonly tags: Readonly<{[tagName: string]: TagModule}>;
533
+ /** All tags declared in this blog. */
534
+ readonly tags: TagsListItem[];
549
535
  }
550
536
 
551
537
  export default function BlogTagsListPage(props: Props): JSX.Element;
552
538
  }
553
539
 
554
540
  declare module '@theme/BlogTagsPostsPage' {
555
- import type {BlogSidebar, TagModule} from '@docusaurus/plugin-content-blog';
541
+ import type {
542
+ BlogSidebar,
543
+ BlogPaginatedMetadata,
544
+ } from '@docusaurus/plugin-content-blog';
556
545
  import type {Content} from '@theme/BlogPostPage';
557
- import type {Metadata} from '@theme/BlogListPage';
546
+ import type {TagModule} from '@docusaurus/utils';
558
547
 
559
548
  export interface Props {
560
549
  /** Blog sidebar. */
561
550
  readonly sidebar: BlogSidebar;
562
551
  /** Metadata of this tag. */
563
- readonly metadata: TagModule;
552
+ readonly tag: TagModule;
564
553
  /** Looks exactly the same as the posts list page */
565
- readonly listMetadata: Metadata;
554
+ readonly listMetadata: BlogPaginatedMetadata;
566
555
  /**
567
556
  * Array of blog posts included on this page. Every post's metadata is also
568
557
  * available.
@@ -0,0 +1,29 @@
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 visit from 'unist-util-visit';
9
+ import {simpleHash} from '@docusaurus/utils';
10
+ import type {Transformer} from 'unified';
11
+ import type {FootnoteReference, FootnoteDefinition} from 'mdast';
12
+
13
+ /**
14
+ * In the blog list view, each post will be compiled separately. However, they
15
+ * may use the same footnote IDs. This leads to duplicated DOM IDs and inability
16
+ * to navigate to footnote references. This plugin fixes it by appending a
17
+ * unique hash to each reference/definition.
18
+ */
19
+ export default function plugin(): Transformer {
20
+ return (root, vfile) => {
21
+ const suffix = `-${simpleHash(vfile.path!, 6)}`;
22
+ visit(root, 'footnoteReference', (node: FootnoteReference) => {
23
+ node.identifier += suffix;
24
+ });
25
+ visit(root, 'footnoteDefinition', (node: FootnoteDefinition) => {
26
+ node.identifier += suffix;
27
+ });
28
+ };
29
+ }
@@ -5,9 +5,12 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
 
8
- import type {BlogContent, BlogPaginated} from './types';
9
- import type {TranslationFileContent, TranslationFiles} from '@docusaurus/types';
10
- import type {PluginOptions} from '@docusaurus/plugin-content-blog';
8
+ import type {TranslationFileContent, TranslationFile} from '@docusaurus/types';
9
+ import type {
10
+ PluginOptions,
11
+ BlogContent,
12
+ BlogPaginated,
13
+ } from '@docusaurus/plugin-content-blog';
11
14
 
12
15
  function translateListPage(
13
16
  blogListPaginated: BlogPaginated[],
@@ -27,7 +30,7 @@ function translateListPage(
27
30
  });
28
31
  }
29
32
 
30
- export function getTranslationFiles(options: PluginOptions): TranslationFiles {
33
+ export function getTranslationFiles(options: PluginOptions): TranslationFile[] {
31
34
  return [
32
35
  {
33
36
  path: 'options',
@@ -51,7 +54,7 @@ export function getTranslationFiles(options: PluginOptions): TranslationFiles {
51
54
 
52
55
  export function translateContent(
53
56
  content: BlogContent,
54
- translationFiles: TranslationFiles,
57
+ translationFiles: TranslationFile[],
55
58
  ): BlogContent {
56
59
  const {content: optionsTranslations} = translationFiles[0]!;
57
60
  return {
package/src/types.ts CHANGED
@@ -6,45 +6,9 @@
6
6
  */
7
7
 
8
8
  import type {BrokenMarkdownLink, ContentPaths} from '@docusaurus/utils';
9
- import type {BlogPostMetadata} from '@docusaurus/plugin-content-blog';
10
- import type {Metadata as BlogPaginatedMetadata} from '@theme/BlogListPage';
11
9
 
12
10
  export type BlogContentPaths = ContentPaths;
13
11
 
14
- export interface BlogContent {
15
- blogSidebarTitle: string;
16
- blogPosts: BlogPost[];
17
- blogListPaginated: BlogPaginated[];
18
- blogTags: BlogTags;
19
- blogTagsListPath: string | null;
20
- }
21
-
22
- export interface BlogTags {
23
- // TODO, the key is the tag slug/permalink
24
- // This is due to legacy frontmatter: tags:
25
- // [{label: "xyz", permalink: "/1"}, {label: "xyz", permalink: "/2"}]
26
- // Soon we should forbid declaring permalink through frontmatter
27
- [tagKey: string]: BlogTag;
28
- }
29
-
30
- export interface BlogTag {
31
- name: string;
32
- items: string[]; // blog post permalinks
33
- permalink: string;
34
- pages: BlogPaginated[];
35
- }
36
-
37
- export interface BlogPost {
38
- id: string;
39
- metadata: BlogPostMetadata;
40
- content: string;
41
- }
42
-
43
- export interface BlogPaginated {
44
- metadata: BlogPaginatedMetadata;
45
- items: string[]; // blog post permalinks
46
- }
47
-
48
12
  export type BlogBrokenMarkdownLink = BrokenMarkdownLink<BlogContentPaths>;
49
13
  export type BlogMarkdownLoaderOptions = {
50
14
  siteDir: string;