# How to Create a Custom Docusaurus Plugin from Scratch

> Learn to create a custom Docusaurus plugin from scratch. Extend Docusaurus site generation using lifecycle hooks and JavaScript.

- Repository: [Meta/docusaurus](https://github.com/facebook/docusaurus)
- Tags: how-to-guide
- Published: 2026-03-04

---

**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:

```javascript
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 after `loadContent`, receiving the loaded content and `actions` object (containing `createData` and `addRoute`).
- **`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`](https://github.com/facebook/docusaurus/blob/main/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:

```javascript
// 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`](https://github.com/facebook/docusaurus/blob/main/src/plugins/fetch-articles/index.js)

```javascript
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:

```javascript
// 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`](https://github.com/facebook/docusaurus/blob/main/package.json) specifying Docusaurus as a peer dependency:

```json
{
  "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`](https://github.com/facebook/docusaurus/blob/main/docusaurus.config.js):

```bash
npm install --save docusaurus-plugin-articles-sync

```

```javascript
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 `name` and lifecycle methods.
- Use `loadContent` to fetch or generate data during the build, and `contentLoaded` with `createData` and `addRoute` to 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.js`](https://github.com/facebook/docusaurus/blob/main/docusaurus.config.js) for quick scripts, or as external modules in `src/plugins/` for better organization.
- For complete API details, reference `website/docs/api/plugin-methods/lifecycle-apis.mdx` in 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`](https://github.com/facebook/docusaurus/blob/main/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.