Generating pages dynamically instead of relying on a 1-1 relationship of files to web pages seems to be an often overlooked aspect of static site generators, and Docusaurus is no exception. However, with some prowess and tinkering with the plugin system, we can definitely make the "impossible" possible, and generate pages from whatever data source we want.
Interested? Read on...
This post is written for v2.4
The Plugin Lifecycleโ
To understand the flow of how pages are generated, we need to understand that Docusaurus and all of its core functionality is a collection of plugins. A plugin is, put simply, an function that returns an object of callbacks. Each callback is executed at different stages of the content generation lifecycle.
We can fully examine how pages are generated by reading through and grokking the source code of the Docusaurus blog content generation plugin. The callback of interest is the loadContent()
callback, where we load content and metadata for each page.
After all pages are loaded, the contentLoaded(){...}
callback then takes the array of page data from the loadContent()
execution and creates one or more pages. The contentLoaded
function receives an object as an argument, with two keys of interest: content
and actions
.
content
would be the content from the previous loadContent()
step, and can be any data type. For example, you can return an object in loadContent
with multiple keys.
actions
is an object of available actions to tell Docusaurus what to do. Of note, the actions.addRoute(...)
function is used to create a file to serve at a given route path.
Full documentation of the plugin lifecycle is here, but the above APIs are the bare minimum for what we want to accomplish.
The Custom Pluginโ
Lets navigate to our docusaurus.config.js
and update our plugins
key:
// docusaurus.config.js
plugins: [
// the function name can be anything
async function pagesGenPlugin(context, options) {
return {
// a unique name for this plugin
name: "pages-gen",
// lifecycle callback
async loadContent() {
// we can make some async/await API calls here
// or load data from a database
return {some: "data"} ;
},
// lifecycle callback
async contentLoaded({ content, actions }) {
const {some} = content
// optional: use Promise.all to execute multiple async functions at once
// this will speed things up by creating pages in parallel
await Promise.all(
[{
myPageData: "some_text",
},{
myPageData: "some_other_text",
}].map(async (page) => {
return actions.addRoute({
// this is the path slug
// you can make it dynamic here
path: `/some-${page.myPageData}`,
// the page component used to render the page
component: require.resolve( "./src/MyCustomPage.tsx"),
// will only match for exactly matching paths
exact: true,
// you can use this to optionally overwrite certain theme components
// see here: https://github.com/facebook/docusaurus/blob/main/packages/docusaurus-plugin-content-blog/src/index.ts#L343
modules: {},
// any extra custom data keys are passed to the page
// in this case, we merge the page data together with the loaded content data
customData: {...content, ...page}
});
})
);
},
// ...
]
The above code can be used as a basis for your custom docusuarus page.
Lets break this down.
Each plugin requires a custom name to be set, and it cannot clash with any other plugin.
Thereafter, the loadContent()
callback loads data to be used by the plugin. For the plugin-content-blog
plugin, it will read files on the filesystem, however you can load data from anywhere you like.
After this, the contentLoaded()
callback will process the data received into different routes. Each route reuires a RouteConfig, which will dictate what is written to the file system on build. It will also specify what components are used to render the page, as well as to specify overwriting of theme component modules. You will determine the page slug here, and it will also be the file path of the generated page.
Notice that you will need a file at src/MyCustomPage.tsx
. This file will be used to render your custom page, and Docusaurus will use it to statically export the React component as a page.
Creating Your Custom Pageโ
// src/MyCustomPage.tsx
import Layout from "@theme/Layout";
const MyCustomPage = (props) => {
const customData = props.route.customData;
return (
<Layout>
// this will show the text "data" on the page
<h1>{customData.some}</h1>
</Layout>
);
};
export default MyCustomPage;
Above is an example page that uses the route config data to render the page. Remember that any custom data that you set on the route config will also be accessible as part of the props that it receives.
Conclusionโ
Docusaurus is one of the most flexible static site generators with extremely helpful out-of-the-box components, and this is just one example of how awesomely extensible I think it is as compared to its competitors.