Show recent posts in Docusaurus
TL;DR
At a high level, these are the steps.
- Create a custom plugin that extends the original blog plugin (plugin-content-blog).
- Disable the original blog plugin and use the new custom one.
- Import the JSON file containing the recent articles.
For details of each step, seeOption 2.
Preamble
I'm in the camp of static site generators when it comes to creating a blog. Hands-downDocusaurus is the best that I've come across.
As soon as I got to understand Docusaurus, I ditchedHugo, my previous static site generator of choice. I knew that I will be using Docusaurus for the long term.
Despite my partiality towards Docusaurus, one function that I've found sorely missing is the ability to show the most recent blog posts in the home page. AndI'm not the only one.
Ever since I moved my blog over in February 2023, I've been looking for a method to do this on and off over the past year.
Until recently, the best explanation that I've founddescribes in abstract terms how to do it but doesn't show any concrete steps.
Option 1
After a few months, I decided that there was no easy way. So I thought of a method where I render the posts client side using the RSS feed from the blog.
It's really more of a workaround rather than an "actual" solution.
This is achieved by usinguseEffect
that fetches and parses the RSS feed. Here is how I did it.
Import the useEffect function if not already done.
import useEffectfrom'react';
Fetch the RSS feed inuseEffect
:
const[feed, setFeed]=useState<Record<string,any>[]|null>(null);
useEffect(functionfetchFeed(){
fetch(`${WEBSITE}/blog/rss.xml`)
.then((resp)=> resp.text())
.then((xmlText)=>{
const xml=newwindow.DOMParser().parseFromString(xmlText,'text/xml');
const items= xml.querySelectorAll('item');
const latestArticle=0;
const endingArticle=2;
const posts=extractPosts(items, latestArticle, endingArticle);
setFeed(posts);
});
},[]);
Thefetch
function retrieves the RSS feed from the published website (indicated by the variableWEBSITE
). The feed is then converted into XML (line 7). The articles are contained initem
elements (line 9). In this code segment, the latest article is selected (latestArticle = 0
) along with the second latest one (endingArticle = 2
), meaning that the newest 2 articles will be shown.
The posts are extracted usingextractPosts
.
Extract the posts
functionextractPosts(elements: NodeListOf<Element>, start=0, count=1){
let subset=[];
for(let i= start; i< start+ count; i++){
if(i< elements.length){
subset.push({
title: elements[i].querySelector('title').textContent,
link: elements[i].querySelector('link').innerHTML,
pubDate: elements[i].querySelector('pubDate').textContent,
description: elements[i].querySelector('description').textContent??'',
});
}
}
return subset;
}
This will make the recent articles are available in the index page (setFeed(posts)
), so they can then be rendered to your desire.
Drawbacks
While this method is straightforward, there are some drawbacks.
- First, it depends on the RSS feed from the production website, which means that it will not reflect the articles that you just added to the site in development.
- Second, since the list is rendered client-side, it will (generally) not be picked up by search engines. Granted, this may not impact the SEO performance much since it's just a short list of articles.
- Third, since the code is fetching another resource after the page is loaded, there will be a delay before the list is rendered. Depending on your preference and how you layout your page, this might be a dealbreaker.
- Fourth, depending on how the server hosting the site behaves, the feed may be stale i.e. the feed may not show the latest articles from the site, making the list show inaccurate data.
Despite all these shortcomings, this was the only solution I had for a while.
But the problem about showing stale data (problem #4) was painful enough for me to keep looking for a better solution.
Option 2
After spending the better part of Easter Friday, I finally got something working.
I found anarticle that actually describes step-by-step how the recent articles can be placed in the index page. However, this method requires replacing the index page with the blog listing page. This was a bit more drastic than what I was expecting - I didn't want to have to redesign the blog listing page to look like the index page.
Instead I opted to use the method suggested bySébastien Lorber (one of Docusaurus' maintainers) in thisGithub issue which is to create an intermediate JSON file. Then the index page will import this JSON file and render the posts on the index page.
As described in theTL;DR, the steps are:
Create a custom plugin that extends the original blog plugin (plugin-content-blog)
const fs=require('node:fs');
const blogPluginExports=require('@docusaurus/plugin-content-blog');
const defaultBlogPlugin= blogPluginExports.default;
asyncfunctionblogPluginEnhanced(...pluginArgs){
const blogPluginInstance=awaitdefaultBlogPlugin(...pluginArgs);
const dir='.docusaurus';
return{
...blogPluginInstance,
contentLoaded:asyncfunction(data){
let recentPosts=[...data.content.blogPosts]
// Only show published posts.
.filter((p)=>!p.metadata.unlisted)
.slice(0,3);
recentPosts= recentPosts.map((p)=>{
return{
id: p.id,
metadata: p.metadata,
};
});
fs.mkdirSync(dir,{
recursive:true,// Avoid error if directory already exists.
});
const fd= fs.openSync(`${dir}/recent-posts.json`,'w');
fs.writeSync(fd,JSON.stringify(recentPosts));
return blogPluginInstance.contentLoaded(data);
},
};
}
module.exports={
...blogPluginExports,
default: blogPluginEnhanced,
};
Create aplugins directory in root of the project (same level assrc). Place the filerecent-blog-posts.js into this directory as the plugin. (You can choose different names for the directory and file - they are not treated any differently.)
By including the plugin (line 3), the plugin is being extended.
Line 17 is of note. It checks to make sure that only listed posts will be included (filter((p) => !p.metadata.unlisted)
). Remember that this value onlytakes effect in production.
On line 18, we tell the plugin to return the latest 3 posts.
Lines 20-25 are optional. They are there to keep the intermediate JSON file small by omitting the full contents of the posts.
Lines 30-31 write the posts into the intermediate JSON which is namedrecent-posts.json
and placed in the.docusaurus directory.
Disable the original blog plugin and use the new custom one
This step requires modifying the configuration filedocusaurus.config.js. The exact changes depend on whether the blog plugin was added manually (standalone) or as part of the theme.
If the plugin is configured standalone, then it would be in theplugins
array:
const config={
plugins:[
[
'@docusaurus/plugin-content-blog',
{
// rest of blog plugin options
},
],
],
// rest of config
};
To use the new custom plugin, swap out the original one with the new one.
const config={
plugins:[
[
'./plugins/recent-blog-posts',
{
// rest of blog plugin options
},
],
],
// rest of config
};
If the plugin is included as part of a preset (e.g. the classic preset@docusaurus/preset-classic
), like the majority of users who follow theinstallation guide do, then the configuration is placed in thepresets
array:
const config={
presets:[
[
'classic',
/**@type{import('@docusaurus/preset-classic').Options} */
({
blog:{
showReadingTime:true,
// rest of blog plugin options
},
// rest of preset options
}),
],
],
// rest of config
};
To use the new custom plugin, disable the blog plugin in the preset:
const config={
presets:[
[
'classic',
/**@type{import('@docusaurus/preset-classic').Options} */
({
blog:false,
// rest of preset options
}),
],
],
// rest of config
};
And add the custom plugin into theplugins
array (you may have to create the entireplugins
property if it is not already present):
const config={
plugins:[
[
'./plugins/recent-blog-posts',
{
showReadingTime:true,
// rest of blog plugin options
},
],
],
// rest of config
};
Import the JSON file containing the recent articles
Add the following import statement into your index page (or component) and you can access the recent articles.
importrecentPostsfrom'@site/.docusaurus/recent-posts.json';
TypeScript users, you may see an wriggly underline error in your IDE sayingCannot find module '@site/.docusaurus/recent-posts.json'. Consider using '--resolveJsonModule' to import module with '.json' extension.
To resolve this issue, follow the instructions and add"resolveJsonModule": true
to thetsconfig.json file found at the root.
Conclusion
I would have preferred to be able to specify the number of recent posts to display by specifying the number as an option indocusaurus.config.js but I couldn't get it to work.
Adding an additional option for the plugin like this:
const config={
plugins:[
[
'./plugins/recent-blog-posts',
{
showReadingTime:true,
recentPosts:5,
// rest of blog plugin options
},
],
],
// rest of config
};
raises this error:
[ERROR] ValidationError: "recentPosts" is not allowed
This is due to the validation of options by the original plugin. I do not think there is much value in modifying the original plugin (or to fork it) to achieve this minor enhancement.
I'm pretty happy with this outcome as it is. Nevertheless, if you have an idea of how to make the number of recent posts configurable indocusaurus.config.js, hit me up onTwitter.