blog-plugin.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. const blogPluginExports = require("@docusaurus/plugin-content-blog");
  2. const utils = require("@docusaurus/utils");
  3. const path = require("path");
  4. const defaultBlogPlugin = blogPluginExports.default;
  5. const pluginDataDirRoot = path.join(
  6. ".docusaurus",
  7. "docusaurus-plugin-content-blog",
  8. );
  9. const aliasedSource = (source) =>
  10. `~blog/${utils.posixPath(path.relative(pluginDataDirRoot, source))}`;
  11. function paginateBlogPosts({
  12. blogPosts,
  13. basePageUrl,
  14. blogTitle,
  15. blogDescription,
  16. postsPerPageOption,
  17. }) {
  18. const totalCount = blogPosts.length;
  19. const postsPerPage =
  20. postsPerPageOption === "ALL" ? totalCount : postsPerPageOption;
  21. const numberOfPages = Math.ceil(totalCount / postsPerPage);
  22. const pages = [];
  23. function permalink(page) {
  24. return page > 0
  25. ? utils.normalizeUrl([basePageUrl, `page/${page + 1}`])
  26. : basePageUrl;
  27. }
  28. for (let page = 0; page < numberOfPages; page += 1) {
  29. pages.push({
  30. items: blogPosts
  31. .slice(page * postsPerPage, (page + 1) * postsPerPage)
  32. .map((item) => item.id),
  33. metadata: {
  34. permalink: permalink(page),
  35. page: page + 1,
  36. postsPerPage,
  37. totalPages: numberOfPages,
  38. totalCount,
  39. previousPage: page !== 0 ? permalink(page - 1) : undefined,
  40. nextPage:
  41. page < numberOfPages - 1 ? permalink(page + 1) : undefined,
  42. blogDescription,
  43. blogTitle,
  44. },
  45. });
  46. }
  47. return pages;
  48. }
  49. function getMultipleRandomElement(arr, num) {
  50. const shuffled = [...arr].sort(() => 0.5 - Math.random());
  51. return shuffled.slice(0, num);
  52. }
  53. function getReletadPosts(allBlogPosts, metadata) {
  54. const relatedPosts = allBlogPosts.filter(
  55. (post) =>
  56. post.metadata.frontMatter.tags?.some((tag) =>
  57. metadata.frontMatter.tags?.includes(tag),
  58. ) && post.metadata.title !== metadata.title,
  59. );
  60. const randomThreeRelatedPosts = getMultipleRandomElement(relatedPosts, 3);
  61. const filteredPostInfos = randomThreeRelatedPosts.map((post) => {
  62. return {
  63. title: post.metadata.title,
  64. description: post.metadata.description,
  65. permalink: post.metadata.permalink,
  66. formattedDate: post.metadata.formattedDate,
  67. authors: post.metadata.authors,
  68. readingTime: post.metadata.readingTime,
  69. date: post.metadata.date,
  70. };
  71. });
  72. return filteredPostInfos;
  73. }
  74. function getAuthorPosts(allBlogPosts, metadata) {
  75. const authorPosts = allBlogPosts.filter(
  76. (post) =>
  77. post.metadata.frontMatter.authors ===
  78. metadata.frontMatter.authors &&
  79. post.metadata.title !== metadata.title,
  80. );
  81. const randomThreeAuthorPosts = getMultipleRandomElement(authorPosts, 3);
  82. const filteredPostInfos = randomThreeAuthorPosts.map((post) => {
  83. return {
  84. title: post.metadata.title,
  85. description: post.metadata.description,
  86. permalink: post.metadata.permalink,
  87. formattedDate: post.metadata.formattedDate,
  88. authors: post.metadata.authors,
  89. readingTime: post.metadata.readingTime,
  90. date: post.metadata.date,
  91. };
  92. });
  93. return filteredPostInfos;
  94. }
  95. async function blogPluginExtended(...pluginArgs) {
  96. const blogPluginInstance = await defaultBlogPlugin(...pluginArgs);
  97. const { blogTitle, blogDescription, postsPerPage } = pluginArgs[1];
  98. return {
  99. // Add all properties of the default blog plugin so existing functionality is preserved
  100. ...blogPluginInstance,
  101. /**
  102. * Override the default `contentLoaded` hook to access blog posts data
  103. */
  104. contentLoaded: async function (data) {
  105. const { content: blogContents, actions } = data;
  106. const { addRoute, createData } = actions;
  107. const {
  108. blogPosts: allBlogPosts,
  109. blogTags,
  110. blogTagsListPath,
  111. } = blogContents;
  112. const blogItemsToMetadata = {};
  113. function blogPostItemsModule(items) {
  114. return items.map((postId) => {
  115. const blogPostMetadata = blogItemsToMetadata[postId];
  116. return {
  117. content: {
  118. __import: true,
  119. path: blogPostMetadata.source,
  120. query: {
  121. truncated: true,
  122. },
  123. },
  124. };
  125. });
  126. }
  127. const featuredBlogPosts = allBlogPosts.filter(
  128. (post) => post.metadata.frontMatter.is_featured === true,
  129. );
  130. const blogPosts = allBlogPosts.filter(
  131. (post) => post.metadata.frontMatter.is_featured !== true,
  132. );
  133. const blogListPaginated = paginateBlogPosts({
  134. blogPosts,
  135. basePageUrl: "/blog",
  136. blogTitle,
  137. blogDescription,
  138. postsPerPageOption: postsPerPage,
  139. });
  140. // Create routes for blog entries.
  141. await Promise.all(
  142. allBlogPosts.map(async (blogPost) => {
  143. const { id, metadata } = blogPost;
  144. const relatedPosts = getReletadPosts(
  145. allBlogPosts,
  146. metadata,
  147. );
  148. const authorPosts = getAuthorPosts(allBlogPosts, metadata);
  149. await createData(
  150. // Note that this created data path must be in sync with
  151. // metadataPath provided to mdx-loader.
  152. `${utils.docuHash(metadata.source)}.json`,
  153. JSON.stringify(
  154. { ...metadata, relatedPosts, authorPosts },
  155. null,
  156. 2,
  157. ),
  158. );
  159. addRoute({
  160. path: metadata.permalink,
  161. component: "@theme/BlogPostPage",
  162. exact: true,
  163. modules: {
  164. content: metadata.source,
  165. },
  166. });
  167. blogItemsToMetadata[id] = metadata;
  168. }),
  169. );
  170. // Create routes for blog's paginated list entries.
  171. await Promise.all(
  172. blogListPaginated.map(async (listPage) => {
  173. const { metadata, items } = listPage;
  174. const { permalink } = metadata;
  175. const pageMetadataPath = await createData(
  176. `${utils.docuHash(permalink)}.json`,
  177. JSON.stringify(metadata, null, 2),
  178. );
  179. const tagsProp = Object.values(blogTags).map((tag) => ({
  180. label: tag.label,
  181. permalink: tag.permalink,
  182. count: tag.items.length,
  183. }));
  184. const tagsPropPath = await createData(
  185. `${utils.docuHash(`${blogTagsListPath}-tags`)}.json`,
  186. JSON.stringify(tagsProp, null, 2),
  187. );
  188. addRoute({
  189. path: permalink,
  190. component: "@theme/BlogListPage",
  191. exact: true,
  192. modules: {
  193. items: blogPostItemsModule(
  194. permalink === "/blog"
  195. ? [
  196. ...items,
  197. ...featuredBlogPosts.map(
  198. (post) => post.id,
  199. ),
  200. ]
  201. : items,
  202. ),
  203. metadata: aliasedSource(pageMetadataPath),
  204. tags: aliasedSource(tagsPropPath),
  205. },
  206. });
  207. }),
  208. );
  209. const authorsArray = allBlogPosts
  210. .map((post) => post.metadata.frontMatter.authors)
  211. .filter((authorName) => authorName !== undefined);
  212. const uniqueAuthors = [...new Set(authorsArray)];
  213. uniqueAuthors.map(async (author) => {
  214. const authorPosts = allBlogPosts.filter(
  215. (post) => post.metadata.frontMatter.authors === author,
  216. );
  217. const authorListPaginated = paginateBlogPosts({
  218. blogPosts: authorPosts,
  219. basePageUrl: "/blog/author/" + author,
  220. blogTitle,
  221. blogDescription,
  222. postsPerPageOption: "ALL",
  223. });
  224. authorListPaginated.map((authorListPage) => {
  225. const { metadata, items } = authorListPage;
  226. const { permalink } = metadata;
  227. addRoute({
  228. path: permalink,
  229. component: "@site/src/components/blog/author-page",
  230. exact: true,
  231. modules: {
  232. items: blogPostItemsModule(items),
  233. },
  234. });
  235. });
  236. });
  237. // Tags. This is the last part so we early-return if there are no tags.
  238. if (Object.keys(blogTags).length === 0) {
  239. return;
  240. }
  241. async function createTagsListPage() {
  242. const tagsProp = Object.values(blogTags).map((tag) => ({
  243. label: tag.label,
  244. permalink: tag.permalink,
  245. count: tag.items.length,
  246. }));
  247. const tagsPropPath = await createData(
  248. `${utils.docuHash(`${blogTagsListPath}-tags`)}.json`,
  249. JSON.stringify(tagsProp, null, 2),
  250. );
  251. addRoute({
  252. path: blogTagsListPath,
  253. component: "@theme/BlogTagsListPage",
  254. exact: true,
  255. modules: {
  256. tags: aliasedSource(tagsPropPath),
  257. },
  258. });
  259. }
  260. async function createTagPostsListPage(tag) {
  261. await Promise.all(
  262. tag.pages.map(async (blogPaginated) => {
  263. const { metadata, items } = blogPaginated;
  264. const tagProp = {
  265. label: tag.label,
  266. permalink: tag.permalink,
  267. allTagsPath: blogTagsListPath,
  268. count: tag.items.length,
  269. };
  270. const tagPropPath = await createData(
  271. `${utils.docuHash(metadata.permalink)}.json`,
  272. JSON.stringify(tagProp, null, 2),
  273. );
  274. const listMetadataPath = await createData(
  275. `${utils.docuHash(metadata.permalink)}-list.json`,
  276. JSON.stringify(metadata, null, 2),
  277. );
  278. const tagsProp = Object.values(blogTags).map((tag) => ({
  279. label: tag.label,
  280. permalink: tag.permalink,
  281. count: tag.items.length,
  282. }));
  283. const tagsPropPath = await createData(
  284. `${utils.docuHash(
  285. `${blogTagsListPath}-tags`,
  286. )}.json`,
  287. JSON.stringify(tagsProp, null, 2),
  288. );
  289. addRoute({
  290. path: metadata.permalink,
  291. component: "@theme/BlogTagsPostsPage",
  292. exact: true,
  293. modules: {
  294. items: blogPostItemsModule(items),
  295. tag: aliasedSource(tagPropPath),
  296. tags: aliasedSource(tagsPropPath),
  297. listMetadata: aliasedSource(listMetadataPath),
  298. },
  299. });
  300. }),
  301. );
  302. }
  303. await createTagsListPage();
  304. await Promise.all(
  305. Object.values(blogTags).map(createTagPostsListPage),
  306. );
  307. },
  308. };
  309. }
  310. module.exports = {
  311. ...blogPluginExports,
  312. default: blogPluginExtended,
  313. };