diff --git a/Makefile b/Makefile
index a3af8f52..f08e3765 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,7 @@
-.PHONY: checks test phpcs
+.PHONY: checks test phpcs mathjax
+
+MATHJAX_VERSION_LOCAL ?= 4.1.3
+MATHJAX_VERSION_CDN ?= 4
checks: test phpcs ## Everything CI runs before merging (needs `composer install`, PHP >= 8.2)
@@ -7,3 +10,6 @@ test: ## Run the pure-PHP test suites
phpcs: ## parallel-lint + minus-x + phpcs against the MediaWiki coding standard
composer test
+
+mathjax: ## Pin the local MathJax submodule + CDN major version, e.g. `make mathjax MATHJAX_VERSION_LOCAL=4.1.3 MATHJAX_VERSION_CDN=4`
+ hack/mathjax.sh $(MATHJAX_VERSION_LOCAL) $(MATHJAX_VERSION_CDN)
diff --git a/extension.json b/extension.json
index ae36dff7..1b026bb0 100644
--- a/extension.json
+++ b/extension.json
@@ -1,6 +1,6 @@
{
"name": "SimpleMathJax",
- "version": "0.11.0",
+ "version": "0.12.0",
"author": "jmnote",
"url": "https://www.mediawiki.org/wiki/Extension:SimpleMathJax",
"description": "render TeX between and ",
diff --git a/hack/mathjax.sh b/hack/mathjax.sh
new file mode 100755
index 00000000..fd98ac6b
--- /dev/null
+++ b/hack/mathjax.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+# Pins the resources/MathJax submodule to a tag and updates the CDN URL's
+# major version in resources/ext.SimpleMathJax.js. The two are versioned
+# separately on purpose: the local copy is pinned to an exact tag, while
+# the CDN URL only pins a major version (jsdelivr resolves it to the
+# latest matching release).
+#
+# Usage: hack/mathjax.sh
+# e.g. hack/mathjax.sh 4.1.3 4
+set -euo pipefail
+cd "$(dirname "$0")/.."
+
+MATHJAX_DIR="resources/MathJax"
+JS_FILE="resources/ext.SimpleMathJax.js"
+LOCAL_VERSION="${1:-}"
+CDN_VERSION="${2:-}"
+
+if [ -z "$LOCAL_VERSION" ] || [ -z "$CDN_VERSION" ]; then
+ echo "Usage: hack/mathjax.sh (e.g. hack/mathjax.sh 4.1.3 4)" >&2
+ exit 1
+fi
+
+git submodule update --init "$MATHJAX_DIR"
+(cd "$MATHJAX_DIR" && git fetch --tags origin && git checkout "tags/$LOCAL_VERSION")
+git add "$MATHJAX_DIR"
+
+sed -i -E "s#(cdn\.jsdelivr\.net/npm/mathjax@)[^/]+#\1${CDN_VERSION}#" "$JS_FILE"
+git add "$JS_FILE"
+
+echo "==> Pinned $MATHJAX_DIR to $LOCAL_VERSION and the CDN URL to mathjax@$CDN_VERSION."
+echo "==> Review with 'git status', then commit the bump."
diff --git a/resources/MathJax b/resources/MathJax
index 600692ad..0bfa5806 160000
--- a/resources/MathJax
+++ b/resources/MathJax
@@ -1 +1 @@
-Subproject commit 600692ad9d3552cc25f85510d5797bc942ecc9f7
+Subproject commit 0bfa580685ca72e4a3a37657ccd754a65c1a42c6
diff --git a/resources/ext.SimpleMathJax.js b/resources/ext.SimpleMathJax.js
index 747b7908..fab0c493 100644
--- a/resources/ext.SimpleMathJax.js
+++ b/resources/ext.SimpleMathJax.js
@@ -137,8 +137,8 @@ window.MathJax = {
(function () {
var script = document.createElement('script');
script.src = mw.config.get('wgSmjUseCdn')
- ? 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js'
- : mw.config.get('wgExtensionAssetsPath') + '/SimpleMathJax/resources/MathJax/es5/tex-chtml.js';
+ ? 'https://cdn.jsdelivr.net/npm/mathjax@4/tex-chtml.js'
+ : mw.config.get('wgExtensionAssetsPath') + '/SimpleMathJax/resources/MathJax/tex-chtml.js';
script.async = true;
document.head.appendChild(script);
})();