Moodle 5.3 Classic theme serves an empty stylesheet: 'Undefined variable $mds-spacing-md'
After an upgrade to Moodle 5.3, every page on the Classic theme lost its layout. The theme’s stylesheet came back like this:
$ curl -s -o /dev/null -w '%{http_code} %{size_download}\n' \
'https://moodle.example.com/theme/styles_debug.php?theme=classic&type=scss'
200 0
That’s a 200 with an empty body, and nothing in the logs. With debugging forced to developer level, the SCSS compiler said:
Error while compiling SCSS: Undefined variable $mds-spacing-md: /opt/moodle/public/theme/classic/scss/../../boost/scss/moodle/course.scss on line 132, at column 17
Classic imports Boost’s SCSS, and in 5.3 that SCSS uses the new design system’s tokens. Classic never imports them. One @import fixes it.
The setup #
- Moodle 5.3.0, PHP 8.4.27, in a Docker image of our own, upgraded from 5.0
- The site’s theme is Classic. It left core in 5.3, so the image installs it from moodlehq/moodle-theme_classic
(at commit
363a179, the only one when I checked) - Theme designer mode was on, so pages loaded the theme through
styles_debug.php
I rehearsed the upgrade on a copy of the production database. Every URL I tried returned 200, and admin/cli/checks.php reported all checks OK. On production, the pages had no layout.
Two things that looked like the cause #
The browser console had two errors:
[Error] Failed to load resource: the server responded with a status of 404 (Not Found) (client.bundle.js.map, line 0)
[Error] Failed to load resource: the server responded with a status of 404 (Not Found) (react.bundle.mjs.map, line 0)
Those are source maps. The browser asks for them only for its developer tools, and Moodle doesn’t ship them. They don’t affect the layout.
The import map had a few 404s as well, for entries like /core/esm/<rev>/bootstrap/. Those entries end in a slash because they map a whole prefix, so the bare URL has nothing to serve. The modules under them loaded.
The problem showed up when I fetched every stylesheet the front page links to and printed its size:
curl -s https://moodle.example.com/ -o index.html
grep -oE '<link[^>]+stylesheet[^>]*>' index.html \
| grep -oE 'href="[^"]+"' | sed 's/href="//; s/"$//; s/&/\&/g' \
| while read -r u; do
printf '%s %s\n' "$(curl -s "$u" | wc -c)" "$u"
done
The plugin stylesheets had a few kilobytes each. The theme’s own, type=scss, had zero bytes.
Getting the real error #
Moodle catches the compiler’s exception and passes the message to debugging() at developer level, so at the usual debug setting you get nothing at all. You don’t have to change the site’s settings to see it. Compile the theme from the CLI with debugging switched on for that one process (run it as the web server’s user and adjust the path to config.php):
sudo -u www-data php -r '
define("CLI_SCRIPT", 1);
require "/opt/moodle/config.php";
$CFG->debug = E_ALL;
$CFG->debugdisplay = 1;
$t = \core\output\theme_config::load("classic");
$m = new ReflectionMethod($t, "get_css_content_from_scss");
$c = $m->invoke($t, false);
var_dump(is_string($c) ? strlen($c) : $c);
'
get_css_content_from_scss isn’t public, hence the reflection. It returns false when the compile fails, and the error comes with the import chain:
++ Error while compiling SCSS: Undefined variable $mds-spacing-md: /opt/moodle/public/theme/classic/scss/../../boost/scss/moodle/course.scss on line 132, at column 17
Call Stack:
#0 import /opt/moodle/public/theme/classic/scss/../../boost/scss/moodle/course.scss /opt/moodle/public/theme/classic/scss/../../boost/scss/moodle.scss on line 13
#1 import /opt/moodle/public/theme/classic/scss/../../boost/scss/moodle.scss /opt/moodle/public/theme/classic/scss/moodle.scss on line 2
#2 import /opt/moodle/public/theme/classic/scss/moodle.scss (unknown file) on line 127 ++
Why #
Boost’s default preset, theme/boost/scss/preset/default.scss, starts with:
// Import Moodle Design System tokens.
// Must be imported before Bootstrap so that the design system tokens can be used to override Bootstrap variables.
@import "design-system";
design-system.scss pulls in the $mds-* tokens. Classic’s presets import fontawesome, bootstrap and moodle, but not design-system, and Classic’s moodle.scss imports Boost’s. So Boost’s course.scss uses $mds-spacing-md, and nothing has defined it.
The fix #
Add the import to the top of theme/classic/scss/classic/pre.scss. Classic’s lib.php puts that file in front of the preset, so this covers both presets:
pre=/opt/moodle/public/theme/classic/scss/classic/pre.scss
{ printf '@import "../../boost/scss/design-system";\n\n'; cat "$pre"; } > /tmp/pre.scss
mv /tmp/pre.scss "$pre"
sudo -u www-data php /opt/moodle/admin/cli/purge_caches.php --theme
The path resolves from theme/classic/scss/, as the call stack above shows. After the purge, the same CLI compile returned 1,218,070 characters instead of false, and styles_debug.php sent 1,557,703 bytes.
If Classic is part of your build, as here, apply the patch in the build. A fix made inside a running container is gone the next time the container is recreated.
Once the layout was back, I turned theme designer mode off (admin/cli/cfg.php --name=themedesignermode --set=0). It recompiles the theme on every page view: here, about 0.8 s and 112 stylesheet requests per page.
Takeaway #
A Moodle theme that fails to compile still answers 200, just with no CSS, so upgrade checks that look only at status codes pass right over it.
Before your next Moodle upgrade, add the theme’s compiled CSS size to your checklist: anything near zero is a failed compile.