Skip to content

Learner Experience

Audience: Anyone designing or supporting the learner-facing surfaces of the LMS.

Scope: The end-to-end learner journey — discovery, account creation, enrolment, sequential entry playback, quiz attempts, dashboards. Authoritative behaviour lives in reference/course-enrollment-system.md, reference/entry-pagination.md, reference/quiz-system.md, and reference/toggleable-navigation.md.

Discovery (Public)

Public learners (no account, no session) can:

  • Browse the course catalogue at /learn.
  • Open a public course landing at /learn/course/\[slug\] — published metadata, preview-safe summary, sign-in / sign-up CTA.
  • Read CMS-authored marketing pages under /\[slug\] and the blog under /blog.

generateStaticParams includes published courses only. Drafts, archived, and unlisted courses are not enumerated.

Account Creation

  • /learn/sign-up — email + password. Tenant is resolved from the request host.
  • A verification email is sent immediately. Account remains unverified until the recipient clicks the token link at /learn/verify/\[token\].
  • Magic-link sign-in (when the tenant magicLink feature flag is enabled): from /learn/sign-in, follow Sign in with a magic link to /learn/magic, submit the account email, then open the one-time link at /learn/magic/\[token\] (emailed in production; logged to the dev server terminal when using the console email driver — see Email Delivery). Details: development/auth.md.
  • Google OAuth (when enabled) auto-links by (tenant_id, email) per development/auth.md.

Forgot-password is at /learn/forgot; reset is at /learn/reset/\[token\]. Both are rate-limited and never reveal whether an account exists.

Enrolment

  • Authenticated learners click Enrol on the course landing page.
  • The server action enrollInCourse(courseSlug):
    • Validates the slug and verifies the course is published.
    • Upserts the enrollments row scoped by tenant_id + user_id.
    • Sets status = 'active' for free courses.
    • Returns 501 Not Implemented with user-safe copy for paid courses (until a payment phase ships).
    • Re-enrolment from cancelled updates the same row, bumps enrolled_at, clears completed_at, and writes an audit event.
  • After success the learner is redirected to /learn/course/\[slug\] (the enrolled course home).

Full state machine and error paths: reference/course-enrollment-system.md.

Course Home

Enrolled view of /learn/course/\[slug\]:

  • Module/entry list with status badges (completed / locked / available).
  • Continue CTA pointing to the resume target (first incomplete entry, or most recently viewed entry).
  • Toggleable course sidebar — see reference/toggleable-navigation.md. [ toggles visibility from anywhere on the course shell.

Sequential Entry Playback

/learn/course/\[slug\]/entry/\[entrySlug\]:

  • Renders the entry body (Portable Text + widgets) and prev/next buttons.
  • Sequential gating: by default an entry is accessible if all prior entries in the same module are complete; cross-module gating is off.
  • Deep-linking to a locked entry redirects to the first available entry with ?notice=<reasonKey>; a translation-keyed toast announces the reason.
  • Server actions markEntryComplete and recordEntryView write entry_progress. Preview mode (?preview=1, available to assigned course_admin, tenant_admin, super_admin) is read-only.
  • Keyboard: / for prev/next, [ for sidebar toggle.

Full gating and resume contract: reference/entry-pagination.md.

Quizzes

When an entry's _type is quiz:

  • The CMS supplies the question shape; the LMS strips correct answers from the client payload (Quiz server-only vs QuizPublic client-safe).
  • QuizPlayer (client) handles per-question validation and one-pass submission.
  • submitQuizAttempt re-fetches the quiz from the CMS, grades server-side, persists a quiz_attempts row with sequential attempt_number, and writes entry_progress on pass or attempts exhaustion.
  • Retry policy reads quiz.maxAttempts from the CMS. After exhaustion the entry enters review-only mode.

Full contract: reference/quiz-system.md.

Assignments

_type: 'assignment' entries are routable in V1 but submission and grading are placeholders. Learners see translation-keyed copy explaining that uploads are not yet implemented and a manual Mark complete button. Real submission/grading is F9 in roadmap.md.

Dashboard

/learn/dashboard:

  • Enrolled courses with progress bars and continue CTAs.
  • In-progress entries.
  • Recent quiz scores.

Tenant-scoped queries via Kysely; analytics shapes documented in reference/analytics.md.

Site header preferences

Every visitor sees three preference controls in the top-right site header on public and learner routes (inside data-testid="site-header-actions"):

Control Location Options Persistence
Language Header dropdown en, es, fr, de, pt, zh Cookie; authenticated users also store preference in users.locale via the account page
Font size Header dropdown (text-size icon) Small, Medium (default), Large, Extra large Browser localStorage (font-size); same device and browser only
Theme Header dropdown Light, Dark, System Browser localStorage (theme); same device and browser only
  • Font size scales all text proportionally — navigation, headings, body copy, course widgets, and footer — so size relationships stay consistent.
  • Medium keeps the site's normal reading size, including any CMS-authored base font size when the tenant site theme defines one.
  • Choices apply immediately and persist across visits on the same browser. They are not synced to the learner account or other devices (same model as theme).

Account

/learn/account:

  • Edit display name and preferred language (stored on your account).
  • Request an email change (sends a confirmation token to the new address).
  • Sign out everywhere (bumps users.token_version).

For light/dark theme and font size, use the site header controls described in Site header preferences above.

Accessibility

  • Every interactive element has a visible focus indicator.
  • Modals and sheets trap focus via useFocusTrap.
  • Keyboard shortcuts (, , [) ignore form / contenteditable focus and respect aria-disabled.
  • Live-region announcements (toasts, prev/next) are translation-keyed.
  • Learners can increase or decrease global text size from the site header without breaking layout or heading hierarchy.