From 793490a0bdaa7180a655dca7afe5c74cdb9524b4 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Thu, 10 Sep 2026 10:25:42 -0700 Subject: [PATCH 1/5] chore(mdx): enable the v4 strict MDX flag --- docusaurus.config.js | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/docusaurus.config.js b/docusaurus.config.js index 6d22e46dfc..f020c84229 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -46,18 +46,27 @@ module.exports = { }, }, onBrokenLinks: 'warn', - /** - * Docusaurus Faster replaces the Webpack/Babel/Terser toolchain with - * Rspack/SWC/Lightning CSS, which cuts build times and memory usage on a - * site with this many versioned pages. It becomes the default in v4. - * - * `removeLegacyPostBuildHeadAttribute` is required by the `ssgWorkerThreads` - * part of `faster`, so it has to be enabled alongside it. - */ future: { v4: { + /** + * Turns off the MDX v1 shims for comments, admonition titles and + * heading ids. Every page uses the native syntax, so the shims are + * no-ops, and disabling them means a page that reintroduces the old + * syntax fails the build now rather than on the upgrade. It becomes + * the default in v4. + */ + mdx1CompatDisabledByDefault: true, + /** + * Required by the `ssgWorkerThreads` part of `faster`, so it has to + * be enabled alongside it. It becomes the default in v4. + */ removeLegacyPostBuildHeadAttribute: true, }, + /** + * Replaces the Webpack/Babel/Terser toolchain with Rspack/SWC/Lightning + * CSS, which cuts build times and memory usage on a site with this many + * versioned pages. It becomes the default in v4. + */ faster: true, }, markdown: { From 3fb56b054fb9d71c2fbe13117033061b6ad1eb60 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Thu, 10 Sep 2026 11:29:58 -0700 Subject: [PATCH 2/5] chore(ci): fail on admonition titles using MDX v1 syntax --- .github/workflows/CI.yml | 1 + .../actions/check-admonitions/action.yml | 84 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 .github/workflows/actions/check-admonitions/action.yml diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index e00f7ba81a..308663ffe0 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -33,6 +33,7 @@ jobs: run: npm run typecheck - name: 🔤 Spell Check run: npm run spellcheck + - uses: ./.github/workflows/actions/check-admonitions - uses: ./.github/workflows/actions/check-translations # Lint and spell check changes should be pushed # to the branch before the branch is merge eligible. diff --git a/.github/workflows/actions/check-admonitions/action.yml b/.github/workflows/actions/check-admonitions/action.yml new file mode 100644 index 0000000000..96668222d9 --- /dev/null +++ b/.github/workflows/actions/check-admonitions/action.yml @@ -0,0 +1,84 @@ +name: 'Check Admonitions' +description: 'Fails on admonition titles using the MDX v1 syntax' +runs: + using: 'composite' + steps: + # `:::note My Title` was MDX v1 syntax that Docusaurus rewrote for us. + # With `future.v4.mdx1CompatDisabledByDefault` that shim is off, and + # the block `:::note My Title` stops being a directive: it renders as + # literal `:::note` text on the page, with no warning and a successful + # build. + # + # The other MDX v1 forms, HTML comments and `{#heading-ids}`, fail the + # build on their own, so this only covers the one that fails silently. + # + # Only the files the pull request touches are checked, so an existing + # page is never anyone else's problem to fix. + # + # The event payload has no file list, so the changed files come from a + # diff. The checkout is shallow and the base commit is fetched here + # rather than through `fetch-depth` on the checkout, which would pull + # the full history for every step in the job just to serve this one. + - name: 🔎 Check Admonitions + shell: bash + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + CHANGED_FILES: ${{ runner.temp }}/changed-files.txt + run: | + git fetch --no-tags --depth=1 origin "$BASE_SHA" + git diff --name-only --diff-filter=ACMR "$BASE_SHA" HEAD > "$CHANGED_FILES" + node <<'JS' + const { readFileSync } = require('fs'); + + // Files Docusaurus compiles as MDX. Under `static/usage` that is + // only the `index` files; the rest are raw code samples. + const compiled = (f) => + !f.endsWith('README.md') && + ((/^(docs|versioned_docs|src\/pages)\//.test(f) && /\.mdx?$/.test(f)) || + (f.startsWith('static/usage/') && f.endsWith('.mdx'))); + + // Defaults from `@docusaurus/mdx-loader/lib/remark/admonitions`. + const keywords = + 'secondary|info|success|danger|note|tip|warning|important|caution'; + const legacyTitle = new RegExp( + `^[ \\t]*:::(?:${keywords})[ \\t]+(?!\\[)\\S[^\\n]*`, + 'gm' + ); + + // A `:::note` in a code sample is an example, not a directive. + // Blank out code samples with spaces, same length so line numbers + // still match. + const blank = (m) => m.replace(/[^\n]/g, ' '); + const mask = (t) => + t + .replace(/^([ \t]*)(`{3,}|~{3,})[^\n]*\n[\s\S]*?^\1\2[ \t]*$/gm, blank) + .replace(/(`+)(?:(?!\1)[\s\S])+?\1/g, blank); + + const files = readFileSync(process.env.CHANGED_FILES, 'utf8') + .split('\n') + .filter(compiled); + + const findings = []; + for (const file of files) { + const source = readFileSync(file, 'utf8'); + for (const m of mask(source).matchAll(legacyTitle)) { + findings.push({ + file, + line: source.slice(0, m.index).split('\n').length, + text: source.slice(m.index, m.index + m[0].length).trim(), + }); + } + } + + if (!findings.length) { + console.log(`No MDX v1 admonition titles in ${files.length} changed file(s).`); + process.exit(0); + } + + console.error(`Found ${findings.length} admonition title(s) using MDX v1 syntax.`); + console.error('Wrap the title in brackets, for example `:::note[My Title]`.\n'); + for (const { file, line, text } of findings) { + console.error(` ${file}:${line} ${text}`); + } + process.exit(1); + JS From f6140dde2b1956fd7e0174dd3e108591f7e1cefa Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Thu, 10 Sep 2026 11:33:34 -0700 Subject: [PATCH 3/5] chore(ci): skip the admonition check when no MDX files changed --- .github/workflows/actions/check-admonitions/action.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/actions/check-admonitions/action.yml b/.github/workflows/actions/check-admonitions/action.yml index 96668222d9..790b5a9af7 100644 --- a/.github/workflows/actions/check-admonitions/action.yml +++ b/.github/workflows/actions/check-admonitions/action.yml @@ -58,6 +58,11 @@ runs: .split('\n') .filter(compiled); + if (!files.length) { + console.log('No MDX files changed.'); + process.exit(0); + } + const findings = []; for (const file of files) { const source = readFileSync(file, 'utf8'); From 1ed22ba39b294a702ec35e58f722f27f35d15b6b Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Thu, 10 Sep 2026 11:37:34 -0700 Subject: [PATCH 4/5] test: prove the admonition check fires --- docs/reference/versioning.mdx | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/reference/versioning.mdx b/docs/reference/versioning.mdx index 51ee30f725..052ec8a536 100644 --- a/docs/reference/versioning.mdx +++ b/docs/reference/versioning.mdx @@ -20,3 +20,27 @@ A patch release will be published when bug fixes were included, but the API has For a list of all notable changes to Ionic please refer to the [changelog](https://github.com/ionic-team/ionic/blob/master/CHANGELOG.md). This contains an ordered list of all bug fixes and new features under each release. + +## CI Check Test + +Temporary. This branch exists only to prove the Check Admonitions action fires. + +:::note Legacy Title +Should be reported: MDX v1 syntax, renders as literal text. +::: + +:::tip[Bracketed Title] +Should not be reported: correct syntax. +::: + +:::warning +Should not be reported: no title. +::: + +```md +:::danger Inside A Fence +Should not be reported: an example in a code block. +::: +``` + +Inline too: `:::info Inside Code` From 272d233973fa606fa3861ad580e823510f35042c Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Thu, 10 Sep 2026 11:48:55 -0700 Subject: [PATCH 5/5] chore(ci): annotate admonition failures on the offending line --- .../workflows/actions/check-admonitions/action.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/actions/check-admonitions/action.yml b/.github/workflows/actions/check-admonitions/action.yml index 790b5a9af7..e3f4bf6948 100644 --- a/.github/workflows/actions/check-admonitions/action.yml +++ b/.github/workflows/actions/check-admonitions/action.yml @@ -25,7 +25,7 @@ runs: BASE_SHA: ${{ github.event.pull_request.base.sha }} CHANGED_FILES: ${{ runner.temp }}/changed-files.txt run: | - git fetch --no-tags --depth=1 origin "$BASE_SHA" + git fetch --quiet --no-tags --depth=1 origin "$BASE_SHA" git diff --name-only --diff-filter=ACMR "$BASE_SHA" HEAD > "$CHANGED_FILES" node <<'JS' const { readFileSync } = require('fs'); @@ -80,10 +80,13 @@ runs: process.exit(0); } - console.error(`Found ${findings.length} admonition title(s) using MDX v1 syntax.`); - console.error('Wrap the title in brackets, for example `:::note[My Title]`.\n'); + // `::error` puts each one on the offending line in the Files changed + // tab and prints it in red in the log, so the failure is not something + // to go hunting for. The message leads with the fix because the found + // text starts with `:::`, which reads badly right after the delimiter. for (const { file, line, text } of findings) { - console.error(` ${file}:${line} ${text}`); + const fix = 'Wrap the title in brackets, for example :::note[My Title]'; + console.log(`::error file=${file},line=${line},title=MDX v1 admonition title::${fix}%0A%0AFound: ${text}`); } process.exit(1); JS