Skip to main content

Creating Plugins

Learn how to extend Doom by creating custom plugins that add new features, transform content, or integrate external tools.

Plugin Basics

A Doom plugin is a function that returns an object conforming to the RspressPlugin interface:
import type { RspressPlugin } from '@rspress/core'

export const myPlugin = (options): RspressPlugin => {
  return {
    name: 'my-plugin',
    // Plugin hooks
  }
}

Plugin Structure

Basic Template

plugins/my-plugin/index.ts
import type { RspressPlugin } from '@rspress/core'

export interface MyPluginOptions {
  enabled?: boolean
  customOption?: string
}

export const myPlugin = ({
  enabled = true,
  customOption = 'default'
}: MyPluginOptions = {}): RspressPlugin => {
  return {
    name: 'my-plugin',
    
    // Modify configuration
    config(config, utils) {
      // Modify and return config
      return config
    },
    
    // Add markdown transformers
    markdown: {
      remarkPlugins: [],
      rehypePlugins: []
    },
    
    // Add runtime modules
    addRuntimeModules(config, isProd) {
      return {
        'my-plugin-virtual': `export default ${JSON.stringify({ customOption })}`
      }
    }
  }
}

Plugin Hooks

config

Modify the user configuration:
config(config, utils) {
  // Add to existing config
  config.title = config.title || 'My Docs'
  
  // Remove other plugins
  utils.removePlugin('some-plugin')
  
  return config
}

markdown

Add remark or rehype plugins:
markdown: {
  remarkPlugins: [
    remarkPlugin,
    [remarkPluginWithOptions, { option: true }]
  ],
  rehypePlugins: [
    rehypePlugin
  ],
  globalComponents: [
    path.resolve(__dirname, './components/MyComponent.tsx')
  ]
}

addRuntimeModules

Create virtual modules accessible at runtime:
addRuntimeModules(config, isProd) {
  return {
    'my-plugin-virtual': `export default { data: 'value' }`,
    'my-plugin-config': `export const config = ${JSON.stringify(config.myPlugin)}`
  }
}
Access in components:
import data from 'my-plugin-virtual'
import { config } from 'my-plugin-config'

addPages

Add custom pages:
addPages(config) {
  return [
    {
      routePath: '/custom-page',
      filepath: path.resolve(__dirname, './pages/CustomPage.tsx')
    }
  ]
}

globalStyles

Add global CSS/SCSS:
globalStyles: path.resolve(__dirname, './styles/global.scss')

globalUIComponents

Add components to every page:
globalUIComponents: [
  path.resolve(__dirname, './components/GlobalHeader.tsx'),
  path.resolve(__dirname, './components/GlobalFooter.tsx')
]

Creating a Remark Plugin

Remark plugins transform the markdown AST:
plugins/my-plugin/remark-my-transform.ts
import type { Root } from 'mdast'
import type { Plugin } from 'unified'
import { visit } from 'unist-util-visit'

export const remarkMyTransform: Plugin<[], Root> = function () {
  return (root) => {
    visit(root, 'code', (node, index, parent) => {
      if (node.lang === 'my-lang') {
        // Transform the node
        parent.children[index] = {
          type: 'mdxJsxFlowElement',
          name: 'MyComponent',
          attributes: [
            {
              type: 'mdxJsxAttribute',
              name: 'code',
              value: node.value
            }
          ],
          children: []
        }
      }
    })
  }
}
Use in your plugin:
import { remarkMyTransform } from './remark-my-transform'

export const myPlugin = (): RspressPlugin => {
  return {
    name: 'my-plugin',
    markdown: {
      remarkPlugins: [remarkMyTransform]
    }
  }
}

Example: Custom Embed Plugin

Create a plugin that embeds external content:
plugins/embed-plugin/index.ts
import type { RspressPlugin } from '@rspress/core'
import type { Root } from 'mdast'
import type { Plugin } from 'unified'
import { visit } from 'unist-util-visit'

export interface EmbedPluginOptions {
  providers: Record<string, string>
}

const remarkEmbed: Plugin<[EmbedPluginOptions], Root> = function (options) {
  return (root) => {
    visit(root, 'code', (node, index, parent) => {
      if (node.lang === 'embed') {
        const [provider, id] = node.value.split(':')
        const url = options.providers[provider]?.replace('{id}', id)
        
        if (url) {
          parent.children[index] = {
            type: 'mdxJsxFlowElement',
            name: 'iframe',
            attributes: [
              { type: 'mdxJsxAttribute', name: 'src', value: url },
              { type: 'mdxJsxAttribute', name: 'width', value: '100%' },
              { type: 'mdxJsxAttribute', name: 'height', value: '400' }
            ],
            children: []
          }
        }
      }
    })
  }
}

