Description
Branding via @openedx/brand-openedx is not reliably applied in sites built on frontend-base. In frontend-site, only the body element picks up the brand's typography tokens on initial load; content inside lazy-loaded app routes reverts to Paragon's defaults. The cause is a CSS load-order fight that the current layering makes easy to hit.
frontend-base/shell/app.scss imports Paragon's core.min.css and light.min.css. The problem is that each consuming app has its own src/app.scss that begins with @use "@openedx/frontend-base/shell/app.scss", and that app.scss is imported from the app's Main module, which is itself code-split via import('./Main') in the app's router. Each of those imports is a separate Sass compilation unit. Sass @use dedupes within one compile, but not across compiles, so Paragon's stylesheet ends up bundled both into the site's main CSS and into each per-app lazy chunk.
At runtime, the site's main bundle loads first with Paragon followed by the brand overrides. When the user navigates to an app route, that app's chunk CSS is appended to <head> after the brand styles. Because CSS custom properties live on the element they target and var() resolves at use-time against the current computed value, the chunk's :root { --pgn-typography-font-family-sans-serif: ... } declaration clobbers the brand's value globally, for every consumer in the document.
The runtime-URL mechanism documented in docs/how_tos/theming.md (siteConfig.theme with core.url and variants.<name>.url) does not avoid this on its own, because the theme <link> is inserted on React mount and the lazy app chunks still append after it.
The fix is to stop apps from re-bundling the shell, and keep the site as the single owner of global styles. frontend-base itself stays brand-agnostic: Paragon remains in shell/app.scss because it is part of the shell's contract, but brand is the site's choice and should not be introduced into frontend-base.
TODO
Description
Branding via
@openedx/brand-openedxis not reliably applied in sites built onfrontend-base. Infrontend-site, only the body element picks up the brand's typography tokens on initial load; content inside lazy-loaded app routes reverts to Paragon's defaults. The cause is a CSS load-order fight that the current layering makes easy to hit.frontend-base/shell/app.scssimports Paragon'score.min.cssandlight.min.css. The problem is that each consuming app has its ownsrc/app.scssthat begins with@use "@openedx/frontend-base/shell/app.scss", and thatapp.scssis imported from the app'sMainmodule, which is itself code-split viaimport('./Main')in the app's router. Each of those imports is a separate Sass compilation unit. Sass@usededupes within one compile, but not across compiles, so Paragon's stylesheet ends up bundled both into the site's main CSS and into each per-app lazy chunk.At runtime, the site's main bundle loads first with Paragon followed by the brand overrides. When the user navigates to an app route, that app's chunk CSS is appended to
<head>after the brand styles. Because CSS custom properties live on the element they target andvar()resolves at use-time against the current computed value, the chunk's:root { --pgn-typography-font-family-sans-serif: ... }declaration clobbers the brand's value globally, for every consumer in the document.The runtime-URL mechanism documented in
docs/how_tos/theming.md(siteConfig.themewithcore.urlandvariants.<name>.url) does not avoid this on its own, because the theme<link>is inserted on React mount and the lazy app chunks still append after it.The fix is to stop apps from re-bundling the shell, and keep the site as the single owner of global styles.
frontend-baseitself stays brand-agnostic: Paragon remains inshell/app.scssbecause it is part of the shell's contract, but brand is the site's choice and should not be introduced intofrontend-base.TODO