Plugin Remark Replace Words
This remark/MDX plugin automatically replaces certain words in your .mdx/.md content with other text, an icon, or even a React component (e.g., Tooltip). It’s ideal for keeping terminology consistent, adding contextual help, or “styling” recurring keywords.
The plugin walks Markdown/MDX text nodes, skips certain parents (links, headings, code…), merges all
+ the current section (blog
/pages
/docs
), sorts keys by decreasing length, then replaces with plain text or MDX components. Matching is Unicode‑safe to avoid false positives inside words.
Combined with a Tooltip
component, this plugin lets you build a real interactive glossary: authors write plain Markdown, and the plugin replaces terms with rich components (tooltips, icons, buttons) without extra effort.
Why this plugin?
- Standardize brand or team terms (e.g., product names)
- Automatically substitute words or components
- Centralize replacement rules in a simple per‑section JSON file (blog, pages, docs)
- Keep Markdown/MDX clean and readable, without intrusive tags or components
- Easier maintenance: a change in JSON propagates everywhere
- And why not?
Plugin code
Create a folder plugins/remark-replace-words
in your Docusaurus project, with a file: index.js
(the plugin).
import { visit } from 'unist-util-visit';
import mapping from './replacements.json' with { type: "json" };
const DEBUG = true;
const stats = {};
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function isForbiddenParent(parent, replacementComponentNames) {
if (!parent) return false;
if (
parent.type === 'link' ||
parent.type === 'linkReference' ||
parent.type === 'definition' ||
parent.type === 'code' ||
parent.type === 'inlineCode' ||
parent.type === 'heading' ||
parent.type === 'image' ||
parent.type === 'imageReference'
) {
return true;
}
if (
(parent.type === 'mdxJsxFlowElement' || parent.type === 'mdxJsxTextElement') && (
parent.name === 'a' ||
replacementComponentNames.has(parent.name) )
) {
return true;
}
return false;
}
export default function remarkReplaceFromJson() {
return (tree, file) => {
const filePath = (file.path || '').replace(/\\/g, '/');
let type = 'pages';
if (filePath.includes('/blog/')) type = 'blog';
else if (filePath.includes('/docs/')) type = 'docs';
else if (filePath.includes('/pages/')) type = 'pages';
const allMapping = mapping.all || {};
const typeMapping = mapping[type] || {};
const mergedMapping = { ...allMapping, ...typeMapping };
const replacementComponentNames = new Set(
Object.values(mergedMapping)
.map((c) => c.component)
.filter(Boolean)
);
const entries = Object.entries(mergedMapping).sort((a, b) => b[0].length - a[0].length);
visit(tree, 'text', (node, index, parent) => {
if (isForbiddenParent(parent, replacementComponentNames)) return;
if (!node.value || typeof node.value !== 'string') return;
node.value = node.value.normalize('NFC');
let fragments = [{ type: 'text', value: node.value }];
let replaced = false;
for (const [word, conf] of entries) {
if (!word) continue;
const safe = escapeRegex(word);
const regex = new RegExp(
`(?<![\\p{L}\\p{N}_/"'’-])${safe}(?!(?:[\\p{L}\\p{N}_/"'’-]|\\.[\\p{L}\\p{N}]))`,
'giu'
);
fragments = fragments.flatMap((frag) => {
if (frag.type !== 'text') return [frag];
if (!regex.test(frag.value)) return [frag];
regex.lastIndex = 0;
const parts = frag.value.split(regex);
const matches = frag.value.match(regex) || [];
if (!matches.length) return [frag];
replaced = true;
if (DEBUG) {
stats[filePath] = stats[filePath] || {};
stats[filePath][word] = (stats[filePath][word] || 0) + matches.length;
}
const newNodes = [];
parts.forEach((part, i) => {
if (part) newNodes.push({ type: 'text', value: part });
if (i < matches.length) {
const compName = conf.component;
const replacementLabel = conf.children || matches[i];
if (compName) {
const isInline =
parent.type === 'paragraph' ||
parent.type === 'mdxJsxFlowElement' ||
parent.type === 'emphasis' ||
parent.type === 'strong' ||
parent.type === 'delete' ||
parent.type === 'listItem';
const nodeType = isInline ? 'mdxJsxTextElement' : 'mdxJsxFlowElement';
newNodes.push({
type: nodeType,
name: compName,
attributes: Object.entries(conf.props || {}).map(([key, value]) => ({
type: 'mdxJsxAttribute',
name: key,
value,
})),
children: replacementLabel ? [{ type: 'text', value: replacementLabel }] : [],
});
} else {
newNodes.push({ type: 'text', value: replacementLabel });
}
}
});
return newNodes;
});
}
if (replaced) {
parent.children.splice(index, 1, ...fragments);
}
});
if (DEBUG && Object.keys(stats).length > 0 && !global.__remarkReplaceWordsReported) {
global.__remarkReplaceWordsReported = true;
setTimeout(() => {
global.__remarkReplaceWordsReported = false;
const typeTotals = { blog: {}, pages: {}, docs: {}, all: {} };
for (const [file, words] of Object.entries(stats)) {
let type = 'pages';
const normalized = file.replace(/\\/g, '/');
if (normalized.includes('/blog/')) type = 'blog';
else if (normalized.includes('/docs/')) type = 'docs';
else if (normalized.includes('/pages/')) type = 'pages';
for (const [word, count] of Object.entries(words)) {
if (mapping.all && Object.prototype.hasOwnProperty.call(mapping.all, word)) {
typeTotals.all[word] = (typeTotals.all[word] || 0) + count;
} else {
typeTotals[type][word] = (typeTotals[type][word] || 0) + count;
}
}
}
console.log("\n=== Rapport de remplacements Remark ===");
for (const type of ['blog', 'pages', 'docs', 'all']) {
const words = typeTotals[type];
if (Object.keys(words).length > 0) {
console.log(`[${type}]`);
for (const [word, count] of Object.entries(words)) {
console.log(`- \"${word}\" remplacé ${count} fois`);
}
}
}
console.log("======================================\n");
}, 5000);
}
};
}
JSON configuration
In your plugins/remark-replace-words
folder, add a file: replacements.json
.
Replacements are driven by plugins/remark-replace-words/replacements.json
. It contains 4 top‑level keys:
all
: global replacements (all sections)blog
: replacements specific to blog postspages
: replacements for pagesdocs
: replacements for documentation
Each entry uses the word to detect as the key, and an object as the value:
component
(optional): name of the MDX component to inject (e.g., Tooltip, LogoIcon)props
(optional): props passed to the componentchildren
(optional): component’s child text; if missing, the matched word is reused
{
"all": {
"Giti": {
"component": "Tooltip",
"props": { "text": "DocuxLab" },
"children": "Global replacement"
},
"doculab": {
"component": "Tooltip",
"props": { "text": "dokidoki" },
"children": "Global replacement"
}
},
"blog": {
"kiki": {
"component": "Tooltip",
"props": { "text": "Appears at the bottom", "position": "bottom", "model": "teacher" },
"children": "my new word"
},
"schema-diagram": {
"component": "ImageOnClick",
"props": {
"imageUrl": "/img/remarkreplace.png",
"altText": "Architecture diagram",
"buttonName": "Click me"
}
},
"Cavo": {
"children": "grandpa Christophe"
},
"cta-docs": {
"component": "DocusaurusButton",
"props": { "to": "/img/remarkreplace.png", "label": "Read Docs" },
"children": "Read the documentation"
},
"nana": {
"component": "LogoIcon",
"props": { "name": "logos:css-3", "size": "124" }
},
"roki": {
"component": "LogoIcon",
"props": { "name": "docusaurus", "size": "124" }
},
"node": {
"component": "LogoIcon",
"props": { "name": "logos:nodejs-icon", "size": 64 }
}
},
"pages": {
"nana": {
"component": "LogoIcon",
"props": { "name": "ccs-3", "size": "124" }
},
"roki": {
"component": "LogoIcon",
"props": { "name": "docusaurus", "size": "124" }
},
"node": {
"component": "LogoIcon",
"props": { "name": "logos:nodejs-icon", "size": 64 }
},
"schema-diagram": {
"component": "ImageOnClick",
"props": {
"imageUrl": "/img/DocuxLab.png",
"altText": "Architecture diagram",
"buttonName": "View diagram"
}
}
},
"docs": {
"kiki": {
"component": "Tooltip",
"props": { "text": "Documentation" },
"children": "Docux Docs"
}
}
}
Installation and setup
The plugin is already present in this repo under plugins/remark-replace-words
. To enable it in Docusaurus, it’s referenced in docusaurus.config.js
at the Blog, Pages, and Docs presets level.
// ...imports
import remarkReplaceWords from "./plugins/remark-replace-words/index.js"
export default {
// ...
presets: [
[
'classic',
({
blog: {
remarkPlugins: [
// ...
[remarkReplaceWords, "blog"],
],
},
pages: {
remarkPlugins: [
// ...
[remarkReplaceWords, "pages"],
],
},
// If you have a Docs section
docs: {
remarkPlugins: [
// ...
[remarkReplaceWords, "docs"],
],
},
})
]
]
}
The second argument ("blog" | "pages" | "docs") selects a specific section in the configuration JSON. An all
block is also available for global replacements.
Strategy
- Section detection from the file path (
/blog/
,/pages/
,/docs/
), otherwise fallback topages
. - Mapping merge:
merged = { ...mapping.all, ...mapping[section] }
to support global then section‑specific rules. - Unicode NFC normalization to stabilize accents (é, è, ê, ô, ç, œ, ï, …).
- Ignored parents to avoid unwanted replacements:
- Markdown/MDX:
link
,linkReference
,definition
,code
,inlineCode
,heading
,image
,imageReference
,blockquote
,url
. - MDX JSX:
a
tag and any component already inserted by a previous replacement (prevents re‑traversal).
- Markdown/MDX:
- Pre‑collect replacement component names (Set) to detect them during the visit and short‑circuit.
- Sort replacement keys by decreasing length to prevent overlaps (longest first).
- Robust Unicode word‑boundary regex to avoid matching inside a word and when adjacent to
_ - / " ' ’
:(?<![\p{L}\p{N}_/"'’-])word(?![\p{L}\p{N}_/"'’-])
withgiu
flags.
- Replace by fragments: split the text and insert either plain text or an MDX element (
mdxJsxTextElement
inline,mdxJsxFlowElement
block) withname
,attributes
(props) andchildren
. children
choice: ifchildren
is defined in JSON, use it; otherwise reuse the captured value.- DEBUG report (optional): aggregate by type (
blog
,pages
,docs
,all
) and print once at the end of build/dev. - Performance goal: avoid replacements in undesired areas, reduce false positives with the Unicode regex, and limit re‑traversal via the Set and longest‑first sort.
Concrete replacement examples
Here are several use cases, based on this project’s JSON.
Word → Word (plain text)
- Key:
Cavo
in theblog
section:In a post, typing"Cavo": { "children": "grandpa Christophe" }
Rendered“Cavo” will render grandpa Christophe
Cavo
= grandpa Christophe
Word → Icon (LogoIcon)
-
Keys:
nana
androki
in thepages
section:"nana": { "component": "LogoIcon", "props": { "name": "ccs-3", "size": "124" } }
"roki": { "component": "LogoIcon", "props": { "name": "docusaurus", "size": "124" } }On an MDX page, typing “nana” will display the CSS‑3 icon and “roki” the Docusaurus icon.
Rendered: On an MDX page, typing “” will display the CSS‑3 icon and “” the Docusaurus icon.
Additional example with an explicit Iconify set:
"node": { "component": "LogoIcon", "props": { "name": "logos:nodejs", "size": 64 } }
Rendered
node
= icon of node.js technology
Word → Tooltip
- Keys:
Giti
andkiki
:"Giti": { "component": "Tooltip", "props": { "text": "DocuxLab" }, "children": "Global replacement" }
"kiki": { "component": "Tooltip", "props": { "text": "Documentation" }, "children": "Docux Docs" }Rendered: The word “DocuxLab” anywhere will be replaced byThe word “Giti” anywhere will be replaced by `<Tooltip text="DocuxLab">Global replacement</Tooltip>`. In docs, “kiki” becomes `<Tooltip text="Documentation">Docux Docs</Tooltip>`.
<Tooltip text="DocuxLab">Global replacement</Tooltip>
. In docs, “Appears at the bottom” becomes<Tooltip text="Documentation">Docux Docs</Tooltip>
.
Word → Clickable image (ImageOnClick)
ImageOnClick
is a local component that displays an image overlay on click.
In a post, typing “schema-diagram” will display a clickable link opening the image `/img/diagram.png` fullscreen.
Replacement example:
{
"pages": {
"schema-diagram": {
"component": "ImageOnClick",
"props": {
"imageUrl": "/img/rocket_1f680.gif",
"altText": "Architecture diagram",
"buttonName": "Click me"
}
}
}
}
On a page, typing Click me will display a clickable link opening the image /img/rocket_1f680.gif
fullscreen.
Important behaviors to know
- Ignored parents: no replacement inside links, headings, code blocks/inline code, images.
- Merged sections: for a blog file, merge
all
thenblog
(the latter wins on conflict). Same logic forpages
anddocs
. - Replacement order: sort by decreasing word length to avoid overlaps.
- Unicode boundaries: the regex uses lookarounds and Unicode classes to avoid matching substrings (e.g., don’t replace “TS” inside “intérêts”).
- Build report: with DEBUG on, a console report per section shows how many times each word was replaced.
Why MDX/MD instead of JSX/TSX pages?
File compatibility: the plugin works in both
.md
and.mdx
pages. Opinion: I chose to convert all my pages to.mdx
to guarantee consistent support for MDX components and the plugin everywhere. Handling.jsx
or.tsx
pages would require a different, more complex approach with a dedicated React component for pages.
- Remark/rehype pipeline: Remark plugins only run on Markdown/MDX content. JSX/TSX pages are plain React components compiled by Babel/TS and bypass the Markdown pipeline, so the plugin never “sees” their text.
- AST expectations: This plugin operates on the Markdown AST (mdast) text nodes. In JSX, text is split across JSXText/StringLiteral and mixed with elements/props. Safe replacement there would require a Babel/SWC transform or a runtime React tree walk, with high risk of touching code, props, or links.
- Word-boundaries and Unicode: The plugin relies on contiguous text to apply robust Unicode word-boundary regexes. In JSX, sentences are often fragmented across nodes/components, making accurate matching and ordering (inline vs block) unreliable.
- MDX is a perfect fit: MDX gives a Markdown-first AST where narrative text lives in text nodes, and it officially supports injecting MDX components as replacements—exactly what the plugin generates.
If you must keep JSX/TSX pages, possible alternatives:
- Build-time: write a Babel/SWC plugin that transforms JSXText/StringLiteral using the same rules (harder to maintain).
- Runtime: create a React wrapper that recursively walks children and replaces strings (perf and correctness caveats).
- Hybrid: wrap JSX pages with a thin MDX shell and keep textual content in MDX so the plugin can process it.
Example of the report log
When DEBUG
is enabled, the plugin prints a summary of replacements to the console after build/dev:
=== Remark replacements report ===
[blog]
- "Cavo" replaced 4 times
- "kiki" replaced 4 times
[all]
- "Giti" replaced 4 times
======================================
This article is part of the Docusaurus Plugins series:
- Plugin Remark Replace Words
Loading comments…
Related posts

Component Tooltip
September 21, 2025

Plugin Simple Analytics
September 14, 2025

Tutorial Docusaurus React Live
October 1, 2025