Case study · Enterprise SaaS learning platform

One YAML file per component. Everything else generated.

The client's enterprise SaaS learning platform, which I rebuilt as a modern AEM Cloud component system. The headline is a schema-first code-generation pipeline I designed: a single YAML definition per component becomes the TypeScript types, the Lit scaffolds, the AEM author dialogs and the HTL — so the same component is never hand-written in four slightly different places. Built and delivered to the client; launch is pending SSO, so it isn't public yet.

Role
Lead frontend / architect (via 3|Share)
Stage
2025–present · built and delivered, launch pending SSO
Scale
~25 AEM components (40+ with Lit atoms) from one generator
Stack
Vite 7, Lit 3, TypeScript 5.9, Storybook 10

·

The problem with hand-built AEM components

An AEM component is really four artefacts that have to agree with each other: a Java/HTL template that renders server-side, an XML dialog that tells authors what fields exist, a TypeScript model for any client behaviour, and the CSS. Keep them in sync by hand across dozens of components and they drift — a field gets renamed in the dialog but not the model, a type goes stale, a new author option never reaches the template. Multiply that by a team and a multi-month build and the drift is constant.

The pipeline

I made the YAML schema the single source of truth. Each component has one component.yaml; npm run generate emits the TypeScript interfaces, the Lit scaffold and the CSS scaffold, and generate:aem emits the dialog XML, component XML, EditConfig and HTL. The YAML became the contract between frontend and backend — change a field once and every downstream artefact follows.

Why it matters to a team: a new component is a YAML file and a generate command, not a half-day of boilerplate spread across four file types. Junior engineers and AI assistants extend the platform by editing a schema, not by reverse-engineering conventions — the generator enforces them.

The hardest bug

Composite multifields — nested repeatable groups of fields — were generating dialog XML that AEM accepted but silently rendered wrong when a nestedType reference was missing. Silent failure is the worst kind in a generator, because the output looks plausible. The fix was to make the generator throw on any missing nestedType reference rather than emit almost-correct XML. Correctness moved left, to generate time, where a developer sees it immediately.

Performance by construction

AEM renders HTML server-side, so the page doesn't need a client framework hydrating the whole tree — only islands of interactivity do. I chose Lit 3 over React for exactly that reason and built progressive hydration with several strategies (eager, idle, visible, interaction) so each component hydrates only when it needs to. Design tokens live in cascade layers with a no-!important rule at org scale, and Vite handles manual-chunk splitting.

  • Vite 7
  • Lit 3
  • TypeScript 5.9 (strict)
  • Zod 4
  • Lightning CSS
  • Open Props
  • Storybook 10
  • cascade layers
  • progressive hydration
  • AEM Cloud
  • HTL / Sling Models
  • GitLab CI
  • Adobe Cloud Manager
  • axe-core
  • Playwright

Real Java, not just frontend

The platform isn't frontend-only. I wrote around 30 Sling Models, an OSGi CourseService, and a Core WCM Accordion extension — the backend the generated components bind to. Coming up through AEM means I can drop into Java when a feature genuinely needs it, rather than throwing it over a wall.

Quality and release

Every component carries axe-core checks and Storybook visual-regression coverage; the suite runs JUnit + Selenium with Allure reports on GitLab Pages. Releases are semver with conventional commits and zero-touch deploys to Adobe Cloud Manager. AI-agent tooling (AGENTS.md / CLAUDE.md plus 17 domain skill files) lets autonomous sessions extend the codebase along the same conventions the generator enforces.

The shape of the contribution: I authored the architecture and the overwhelming majority of the repository — roughly 292 of 294 commits, about 116k lines and 36 PRs in the first five weeks — then set the conventions that let other engineers extend it safely.

Back to selected work  ·  Next: a global pharmaceutical client, three regulated platforms  ·  Talk to me about a similar build