diff --git a/website/plugins/index.mjs b/website/plugins/index.mjs
index fe2e29758..eb5343110 100644
--- a/website/plugins/index.mjs
+++ b/website/plugins/index.mjs
@@ -1,6 +1,8 @@
+import remarkCustomAttrs from './remarkCustomAttrs.mjs'
 import remarkWrapSections from './remarkWrapSections.mjs'
 
 const remarkPlugins = [
+    remarkCustomAttrs,
     remarkWrapSections,
 ]
 
diff --git a/website/plugins/remarkCustomAttrs.mjs b/website/plugins/remarkCustomAttrs.mjs
index 59b539882..bd9a0baff 100644
--- a/website/plugins/remarkCustomAttrs.mjs
+++ b/website/plugins/remarkCustomAttrs.mjs
@@ -1,55 +1,71 @@
-/**
- * Simplified implementation of remark-attr that allows custom attributes on
- * nodes *inline* via the following syntax: {#some-id key="value"}. Extracting
- * them inline is slightly hackier (at least in this implementation), but it
- * makes the resulting markup valid and compatible with formatters like
- * Prettier, which do not allow additional content right below headlines.
- * Based on: https://github.com/arobase-che/remark-attr
- */
-
-const visit = require('unist-util-visit')
-const parseAttr = require('md-attr-parser')
-
-const defaultOptions = {
-    elements: ['heading', 'link'],
-}
-
-function remarkCustomAttrs(userOptions = {}) {
-    const options = Object.assign({}, defaultOptions, userOptions)
-    function transformer(tree) {
-        visit(tree, null, (node) => {
-            if (options.elements.includes(node.type)) {
-                if (
-                    node.children &&
-                    node.children.length &&
-                    node.children[0].type === 'text' &&
-                    node.children[0].value
-                ) {
-                    if (
-                        node.children.length > 1 &&
-                        node.children.every((el) => el.type === 'text')
-                    ) {
-                        // If headlines contain escaped characters, e.g.
-                        // Doc.\_\_init\_\_, it will be split into several nodes
-                        const mergedText = node.children.map((el) => el.value).join('')
-                        node.children[0].value = mergedText
-                        node.children = [node.children[0]]
-                    }
-                    const parsed = node.children[0].value.split(/\{(.*?)\}/)
-                    if (parsed.length >= 2 && parsed[1]) {
-                        const text = parsed[0].trim()
-                        const { prop } = parseAttr(parsed[1])
-                        const data = node.data || (node.data = {})
-                        const hProps = data.hProperties || (data.hProperties = {})
-                        node.data.hProperties = Object.assign({}, hProps, prop)
-                        node.children[0].value = text
-                    }
-                }
-            }
-        })
-        return tree
+const parseAttribute = (expression) => {
+    if (expression.type !== 'AssignmentExpression' || !expression.left || !expression.right) {
+        return
     }
-    return transformer
+
+    const { left, right } = expression
+
+    if (left.type !== 'Identifier' || right.type !== 'Literal' || !left.name || !right.value) {
+        return
+    }
+
+    return { type: 'mdxJsxAttribute', name: left.name, value: right.value }
 }
 
-module.exports = remarkCustomAttrs
+const handleNode = (node) => {
+    if (node.type === 'section' && node.children) {
+        return {
+            ...node,
+            children: node.children.map(handleNode),
+        }
+    }
+
+    if (node.type !== 'heading' || !node.children || node.children < 2) {
+        return node
+    }
+
+    const indexLast = node.children.length - 1
+    const lastNode = node.children[indexLast]
+
+    if (lastNode.type !== 'mdxTextExpression' || !lastNode.data || !lastNode.data.estree) {
+        return node
+    }
+
+    const { estree } = lastNode.data
+
+    if (estree.type !== 'Program' || !estree.body || estree.body.length <= 0 || !estree.body[0]) {
+        return node
+    }
+
+    const estreeBodyFirstNode = estree.body[0]
+
+    if (estreeBodyFirstNode.type !== 'ExpressionStatement' || !estreeBodyFirstNode.expression) {
+        return node
+    }
+
+    const statement = estreeBodyFirstNode.expression
+
+    const attributeExpressions = [
+        ...(statement.type === 'SequenceExpression' && statement.expressions
+            ? statement.expressions
+            : []),
+        ...(statement.type === 'AssignmentExpression' ? [statement] : []),
+    ]
+
+    // This replaces the markdown heading with a JSX element
+    return {
+        type: 'mdxJsxFlowElement',
+        name: `h${node.depth}`,
+        attributes: attributeExpressions.map(parseAttribute),
+        children: [node.children[0]],
+    }
+}
+
+const parseAstTree = (markdownAST) => ({
+    ...markdownAST,
+    children: markdownAST.children.map(handleNode),
+})
+
+const remarkCustomAttrs = () => parseAstTree
+
+export default remarkCustomAttrs