Use when creating Docusaurus plugins — write remark transformers for markdown AST, rehype transformers for HTML/HAST, lifecycle plugins that add routes/webpack config/global data via loadContent and contentLoaded, theme plugins and swizzled components, and content plugins for custom data sources. Triggers on tasks involving custom remark/rehype plugins, content plugins, theme plugins, or Docusaurus lifecycle hooks.
71
89%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Passed
No known issues
Remark plugins transform markdown content during the parsing phase, before it's converted to HTML. They operate on the MDAST (Markdown Abstract Syntax Tree).
%%term%% for glossary terms)// index.js - Main plugin file
const { visit } = require('unist-util-visit');
const fs = require('fs');
const path = require('path');
module.exports = function remarkCustomPlugin(options = {}) {
// Validate and process options
const {
pattern = /%%(.+?)%%/g,
dataFile = './data/terms.json',
componentName = 'CustomTooltip'
} = options;
// Load external data if needed
let glossaryData = {};
if (fs.existsSync(dataFile)) {
glossaryData = JSON.parse(fs.readFileSync(dataFile, 'utf-8'));
}
// Return the transformer function
return async function transformer(ast, vfile) {
const filePath = vfile.path || '';
// Visit specific node types
visit(ast, 'text', (node, index, parent) => {
const matches = [...node.value.matchAll(pattern)];
if (matches.length === 0) return;
// Build replacement nodes
const newNodes = [];
let lastIndex = 0;
matches.forEach((match) => {
const [fullMatch, termKey] = match;
const startIndex = match.index;
// Add text before match
if (startIndex > lastIndex) {
newNodes.push({
type: 'text',
value: node.value.slice(lastIndex, startIndex)
});
}
// Add custom component node
const termData = glossaryData[termKey];
if (termData) {
newNodes.push({
type: 'jsx',
value: `<${componentName} term="${termKey}" tooltip="${termData.tooltip}">${termData.display}</${componentName}>`
});
} else {
// Fallback if term not found
newNodes.push({
type: 'text',
value: fullMatch
});
}
lastIndex = startIndex + fullMatch.length;
});
// Add remaining text
if (lastIndex < node.value.length) {
newNodes.push({
type: 'text',
value: node.value.slice(lastIndex)
});
}
// Replace the original node
parent.children.splice(index, 1, ...newNodes);
});
// Visit links
visit(ast, 'link', (node) => {
if (node.url.endsWith('.md')) {
// Transform internal markdown links
node.data = node.data || {};
node.data.hProperties = {
className: 'internal-link',
'data-internal': true
};
}
});
return ast;
};
};// Basic configuration
module.exports = {
presets: [
[
'@docusaurus/preset-classic',
{
docs: {
remarkPlugins: [
require('./plugins/my-remark-plugin')
]
}
}
]
]
};
// With options
module.exports = {
presets: [
[
'@docusaurus/preset-classic',
{
docs: {
remarkPlugins: [
[require('./plugins/my-remark-plugin'), {
pattern: /\[\[(.+?)\]\]/g,
dataFile: './glossary.json',
componentName: 'GlossaryTerm'
}]
]
}
}
]
]
};
// Execute BEFORE default Docusaurus plugins
module.exports = {
presets: [
[
'@docusaurus/preset-classic',
{
docs: {
beforeDefaultRemarkPlugins: [
require('./plugins/my-remark-plugin')
]
}
}
]
]
};{
type: 'text',
value: 'Some text content'
}{
type: 'link',
url: 'https://example.com',
children: [{ type: 'text', value: 'Link text' }]
}{
type: 'paragraph',
children: [...]
}{
type: 'jsx',
value: '<CustomComponent prop="value">Content</CustomComponent>'
}{
type: 'heading',
depth: 2, // h2
children: [{ type: 'text', value: 'Heading text' }]
}const { visit } = require('unist-util-visit');
// Visit all nodes of a specific type
visit(ast, 'link', (node) => {
console.log(node.url);
});
// Visit multiple types
visit(ast, ['link', 'image'], (node) => {
console.log(node.type, node.url);
});
// Visit with index and parent access
visit(ast, 'text', (node, index, parent) => {
// Modify parent.children to replace nodes
parent.children[index] = newNode;
});
// Visit all nodes
visit(ast, (node) => {
if (node.type === 'link' && node.url.startsWith('http')) {
// Process external links
}
});Based on docusaurus-plugin-glossary pattern:
// plugins/glossary-plugin.js
const { visit } = require('unist-util-visit');
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
module.exports = function glossaryPlugin(options = {}) {
const {
termsDir = './docs/terms',
docsDir = './docs',
glossaryFilepath = './docs/glossary.md'
} = options;
// Load all term files
const loadTerms = () => {
const terms = {};
const termFiles = fs.readdirSync(termsDir);
termFiles.forEach((file) => {
if (!file.endsWith('.md')) return;
const content = fs.readFileSync(path.join(termsDir, file), 'utf-8');
const [, frontmatter, body] = content.match(/^---\n([\s\S]+?)\n---\n([\s\S]*)$/);
const meta = yaml.load(frontmatter);
terms[meta.id] = {
title: meta.title,
hoverText: meta.hoverText || body.slice(0, 200),
path: `terms/${file.replace('.md', '')}`
};
});
return terms;
};
return function transformer(ast, vfile) {
const terms = loadTerms();
// Convert [[term]] syntax to tooltipped links
visit(ast, 'text', (node, index, parent) => {
const matches = [...node.value.matchAll(/\[\[(.+?)\]\]/g)];
if (matches.length === 0) return;
const newNodes = [];
let lastIndex = 0;
matches.forEach((match) => {
const [fullMatch, termKey] = match;
const term = terms[termKey];
if (!term) {
console.warn(`Term not found: ${termKey}`);
return;
}
// Add text before match
if (match.index > lastIndex) {
newNodes.push({
type: 'text',
value: node.value.slice(lastIndex, match.index)
});
}
// Add glossary link with tooltip
newNodes.push({
type: 'jsx',
value: `<GlossaryTerm term="${termKey}" tooltip="${term.hoverText}" href="/${term.path}">${term.title}</GlossaryTerm>`
});
lastIndex = match.index + fullMatch.length;
});
// Remaining text
if (lastIndex < node.value.length) {
newNodes.push({
type: 'text',
value: node.value.slice(lastIndex)
});
}
parent.children.splice(index, 1, ...newNodes);
});
return ast;
};
};{
"dependencies": {
"unist-util-visit": "^4.0.0"
},
"peerDependencies": {
"@docusaurus/core": "^3.0.0",
"remark": "^13.0.0"
},
"devDependencies": {
"@types/unist": "^2.0.0",
"jest": "^29.0.0"
}
}// index.d.ts
import { Plugin } from 'unified';
import { Root } from 'mdast';
export interface RemarkPluginOptions {
pattern?: RegExp;
dataFile?: string;
componentName?: string;
termsDir?: string;
docsDir?: string;
glossaryFilepath?: string;
}
declare const remarkPlugin: Plugin<[RemarkPluginOptions?], Root>;
export default remarkPlugin;// __tests__/plugin.test.js
const remark = require('remark');
const remarkMdx = require('remark-mdx');
const glossaryPlugin = require('../index');
describe('Glossary Plugin', () => {
const processor = remark()
.use(remarkMdx)
.use(glossaryPlugin, {
termsDir: './__fixtures__/terms'
});
it('transforms glossary syntax', async () => {
const input = 'This is a [[test-term]] example.';
const result = await processor.process(input);
expect(result.toString()).toContain('<GlossaryTerm');
expect(result.toString()).toContain('test-term');
});
it('handles missing terms gracefully', async () => {
const input = 'This is a [[missing-term]] example.';
const result = await processor.process(input);
// Should leave unmatched terms as-is or show warning
expect(result.toString()).toBeTruthy();
});
});unist-util-visit for traversalposition property for error reportingjsx integrates with MDXasync/await when reading filesTransform text patterns into links automatically.
Add markdown extensions like ::note[text] or %%term%%.
Generate tables, lists, or summaries from markdown structure.
Check internal links exist, add attributes to external links.
Add frontmatter data as HTML attributes or classes.
// Add logging to see AST structure
visit(ast, (node) => {
console.log(JSON.stringify(node, null, 2));
});
// Log only specific types
visit(ast, 'link', (node) => {
console.log('Link:', node.url);
});Use online AST explorers: