Files
classroomio/packages/ui/src/custom/org-landing-page/README.md
T
Rotimi Best d5b07d356e feat: HLS streaming, transcripts, agent course generation & landing-page course previews (#663)
New features:
- HLS video encoding/streaming (hls routes, hls-encoder) and a media
  transcripts pipeline (transcripts route + media-transcript queries)
- AI assistant chat vs. run model: agent-course-generation worker/queue,
  run-monitor pane, chat-changes card, content ask-ai bar
- Per-theme course-page previews across org landing-page themes (bold,
  classic, corporate, editorial, etc.) with a shared course token/component set
- View-as-student mode and login-link / link-invite invite flows

Refactor:
- Extract packages/core consolidating config (env, storage, dashboard-url),
  services and utils previously duplicated under apps/api/src/config
- Remove legacy agent service modules (chat-tools, chat-context,
  context-window, document, fetch-url, lesson-content)
- Relax Cross-Origin-Resource-Policy to allow cross-origin media embedding
- Add format script to tenant-router so turbo run format covers it
2026-06-07 18:43:05 +01:00

11 KiB

Org + Course landing pages

This folder holds the entire landing-page system: the public org site ({theme}.org) and the per-course landing ({theme}.course), in 10 themes.

Both surfaces share the same nav, hero, footer, tokens, and edit context. A course landing inherits its theme from the org's landingpage.theme — courses don't pick their own.

Folder map

packages/ui/src/custom/org-landing-page/
│
├── types.ts                       Public types: OrgLandingPageProps, CourseLandingPageProps,
│                                  CourseLandingPageLabels, OrgLandingPageTheme, …
├── theme-vars-base.ts             Type + baseTokenVars for --landing-* CSS variables
├── theme-style.ts                 Thin registry: aggregates per-theme vars + exports themeStyle()
├── course-tokens-base.ts          Type + baseCourseTokens + tokenize() helper
├── course-landing-page.tokens.ts  Thin registry: aggregates per-theme courseTokens + exports
│                                  courseLandingTokens(theme)
├── course-landing-page.helpers.ts alignHeroCtaWithPricing, buildCourseSectionNavItems
├── fixtures.ts                    mockOrgLandingPageProps, mockCourseLandingPageProps
├── index.ts                       Public exports
│
├── edit-context.ts                LandingSectionKey + setLandingPageEditContext/getLandingPageEditContext
├── editable-section.svelte        Wrapper that makes a section click-to-edit when a context is set
├── landing-button.svelte          Theme-aware button used by every CTA
├── landing-page-footer.svelte     Shared footer, per-theme class tokens
├── landing-page-footer.tokens.ts
│
├── {theme}/                       Everything for one theme (minimal, bold, classic, saas, tech,
│   │                              studio, corporate, terminal, editorial, vibrant)
│   ├── index.ts                   re-exports { nav, hero, courseCard, org, course }
│   ├── nav.svelte                 reused by both org and course
│   ├── hero.svelte                reused; course populates with course-shaped hero data
│   ├── course-card.svelte         used by the org catalog
│   ├── org.svelte                 org-landing composer for this theme
│   ├── course.svelte              course-landing composer for this theme
│   ├── vars.ts                    --landing-* CSS variables for this theme
│   └── course-tokens.ts           CourseLandingTokens record for this theme
│
└── course-*.svelte                Shared body components driven by `variant` + tokens:
    course-section-nav, course-social-proof, course-info-blocks, course-curriculum,
    course-chips, course-instructor, course-reviews, course-pricing

A theme is one folder

Every artifact for a theme — composer, sub-components, CSS variables, class tokens — lives in {theme}/. To touch one theme you edit one directory. To delete one you rm -rf {theme}/. To duplicate one you copy the folder.

The central registries (theme-style.ts, course-landing-page.tokens.ts, index.ts) only import-and-aggregate from each {theme}/.

Three-layer separation

┌────────────────────────────────────────┐
│ {theme}/course.svelte    (orchestration)│  ← composes the theme
│ ── reuses {theme}/nav, {theme}/hero,    │
│    landing-page-footer                  │
└──┬─────────────────────────────────────┘
   │ renders, passes variant="…"
   ▼
┌────────────────────────────────────────┐
│ course-*.svelte           (shared body) │  ← one component per section
│   • course-section-nav                  │    used by all 10 themes
│   • course-social-proof                 │
│   • course-info-blocks                  │
│   • course-curriculum                   │
│   • course-chips                        │
│   • course-instructor                   │
│   • course-reviews                      │
│   • course-pricing                      │
└──┬─────────────────────────────────────┘
   │ reads courseLandingTokens(variant)
   ▼
┌────────────────────────────────────────┐
│ {theme}/course-tokens.ts     (tokens)   │  ← per-theme class strings
│ {theme}/vars.ts              (vars)     │     and CSS variables
└────────────────────────────────────────┘

The composer only orchestrates. Body components only render. Theme variation lives in tokens + CSS vars.

Token contract

Each theme owns its course-tokens.ts exporting courseTokens: CourseLandingTokens via tokenize({...overrides}). The base lives in course-tokens-base.ts. Every body component reads t = courseLandingTokens(variant) and applies {t.sectionShell}, {t.heading}, {t.curriculumModule}, … Anything not overridden falls through to the base.

Tokens-first rule. Body components must not hardcode theme-varying utilities (font-bold, tracking-tight, rounded-2xl, bg-card-soft, …). Anything that differs between themes lives in tokens. Layout-only utilities that are identical everywhere (flex, grid, gap-6, py-12) can sit inline.

Off-palette rule. A course body section must only use surfaces that exist in the theme's org template. Don't introduce bg-section greys to minimal, don't paint card-soft on bold, etc.

Data flow

The dashboard's <CourseLandingPage> (apps/dashboard/src/lib/features/ui/course-landing-page/course-landing-page.svelte) is the entry point. It:

  1. Reads the org's chosen theme from landingpage.theme.
  2. Maps course + org → CourseLandingPageProps via buildCourseLandingPageProps(). Section visibility (metadata.sectionDisplay) is honoured here — hidden sections are stripped from info / reviews / instructor before rendering.
  3. Lazy-loads the theme bundle via importThemeBundle(theme) — one bundle per theme; the other nine never download. The bundle exposes { nav, hero, courseCard, org, course }.

The composer wires nav/hero/footer (theme-specific files) around the shared body components, threading variant={theme} and the same labels prop into each.

Edit context (click-to-edit)

EditableLandingSection sectionKey="…" wraps every editable region. When a parent route calls setLandingPageEditContext({ selectedKey, selectKey, labelFor, iconFor }), clicking a section in the preview calls selectKey(key). The Editor sidebar reads the same key from a bound prop and opens the matching form. Without a context, the wrapper is a transparent pass-through.

Course LandingSectionKey values: header, requirement, description, goals, certificate, curriculum, chips, instructor, reviews, pricing.


Adding a new theme

A theme is one folder. The central registries are the only files outside that folder you touch.

1. Declare the theme key

types.ts:

export type OrgLandingPageTheme =
  | 'minimal'
  | …
  | 'newtheme';

2. Create the folder

newtheme/
├── index.ts
├── nav.svelte
├── hero.svelte
├── course-card.svelte
├── org.svelte
├── course.svelte
├── vars.ts
└── course-tokens.ts

Quickest path: cp -R minimal/ newtheme/ and edit.

3. Fill in each file

  • nav.svelte, hero.svelte, course-card.svelte — the theme's UI personality. Hero composes {@render navigation()} if the theme renders nav inside the hero.

  • org.svelte — org landing composer for this theme.

  • course.svelte — course landing composer (use any existing theme's course.svelte as a template; the body sequence is the same).

  • vars.ts — exports vars: LandingThemeVars. Spread ...baseTokenVars and override the differences. Use raw hex only for genuinely off-palette themes (see terminal/vars.ts, editorial/vars.ts).

  • course-tokens.ts — exports courseTokens: CourseLandingTokens = tokenize({ ... }). Only override regions that differ from the base.

  • index.ts:

    export { default as nav } from './nav.svelte';
    export { default as hero } from './hero.svelte';
    export { default as courseCard } from './course-card.svelte';
    export { default as org } from './org.svelte';
    export { default as course } from './course.svelte';
    

4. Wire into the central registries

theme-style.ts:

import { vars as newthemeVars } from './newtheme/vars';
…
export const LANDING_THEME_VARS = {
  …,
  newtheme: newthemeVars
};

course-landing-page.tokens.ts:

import { courseTokens as newthemeTokens } from './newtheme/course-tokens';
…
const TOKENS = {
  …,
  newtheme: newthemeTokens
};

index.ts:

import * as newtheme from './newtheme';
export const NewthemeLandingPage = newtheme.org;
export const NewthemeCourseLanding = newtheme.course;
export const NewthemeLandingNav = newtheme.nav;
export const NewthemeLandingHero = newtheme.hero;
export const NewthemeLandingCourseCard = newtheme.courseCard;

5. Register in the dashboard utils

apps/dashboard/src/lib/features/org/utils/landing-page.ts:

  • Add the key to landingPageThemes.
  • Add a case 'newtheme' to importThemeBundle (this is the only lazy-load switch — importThemeComponent, importThemeNavHero, importThemeCourseCard, and importCourseLandingPageTheme are thin adapters over it).
  • If your hero composes {@render navigation()}, add the theme key to NAV_SNIPPET_THEMES.
  • Add an entry to themeCourseGridClass.

6. Pick body layouts

  • course-curriculum.svelte has a layoutByTheme map (accordion for collapsible, chapters for permanently expanded). Add newtheme.
  • If the info-blocks should use a different layout, do the same in course-info-blocks.svelte.

7. Storybook

In packages/storybook/src/templates/org-landing-page/:

  • org-landing-page.stories.svelte — add <NewthemeLandingPage {...mockProps} />.
  • course-landing-page.stories.svelte — add <NewthemeCourseLanding {...mockProps} />.

8. Audit

  • Walk the new story top-to-bottom and compare against the org story for the same theme. Surfaces should match.
  • grep -nE 'font-(black|extrabold|bold|semibold|medium)|tracking-(tight|tighter|wider|widest)|rounded-(none|sm|md|lg|xl|2xl|3xl|full)' course-*.svelte — body components should return no matches.
  • grep -nE 'bg-\[var\(--landing-(bg-section|card-soft|card)\)' newtheme/course-tokens.ts — confirm you only used surfaces present in the org theme.
  • pnpm exec svelte-check --threshold error from the repo root.

That's it — one folder, two registry hooks, two Storybook entries.