export const embedPlugin = (options: EmbedPluginOptions): RspressPlugin => {
  return {
    name: 'embed-plugin',
    markdown: {
      remarkPlugins: [[remarkEmbed, options]]
    }
  }
}
Usage:
export default defineConfig({
  plugins: [
    embedPlugin({
      providers: {
        youtube: 'https://www.youtube.com/embed/{id}',
        vimeo: 'https://player.vimeo.com/video/{id}'
      }
    })
  ]
})
In markdown:
```embed
youtube:dQw4w9WgXcQ
```

Example: Analytics Plugin

Add analytics tracking:
plugins/analytics-plugin/index.ts
import type { RspressPlugin } from '@rspress/core'
import path from 'node:path'

export interface AnalyticsPluginOptions {
  trackingId: string
  provider: 'google' | 'plausible'
}

export const analyticsPlugin = ({
  trackingId,
  provider
}: AnalyticsPluginOptions): RspressPlugin => {
  return {
    name: 'analytics-plugin',
    
    globalUIComponents: [
      path.resolve(__dirname, './components/Analytics.tsx')
    ],
    
    addRuntimeModules(config, isProd) {
      return {
        'analytics-config': `export default ${JSON.stringify({
          trackingId,
          provider,
          enabled: isProd
        })}`
      }
    }
  }
}
plugins/analytics-plugin/components/Analytics.tsx
import { useEffect } from 'react'
import config from 'analytics-config'

export default function Analytics() {
  useEffect(() => {
    if (config.enabled) {
      // Load analytics script
      const script = document.createElement('script')
      script.src = `https://analytics.example.com/${config.provider}.js`
      script.dataset.trackingId = config.trackingId
      document.head.appendChild(script)
    }
  }, [])
  
  return null
}

Loading External Data

Load and process external data at build time:
import fs from 'node:fs/promises'
import path from 'node:path'
import type { RspressPlugin } from '@rspress/core'

export const dataPlugin = (dataPath: string): RspressPlugin => {
  return {
    name: 'data-plugin',
    
    async addRuntimeModules(config) {
      const fullPath = path.resolve(config.root!, dataPath)
      const data = await fs.readFile(fullPath, 'utf-8')
      const parsed = JSON.parse(data)
      
      return {
        'data-plugin-data': `export default ${JSON.stringify(parsed)}`
      }
    }
  }
}

Working with AST

Understand the markdown AST structure:
import type { Root, Heading, Paragraph, Text } from 'mdast'
import { visit } from 'unist-util-visit'

const remarkExample: Plugin<[], Root> = function () {
  return (root) => {
    // Visit all nodes
    visit(root, (node) => {
      console.log(node.type)
    })
    
    // Visit specific node types
    visit(root, 'heading', (node: Heading) => {
      console.log(`H${node.depth}`, node.children)
    })
    
    // Transform nodes
    visit(root, 'paragraph', (node: Paragraph, index, parent) => {
      // Modify or replace the node
    })
  }
}

Best Practices

  1. Use TypeScript - Provide type definitions for options
  2. Document Options - Clearly document all configuration options
  3. Handle Errors - Gracefully handle missing files or invalid data
  4. Performance - Minimize build-time operations
  5. Modularity - Keep plugins focused on one task
  6. Testing - Test with various configurations
  7. Naming - Use descriptive, unique plugin names

Testing Plugins

Create a test setup:
import { describe, it, expect } from 'vitest'
import { unified } from 'unified'
import remarkParse from 'remark-parse'
import { remarkMyPlugin } from './remark-my-plugin'

describe('remarkMyPlugin', () => {
  it('transforms code blocks', async () => {
    const processor = unified()
      .use(remarkParse)
      .use(remarkMyPlugin)
    
    const input = '```my-lang\ncode\n```'
    const result = await processor.process(input)
    
    expect(result).toMatchSnapshot()
  })
})

Publishing Plugins

Share your plugin with others:
  1. Package Structure
my-doom-plugin/
├── src/
│   ├── index.ts
│   └── remark-plugin.ts
├── dist/
├── package.json
├── README.md
└── tsconfig.json
  1. package.json
{
  "name": "doom-plugin-my-feature",
  "version": "1.0.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "peerDependencies": {
    "@rspress/core": "^1.0.0"
  }
}
  1. Publish to npm
npm publish

Build docs developers (and LLMs) love