[Guide] Build a Learning Platform (Route Layout Templates) - implementation notes
The Learning Platform guide frames layouts as templates: layout routes own shared state (course progress, current module), route grouping organizes content motion, and navigation becomes a contract rather than an emergent property of nested components. I like the "template" framing because it makes shared state location obvious, but I'm curious how it plays out when the UI gets complex.
What did you let the layout own (progress, sidebar expansion, playback posture) versus leaving to child routes? How did you model route grouping so module/lesson navigation stayed coherent with back/forward? What evidence did you render so screenshots show current template + module + lesson identity and motion keys? How did you avoid the layout becoming a mega component with too many responsibilities?
Comments (18)
Back to latestLayout-as-template worked for us because it forced us to decide what is truly shared.
If the sidebar needs to stay stable while lessons change, the sidebar state belongs in the template.
We logged layout contract transitions and it made navigation bugs diagnosable:
txt
[tips] layoutTemplate=courseShell module=m3 lesson=l2 reason=nav:click
[tips] routeGroup=module motion=lesson:swap remountKey=lesson:l2
[tips] layoutState progress=0.63 reason=derive:progress
Then we render data-template, data-module, data-lesson, and data-motion on the shell.
Counterpoint: layouts owning global state can turn into a hidden global store if you're not careful.
We kept layout state to navigation-adjacent values only. Anything domain-y lived in a course document keyed by courseId.
Yes. Template state isn't "everything" state.
If a value isn't required to keep navigation and shared chrome coherent, it probably shouldn't live in the layout.
Route grouping helped us avoid "one route per UI" chaos.
We grouped by user mental model: course -> module -> lesson, and motion keys matched that model.
Long-form take: learning platforms fail when the chrome (nav/progress) and the content disagree.
Layouts as templates solve that because they colocate the chrome contract with the state that drives it.
But you must render evidence of identity (course/module/lesson) or support can't debug anything.
We stored derived progress in the layout because it influences multiple routes (sidebar, header, next lesson CTA). And we logged progress derive so it didn't feel like magic:
txt
[tips] derive=progress course=c7 ratio=0.63 reason=lesson:completeCounterpoint: putting progress in layout can make it update too often and hurt perf.
We introduced a tick posture (warm/cool) for progress derive and made it explicit in logs + evidence.
We avoided layout bloat by treating the layout as a composition point, not a logic point.
Logic lived in small docs/derivers; layout just renders outputs and wires motion keys.
Long-form counterpoint: templates can encourage teams to couple unrelated features because "it's shared chrome".
We had to enforce boundaries: template gets navigation + progress, not messaging, not billing, not random banners.
Docs tie-in: routing + app router mindset pages are basically the rationale for template layouts + grouping.
Related: Routing and Navigation and The App Router Mindset.
We also rendered a data-template-version so we could correlate layout changes with navigation bugs.
It sounds like overkill, but it saved us during refactors.
Motion keys were the hidden win: module switches remount, lesson switches preserve sidebar state.
Once you encode that as keys, the UI becomes calmer.
Counterpoint: preserving sidebar state across lessons can be confusing if the sidebar content changes per lesson.
We scoped the preserved state to expansion/collapse only; selected item derived from route identity.
We logged route grouping decisions when parsing URLs (defaults, normalizations).
It prevented subtle "wrong lesson" bugs because you can see how the URL was interpreted.
Long-form argument: layouts as templates are a way to keep UX constraints close to the routing model.
If the UX says "sidebar persists", the route model needs to encode that persistence, not rely on component happenstance.
We also kept a small "template summary" string for support (course/module/lesson/tick posture).
Once that existed, most navigation bugs were diagnosable from a single screenshot.
If you're implementing this, start by deciding your identity boundaries (courseId/moduleId/lessonId) and how keys remount.
Everything else is detail once those boundaries are explicit.
The guide reads like a routing document disguised as a UX guide, which is exactly what it should be.
Learning platforms live or die by navigation coherence.