Skip to content

Frontend Build & Theme Bridge ​

Reference for how a compiled frontend (React and Inertia, built by the consumer) is hosted on a Moodle page: the adapter's host-page bridge, the theme-color bridge, and the consumer-side AMD build. Verified against middag-php-moodle main@6aae04d on 2026-11-04.

These mechanisms adapt to Moodle itself (Moodle's page pipeline, theme settings and AMD loader), which is why they live in the Moodle adapter and the consumer rather than in middag-io/ui. The page contract the frontend renders is middag-io/ui's concern (see UI Wire Contract); the product shell and visual identity belong to the consuming product.

Host-page bridge (adapter) ​

Http\Inertia\MoodleInertiaBootstrap::registerHooks() wires the framework's Inertia runtime into Moodle. The conventions are per component and product-agnostic:

ConventionValue
AMD entry module{component}/inertia_app, called with $PAGE->requires->js_call_amd($module, 'init'); Moodle resolves {component}/* to the plugin's amd/build/
Version hash source{plugin dir}/amd/build/inertia_app.min.js (InertiaVersionManager::setBundlePath())
Stylesheetsstyles/middag-app.css and styles/isolation.css, each added only if the file exists
Mount point<div id="{appId}" class="middag-root"> plus <script type="application/json" data-page="{appId}"> (Inertia v3 bootstrap); the id comes from the composition root (InertiaFactory::setAppId())
AppearanceA small blocking script applies the stored light/dark preference to <html> before React mounts, to avoid a flash of the wrong theme

Theme-color bridge (adapter) ​

The frontend can inherit the active Moodle theme's brand color.

StepDetail
ReadThemeSupport::getBrandColor() reads $PAGE->theme->settings->brandcolor and returns it only if it is a well-formed CSS color (hex, rgb()/rgba()/hsl()/hsla() with numeric arguments, or a plain identifier); otherwise null
GateThemeSupport::isInheritanceEnabled() reads the inherit_theme_colors setting of the core extension ({prefix}core_inherit_theme_colors; see Settings & Config Keys)
InjectThemeSupport::getCssInjection() returns :root { --brand: <color>; } for the consumer to place in the page head, or null when inheritance is off or no valid color exists. A host whose stylesheets read another variable overrides brandCssVariable() (the MIDDAG product uses --middag-brand)
ShareThemeSupport::buildTheme() feeds brandColor and inherit into the Inertia shared props (InertiaSharedProps)
MapThe consumer's design tokens (--primary and siblings) reference the brand variable when inheritance is on; off means the product's own fixed theme applies

The color validation matters: the value is interpolated into inline <style> content.

AMD build output (consumer) ​

The adapter ships no frontend build. The consumer compiles its bundle for Moodle's AMD/RequireJS loader. The MIDDAG Moodle plugin, the reference consumer, does it this way:

ChunkContentsLoading
middag-app.min.jsEntry, app code, registriesImmediate
react-vendor-lazy.min.jsreact, react-dom, @inertiajs/reactLazy
react-ui-lazy.min.jsRadix UI primitivesLazy
react-table-lazy.min.jsTanStack TableLazy
middag-ext-{slug}.min.jsExtension chunks (non-core, via CDN)Lazy

The entry stays small; the React runtime, UI primitives, the table library and extension bundles load on demand through Moodle's requirejs.php.

A dedicated Vite plugin (vite-plugin-moodle-amd) emits AMD output directly into amd/build/, with no manual repackaging step:

ResponsibilityDetail
Path rewritingRewrites relative imports between chunks into the {component}/{name} form Moodle's AMD loader expects
Dev-mode duplicationCopies the lazy chunks into amd/src/, so one watch process covers both amd/build/ and amd/src/

The cost is a bespoke plugin that must track Moodle's requirejs.php expectations.

Enforcement state ​

ClauseState
Theme inheritance is gated by the setting; invalid colors are rejectedcoded — ThemeSupport
AMD path rewriting matches Moodle's loaderplanned — no check in the adapter

Decision records: MDL-001 and MDL-019 in middag-php-moodle/docs/decisions.

MIDDAG © 2015-2026