.agents/skills/learn-site-structure/mdx-rules.md
Docusaurus uses MDX 3 for .mdx files. MDX is markdown plus
JSX, which means certain markdown text that is harmless in
plain GitHub-rendered markdown will break MDX parsing. Ingest
runs a battery of escape transformations to make source .md
content survive the conversion to .mdx.
Live in ${NETDATA_REPOS_DIR}/learn/ingest/ingest.py:1721-1799
(the _escape_mdx_braces function and adjacent transforms),
exhaustively tested in
${NETDATA_REPOS_DIR}/learn/test_escape_mdx_braces.py. Every
transform is applied to every published file via
sanitize_page (ingest.py:1765-1829), in this order:
<!-- -> ---, first --> -> --- (ingest.py:1779-1780).<!--unhideme and unhideme--> markers are stripped
(ingest.py:1783-1784).
<details><summary> newline fix<details><summary> -> <details>\n<summary> (and the
<details open> variant) for MDX 3 compatibility
(ingest.py:1787-1788). Without this fix, the inline form
breaks the parser.
Caveat: only the two literal forms it knows are fixed. Any
other variant (e.g. <details class="x"><summary>) breaks
MDX rendering silently.
_escape_mdx_bracesEscape every bare { outside fenced/inline code:
... ``` blocks (DOTALL
match).`...` (no newlines, no nested
backticks).^import ...$ lines (MDX ESM destructuring
syntax).^export (default|function|const|let|var|{) ...$
lines (regex
export\s+(?:default|function|const|let|var|\{)).{ (not already preceded by \) with
\{.style=\{\{ back to style={{ because that's valid
JSX.Bash export VAR=... lines are NOT preserved as ESM. The
export regex requires default, function, const, let,
var, or { after export. Bash-style export NETDATA_FOO=bar
passes through escape unchanged because it has no {. If a
bash export contained {, it would get escaped (probably what
you want).
<= -> \<=%< -> %\<<-> -> \<->(ingest.py:1792-1794). MDX would otherwise try to parse
these as JSX tags. Note these are exact-substring rules;
near-variants like < = (with space) or <----> (multi-dash)
are NOT covered. See pitfalls-and-gotchas.md.
<word> placeholders in prose (e.g. <service-name>,
<scope>, <app>). MDX 3 parses anything that lexically
looks like an open tag and then expects a matching close tag.
Without one, the build fails with
Expected a closing tag for \<word>` ... before the end of `paragraph``.< followed by a digit (e.g. <100 minutes,
<5 seconds). MDX rejects this with
Unexpected character '1' (U+0031) before name, expected a character that can start a name, such as a letter, $, or _.
This pattern is common in lists describing thresholds.Type<param> Rust/C++/Java generic syntax (e.g.
Vec<u32>, HashMap<String, Vec<u8>>, unique_ptr<T>).
Same JSX-tag issue.The safe options, in order of preference:
\<service-name>`, `Vec<u32>`, `<APP>`` all survive intact. This is
the standard fix for our Netdata integration content.< 100 minutes
becomes under 100 minutes. Often clearer than the
original anyway.< -- \<word>. Works but
uglier than backticks. Use only when the < must remain
visibly a less-than operator, not a placeholder.These three were exercised in the netflow-plugin docs
(2026-05-07 ingest preview deploy failure):
docs/network-flows/retention-querying.md had <100 minutes,
fixed by rephrasing; aws_ip_ranges.md, gcp_ip_ranges.md,
and generic_json-over-http_ipam.md (generated from
metadata.yaml) had <service-name>, <scope>, <app>,
fixed by wrapping the placeholders in backticks at the
metadata.yaml source.
Converted to markdown links (ingest.py:1797-1799):
<https://...> -> [https://...](https://...)<http://...> -> [http://...](http://...)<[email protected]> -> [[email protected]](mailto:[email protected])So <...> used as a "I want this rendered as a link" markdown
shortcut works correctly through ingest.
meta_yaml rewriteIf meta_yaml: "<url>" is present in the file, the file is
treated as an integration. meta_yaml: is removed and
custom_edit_url is rewritten to that URL
(ingest.py:1801-1808). Silent rewrite -- any file with that
key triggers it.
Integration files (INTEGRATION_MARKER present) get
`` annotated
with data-integration-logo, data-logo-contrast-light,
data-logo-contrast-dark, data-logo-contrast-confidence
after a fetch+luminance analysis
(_annotate_integration_logo_tags and _analyze_remote_logo,
ingest.py:1647-1718). Used by Grid_integrations and the
dashboard theme to add a subtle glow on low-contrast logos.
Lines starting with [![analytics] are dropped
(ingest.py:1816-1817). These are leftover GitHub
README-style tracking pixels that don't make sense on Learn.
The escape rules cover the most common breakage patterns:
| Pattern | Survives? | Why |
|---|---|---|
{word} outside code | escaped to \{word} | rule 4 |
${expr} | escaped to \${expr} | rule 4 |
style={{ }} JSX | yes (un-escaped) | rule 4 restoration |
| Already-escaped braces | unchanged | rule 4 idempotent |
| Fenced or inline code | unchanged | rule 4 preservation |
MDX import/export ESM | unchanged | rule 4 preservation |
Tables with { cells | escaped per cell | rule 4 |
<=, %<, <-> exact | escaped | rule 5 |
< = (with space) | NOT covered | rule 5 is exact-substring |
<----> (long arrow) | NOT covered | rule 5 is exact-substring |
<htmltag> body content | NOT escaped | breaks MDX unless wrapped in code |
<word> placeholders in prose | NOT escaped | breaks MDX; wrap in backticks or rephrase |
< + digit (<100, <5s) | NOT escaped | breaks MDX; rephrase as "under N" or escape |
Vec<u32>, HashMap<K,V> generics | NOT escaped | breaks MDX; wrap in backticks |
<details> inline summary | fixed | rule 3 |
<details class="x"> | NOT fixed | rule 3 only knows two forms |
} closing brace alone | NOT escaped | rule 4 only escapes { |
Bare URL in <...> | converted to MD link | rule 6 |
${NETDATA_REPOS_DIR}/learn/test_escape_mdx_braces.py:74-377
exercises:
{word}, ${expr} templates;{{double}} braces;style={{ }} JSX (preserved);import/export ESM lines;export lines (currently pass through unchanged
because they have no braces);{} content;{outer{inner}};{;};{;{attribute_name} patterns;Mermaid diagrams are enabled at the markdown level in
docusaurus.config.js:26-30. The escape rules preserve
fenced code blocks, so ```mermaid ... ``` blocks survive
intact.
.md files safely.mdx,
not in .md; ingest converts to .mdx).style={{ ... }} JSX (preserved).mermaid ```.<details> and <summary> with newlines between them.<htmltag> in body content -- always wrap in code (fenced
or inline).<-> with extra dashes
(<-->, <--->).< or < with spaces around the <.docusaurus.config.js:22: onBrokenLinks: 'warn'. Broken
links never fail the Docusaurus build at the parse level; they
only get caught by the ingest's pre-build link checker
(--fail-links) and the daily 404 sweep
(daily-learn-link-check.yml). So a broken link in your .md
will silently make it to production unless caught by one of
those two gates.