const blogPluginExports = require("@docusaurus/plugin-content-blog"); const utils = require("@docusaurus/utils"); const path = require("path"); const defaultBlogPlugin = blogPluginExports.default; const pluginDataDirRoot = path.join( ".docusaurus", "docusaurus-plugin-content-blog", ); const aliasedSource = (source) => `~blog/${utils.posixPath(path.relative(pluginDataDirRoot, source))}`; function paginateBlogPosts({ blogPosts, basePageUrl, blogTitle, blogDescription, postsPerPageOption, }) { const totalCount = blogPosts.length; const postsPerPage = postsPerPageOption === "ALL" ? totalCount : postsPerPageOption; const numberOfPages = Math.ceil(totalCount / postsPerPage); const pages = []; function permalink(page) { return page > 0 ? utils.normalizeUrl([basePageUrl, `page/${page + 1}`]) : basePageUrl; } for (let page = 0; page < numberOfPages; page += 1) { pages.push({ items: blogPosts .slice(page * postsPerPage, (page + 1) * postsPerPage) .map((item) => item.id), metadata: { permalink: permalink(page), page: page + 1, postsPerPage, totalPages: numberOfPages, totalCount, previousPage: page !== 0 ? permalink(page - 1) : undefined, nextPage: page < numberOfPages - 1 ? permalink(page + 1) : undefined, blogDescription, blogTitle, }, }); } return pages; } function getMultipleRandomElement(arr, num) { const shuffled = [...arr].sort(() => 0.5 - Math.random()); return shuffled.slice(0, num); } function getReletadPosts(allBlogPosts, metadata) { const relatedPosts = allBlogPosts.filter( (post) => post.metadata.frontMatter.tags?.some((tag) => metadata.frontMatter.tags?.includes(tag), ) && post.metadata.title !== metadata.title, ); const randomThreeRelatedPosts = getMultipleRandomElement(relatedPosts, 3); const filteredPostInfos = randomThreeRelatedPosts.map((post) => { return { title: post.metadata.title, description: post.metadata.description, permalink: post.metadata.permalink, formattedDate: post.metadata.formattedDate, authors: post.metadata.authors, readingTime: post.metadata.readingTime, date: post.metadata.date, }; }); return filteredPostInfos; } function getAuthorPosts(allBlogPosts, metadata) { const authorPosts = allBlogPosts.filter( (post) => post.metadata.frontMatter.authors === metadata.frontMatter.authors && post.metadata.title !== metadata.title, ); const randomThreeAuthorPosts = getMultipleRandomElement(authorPosts, 3); const filteredPostInfos = randomThreeAuthorPosts.map((post) => { return { title: post.metadata.title, description: post.metadata.description, permalink: post.metadata.permalink, formattedDate: post.metadata.formattedDate, authors: post.metadata.authors, readingTime: post.metadata.readingTime, date: post.metadata.date, }; }); return filteredPostInfos; } async function blogPluginExtended(...pluginArgs) { const blogPluginInstance = await defaultBlogPlugin(...pluginArgs); const { blogTitle, blogDescription, postsPerPage } = pluginArgs[1]; return { // Add all properties of the default blog plugin so existing functionality is preserved ...blogPluginInstance, /** * Override the default `contentLoaded` hook to access blog posts data */ contentLoaded: async function (data) { const { content: blogContents, actions } = data; const { addRoute, createData } = actions; const { blogPosts: allBlogPosts, blogTags, blogTagsListPath, } = blogContents; const blogItemsToMetadata = {}; function blogPostItemsModule(items) { return items.map((postId) => { const blogPostMetadata = blogItemsToMetadata[postId]; return { content: { __import: true, path: blogPostMetadata.source, query: { truncated: true, }, }, }; }); } const featuredBlogPosts = allBlogPosts.filter( (post) => post.metadata.frontMatter.is_featured === true, ); const blogPosts = allBlogPosts.filter( (post) => post.metadata.frontMatter.is_featured !== true, ); const blogListPaginated = paginateBlogPosts({ blogPosts, basePageUrl: "/blog", blogTitle, blogDescription, postsPerPageOption: postsPerPage, }); // Create routes for blog entries. await Promise.all( allBlogPosts.map(async (blogPost) => { const { id, metadata } = blogPost; const relatedPosts = getReletadPosts( allBlogPosts, metadata, ); const authorPosts = getAuthorPosts(allBlogPosts, metadata); await createData( // Note that this created data path must be in sync with // metadataPath provided to mdx-loader. `${utils.docuHash(metadata.source)}.json`, JSON.stringify( { ...metadata, relatedPosts, authorPosts }, null, 2, ), ); addRoute({ path: metadata.permalink, component: "@theme/BlogPostPage", exact: true, modules: { content: metadata.source, }, }); blogItemsToMetadata[id] = metadata; }), ); // Create routes for blog's paginated list entries. await Promise.all( blogListPaginated.map(async (listPage) => { const { metadata, items } = listPage; const { permalink } = metadata; const pageMetadataPath = await createData( `${utils.docuHash(permalink)}.json`, JSON.stringify(metadata, null, 2), ); const tagsProp = Object.values(blogTags).map((tag) => ({ label: tag.label, permalink: tag.permalink, count: tag.items.length, })); const tagsPropPath = await createData( `${utils.docuHash(`${blogTagsListPath}-tags`)}.json`, JSON.stringify(tagsProp, null, 2), ); addRoute({ path: permalink, component: "@theme/BlogListPage", exact: true, modules: { items: blogPostItemsModule( permalink === "/blog" ? [ ...items, ...featuredBlogPosts.map( (post) => post.id, ), ] : items, ), metadata: aliasedSource(pageMetadataPath), tags: aliasedSource(tagsPropPath), }, }); }), ); const authorsArray = allBlogPosts .map((post) => post.metadata.frontMatter.authors) .filter((authorName) => authorName !== undefined); const uniqueAuthors = [...new Set(authorsArray)]; uniqueAuthors.map(async (author) => { const authorPosts = allBlogPosts.filter( (post) => post.metadata.frontMatter.authors === author, ); const authorListPaginated = paginateBlogPosts({ blogPosts: authorPosts, basePageUrl: "/blog/author/" + author, blogTitle, blogDescription, postsPerPageOption: "ALL", }); authorListPaginated.map((authorListPage) => { const { metadata, items } = authorListPage; const { permalink } = metadata; addRoute({ path: permalink, component: "@site/src/components/blog/author-page", exact: true, modules: { items: blogPostItemsModule(items), }, }); }); }); // Tags. This is the last part so we early-return if there are no tags. if (Object.keys(blogTags).length === 0) { return; } async function createTagsListPage() { const tagsProp = Object.values(blogTags).map((tag) => ({ label: tag.label, permalink: tag.permalink, count: tag.items.length, })); const tagsPropPath = await createData( `${utils.docuHash(`${blogTagsListPath}-tags`)}.json`, JSON.stringify(tagsProp, null, 2), ); addRoute({ path: blogTagsListPath, component: "@theme/BlogTagsListPage", exact: true, modules: { tags: aliasedSource(tagsPropPath), }, }); } async function createTagPostsListPage(tag) { await Promise.all( tag.pages.map(async (blogPaginated) => { const { metadata, items } = blogPaginated; const tagProp = { label: tag.label, permalink: tag.permalink, allTagsPath: blogTagsListPath, count: tag.items.length, }; const tagPropPath = await createData( `${utils.docuHash(metadata.permalink)}.json`, JSON.stringify(tagProp, null, 2), ); const listMetadataPath = await createData( `${utils.docuHash(metadata.permalink)}-list.json`, JSON.stringify(metadata, null, 2), ); const tagsProp = Object.values(blogTags).map((tag) => ({ label: tag.label, permalink: tag.permalink, count: tag.items.length, })); const tagsPropPath = await createData( `${utils.docuHash( `${blogTagsListPath}-tags`, )}.json`, JSON.stringify(tagsProp, null, 2), ); addRoute({ path: metadata.permalink, component: "@theme/BlogTagsPostsPage", exact: true, modules: { items: blogPostItemsModule(items), tag: aliasedSource(tagPropPath), tags: aliasedSource(tagsPropPath), listMetadata: aliasedSource(listMetadataPath), }, }); }), ); } await createTagsListPage(); await Promise.all( Object.values(blogTags).map(createTagPostsListPage), ); }, }; } module.exports = { ...blogPluginExports, default: blogPluginExtended, };