How to Create a Custom Docusaurus Plugin from Scratch
A Docusaurus plugin is a JavaScript function that receives a context object and optional options, then returns a plugin instance object implementing lifecycle hooks such as loadContent and contentLoaded to extend the static site generation process.
Every feature in Docusaurus—from documentation to blog posts—is implemented as a plugin within the facebook/docusaurus architecture. Understanding how to create a custom Docusaurus plugin allows you to inject custom data sources, modify the webpack configuration, or generate new pages during the build process.
Understanding the Plugin Architecture
Docusaurus treats every feature as a plugin. Whether you define it inline or as an external module, a plugin follows a strict contract defined in the official documentation and source code.
The Core Plugin Contract
A plugin is simply a JavaScript (or TypeScript) function that receives two arguments: a context object containing Docusaurus internals, and an optional options object for configuration. The function must return an object containing at least a name property and any lifecycle methods you wish to implement.
According to website/docs/advanced/plugins.mdx, the basic structure looks like this:
export default async function myPlugin(context, options) {
return {
name: 'my-plugin',
// Lifecycle methods go here
};
}
Key Lifecycle Hooks
The website/docs/api/plugin-methods/lifecycle-apis.mdx file defines the available hooks that Docusaurus invokes during the build process:
loadContent– Runs during the build to fetch or generate data. Can be asynchronous.contentLoaded– Executes afterloadContent, receiving the loaded content andactionsobject (containingcreateDataandaddRoute).configureWebpack– Allows modification of the webpack configuration for both development and production.extendCli– Adds custom CLI commands to the Docusaurus command line interface.
Because plugins execute in a Node.js environment while themes run in the browser, any data destined for the client must be explicitly serialized using actions.createData.
Methods to Create a Custom Plugin
There are two primary approaches to adding a plugin to your site: inline definition for quick prototypes, or external modules for reusable logic.
Inline Function Definition (Quickest Method)
For simple, site-specific functionality, define the plugin directly inside your docusaurus.config.js (or docusaurus.config.mjs). Docusaurus evaluates this function while building the configuration.
This example reads a local JSON file at build time and creates a new route:
// docusaurus.config.js
export default {
plugins: [
async function myLocalDataPlugin(context, options) {
return {
name: 'my-local-data-plugin',
async loadContent() {
const fs = require('fs');
const data = JSON.parse(
await fs.promises.readFile('./data/stats.json', 'utf-8')
);
return data;
},
async contentLoaded({content, actions}) {
const {createData, addRoute} = actions;
// Serialize data for client-side consumption
const dataPath = await createData(
'stats.json',
JSON.stringify(content)
);
// Register a new page route
addRoute({
path: '/statistics',
component: '@site/src/components/StatsPage',
exact: true,
modules: {data: dataPath},
});
},
};
},
],
};
The modules property in addRoute passes the serialized data path to your React component as a prop.
External Module Definition (Reusable Method)
For cleaner code organization or sharing across projects, place your plugin in a separate folder. Create a directory structure like src/plugins/my-plugin/ or a standalone my-plugin/ folder at the project root.
File: src/plugins/fetch-articles/index.js
export default async function fetchArticlesPlugin(context, options) {
return {
name: 'fetch-articles-plugin',
async loadContent() {
// Fetch from a headless CMS or external API
const response = await fetch('https://api.example.com/articles');
const articles = await response.json();
return articles;
},
async contentLoaded({content, actions}) {
const {createData, addRoute} = actions;
const dataPath = await createData(
'articles.json',
JSON.stringify(content)
);
addRoute({
path: '/articles',
component: '@site/src/components/ArticleList',
exact: true,
modules: {articlesData: dataPath},
});
},
};
}
Reference the plugin in your configuration using a relative path:
// docusaurus.config.js
export default {
plugins: [
// Without options
'./src/plugins/fetch-articles',
// With options (as a tuple)
// ['./src/plugins/fetch-articles', {limit: 10}],
],
};
Publishing as an NPM Package
To distribute your plugin across multiple projects, structure it as an npm package with a package.json specifying Docusaurus as a peer dependency:
{
"name": "docusaurus-plugin-articles-sync",
"version": "1.0.0",
"main": "index.js",
"peerDependencies": {
"@docusaurus/core": "^3.0.0"
}
}
After publishing to npm, install and reference the package name directly in docusaurus.config.js:
npm install --save docusaurus-plugin-articles-sync
export default {
plugins: ['docusaurus-plugin-articles-sync'],
};
Critical Implementation Details
When you create a custom Docusaurus plugin, remember that data serialization is mandatory for client-side access. The loadContent hook runs in Node.js during the build, but your React components run in the browser. The createData method writes JSON files to the static build output, which the browser then fetches via the modules property in addRoute.
Additionally, the configureWebpack hook allows you to modify the build pipeline, but changes here affect both development and production environments. For CLI extensions, use extendCli to add custom commands to the Docusaurus binary.
Summary
- A Docusaurus plugin exports an async function returning an object with a
nameand lifecycle methods. - Use
loadContentto fetch or generate data during the build, andcontentLoadedwithcreateDataandaddRouteto expose that data to client-side routes. - Plugins execute in the Node.js environment, so any data passed to React components must be serialized via
actions.createData. - You can define plugins inline in
docusaurus.config.jsfor quick scripts, or as external modules insrc/plugins/for better organization. - For complete API details, reference
website/docs/api/plugin-methods/lifecycle-apis.mdxin the Docusaurus repository.
Frequently Asked Questions
What is the difference between a Docusaurus plugin and a theme?
A plugin runs in the Node.js environment during the build process to load content, create routes, and modify webpack configurations. A theme runs in the browser and provides React components for rendering the user interface. Plugins generate the data structure; themes consume it.
How do I pass custom data from a plugin to a React component?
Use the actions.createData method inside contentLoaded to serialize your data to a JSON file. Then pass the returned dataPath to the modules property in actions.addRoute. Your React component receives this as a prop and can load it using useRouteContext or direct import depending on the Docusaurus version.
Can I modify the webpack configuration with a custom plugin?
Yes. Implement the configureWebpack lifecycle method in your plugin object. This method receives the existing webpack config and the build context (isServer, isProd, etc.), allowing you to add loaders, plugins, or resolve aliases for both development and production builds.
Where should I place custom plugin files in my project?
Store site-specific plugins in a src/plugins/ directory or a top-level plugins/ folder. Reference them in docusaurus.config.js using relative paths like './src/plugins/my-plugin'. For reusable plugins intended for multiple projects, maintain them in separate repositories and publish them to npm.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →