Custom theme for Chainguard Courses, a learning platform built on Skilljar. This repo contains the CSS and JavaScript that override Skilljar's default UI with Chainguard branding and additional functionality.
Note: This is a public repository. It contains no credentials or secrets, but keep that in mind when adding comments, commit messages, or documentation.
Skilljar is a SaaS LMS. It renders pages server-side and allows custom CSS and JavaScript to be injected via its admin console. This repo provides those assets:
production/style.css— CSS entry point, imported by Skilljar. Uses@importto pull in modular stylesheets.production/theme.mjs— JS entry point. Runs onDOMContentLoaded, detects which Skilljar page type is loaded, and applies the appropriate view handler.production/preload.js— Injected beforetheme.mjs. Setsbody { display: none }to prevent a flash of unstyled content;theme.mjsrestores visibility once the DOM is ready.
Both the CSS and JS URLs are configured directly in the Skilljar admin console, pointing to the files served from Google Cloud Storage.
courses-theme/
├── production/ # Source files (synced to GCS on merge to main)
│ ├── theme.mjs # JS entry point
│ ├── preload.js # Anti-FOUC preload script
│ ├── style.css # CSS entry point (@import manifest)
│ ├── skilljar-theme-v3.0/ # Theme modules
│ │ ├── CG.mjs # Global state, environment detection, DOM refs
│ │ ├── router.mjs # Page-type detection and view dispatch
│ │ ├── sections.mjs # Course section / card rendering
│ │ ├── views/ # One handler per Skilljar page type
│ │ └── ... # Utilities, icons, footer, logger, etc.
│ ├── css/ # Modular stylesheets
│ │ ├── defs/ # Design tokens (colors, fonts, variables)
│ │ └── *.css # Per-page and global styles
│ └── data/ # Static content: copy, config, SVG, sections
├── fonts/ # Custom font files (Gellix, Roobert)
├── dist/ # Built CSS output (not committed; CI-generated)
├── test/ # Test files + compiled CSS bundle for the mock
├── internal/ # Internal dev tooling (not deployed)
│ ├── mock-landing-page.html # Static HTML preview of the landing page
│ └── PLANS.md # Rough notes and future ideas
└── package.json
- Node.js 25.4.0 (only version tested)
- npm
npm installThere is no local dev server that connects to Skilljar. The options for previewing changes are:
1. Mock page (CSS / landing page layout only)
Build the test CSS bundle and open the mock in a browser:
npm run build:css:test # outputs to test/bundle.min.css
open internal/mock-landing-page.htmlThe mock reflects the landing page structure with static HTML. It does not run the JS theme or Skilljar globals.
2. Staging (full Skilljar integration)
Push changes to main. The GCS sync workflow deploys production/ automatically, and the staging instance at chainguard-test.skilljar.com picks up the changes. There is no way to test against staging locally.
| Command | What it does |
|---|---|
npm run build:css |
Build minified CSS → dist/bundle.min.css |
npm run build:css:test |
Build minified CSS → test/bundle.min.css (for mock page) |
npm run build:css:dev |
Build CSS with source maps → dist/bundle.css |
npm run watch:css |
Watch and rebuild CSS on changes |
npm run build |
Lint everything, then build minified CSS |
npm run lint |
Run CSS and JS linters |
npm run lint:css:fix |
Auto-fix CSS lint issues |
npm test |
Run unit tests |
Unit tests live in test/ and use Node's native test runner:
npm testTests cover utility functions (sanitizeUrl, toTitleCase) and code block utilities (parseLineSpec, cleanCommandPrompt, line number injection).
CSS is linted with stylelint (config in .stylelintrc.json) and JS with ESLint (config in eslint.config.mjs). Both run as part of npm run build and are enforced by pre-commit hooks via .pre-commit-config.yaml.
npm run lint # check
npm run lint:css:fix # auto-fix CSSOn every push to main that touches production/ or fonts/, a GitHub Actions workflow syncs those directories to the GCS bucket chainguard-courses-theme:
production/ → gs://chainguard-courses-theme/production/
fonts/ → gs://chainguard-courses-theme/fonts/
Skilljar then serves the assets from those GCS URLs. Authentication uses Workload Identity Federation — no long-lived credentials are stored.
A separate CI step to build dist/bundle.min.css and deploy it is planned but not yet active. The current setup serves production/style.css directly, with browser-level @import resolution loading the individual stylesheets from GCS.
preload.js → hides body (anti-FOUC)
theme.mjs → DOMContentLoaded
├── CG (init) → detects environment, page type, DOM references
├── preRoute() → shared DOM setup (header, breadcrumbs, body classes)
├── route() → finds matching view handler and runs it
└── postRoute() → final DOM adjustments (footer, messages)
router.mjs maps Skilljar page types to view handlers:
| Page | Handler |
|---|---|
| Login / signup | authView |
| Course (unregistered) | courseUnregisteredView |
| Course (registered) | courseRegisteredView |
| Learning path (unregistered) | pathUnregisteredView |
| Learning path (registered) | pathRegisteredView |
| Lesson | lessonView |
| Catalog / landing | catalogView |
| 404 | notFoundView |
Page type is detected from Skilljar-injected CSS classes on <body> (e.g. sj-page-lesson, sj-page-catalog).
CG.mjs is the global namespace for the theme:
CG.env— environment flags (isStaging,isAdmin,isInternal,isPartner, …)CG.page— page type flags (isLesson,isCatalog,isLanding, …)CG.state— runtime state (course data, user info, breadcrumbs, progress)CG.dom— cached references to key DOM nodesCG.el— element factories (logo, CTAs, mobile header)CG.data— computed data (curriculum sections)
On staging and for admin users, CG, logger, and animateCompletion are exposed on window for debugging.
All static content (landing page copy, course sections, footer links, SVG graphics, UTM parameters) lives in production/data/. To add or edit courses/paths shown on the landing page, edit production/data/path-sections.mjs.
production/style.css loads stylesheets in a fixed order:
- Definitions —
defs/colors.css,defs/fonts.css,defs/variables.css - Globals —
animations.css,globals.css,header.css,footer.css,breadcrumbs.css - Page styles —
catalog.css,lessons.css,auth.css, etc.
All spacing, colour, typography, and motion values are CSS custom properties defined in production/css/defs/. Use these variables rather than raw values. Key scales:
- Spacing:
--spacing-1(4px) →--spacing-24(96px) - Gap:
--gap-1(4px) →--gap-10(40px) - Font sizes:
--font-size-xs(12px) →--font-size-6xl(fluid, up to 96px) - Colours:
--blurple,--ink,--ink-dim,--color-brand-lime, etc.
Skilljar ships its own CSS that must be overridden. !important is used throughout — this is expected and necessary, not a mistake.