* fix(dashboard): show course cards in landing page settings preview Fetch public courses for the org and pass them to the theme preview on the landing page settings screen, matching the editor preview. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * refactor(dashboard): skip redundant public course fetches Add loadPublicCoursesIfNeeded so settings, editor, and live landing page reuse cached courses for the same org site instead of clearing and refetching on every mount. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(dashboard): always refetch courses on live org landing page Keep loadPublicCoursesIfNeeded for settings preview and editor only. The public-facing landing page should fetch fresh on each visit. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * feat(ui): add shared empty state for org landing page catalogs Introduce OrgLandingPageCoursesEmpty using the dashboard Empty pattern with landing-theme colors. Show it across all ten org landing templates when no courses are published, and wire translated labels through buildOrgLandingPageProps. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(dashboard): address Greptile review on public courses cache - Add coursesLoaded prop so empty catalog state waits for fetch - Deduplicate concurrent public course requests via in-flight promise - Refresh courses on settings mount; reuse cache in editor only - Expose invalidatePublicCourses for explicit cache clearing Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(dashboard): load landing preview courses when org store hydrates Replace onMount course fetch with a reactive effect keyed on the route slug and currentOrg.siteName so public courses load after app init. Mark the fetch as complete in finally so failed requests show the empty state instead of a permanently blank catalog section. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> ---------
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:
- Reads the org's chosen theme from
landingpage.theme. - Maps
course + org→CourseLandingPagePropsviabuildCourseLandingPageProps(). Section visibility (metadata.sectionDisplay) is honoured here — hidden sections are stripped frominfo/reviews/instructorbefore rendering. - 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'scourse.svelteas a template; the body sequence is the same). -
vars.ts— exportsvars: LandingThemeVars. Spread...baseTokenVarsand override the differences. Use raw hex only for genuinely off-palette themes (seeterminal/vars.ts,editorial/vars.ts). -
course-tokens.ts— exportscourseTokens: 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'toimportThemeBundle(this is the only lazy-load switch —importThemeComponent,importThemeNavHero,importThemeCourseCard, andimportCourseLandingPageThemeare thin adapters over it). - If your hero composes
{@render navigation()}, add the theme key toNAV_SNIPPET_THEMES. - Add an entry to
themeCourseGridClass.
6. Pick body layouts
course-curriculum.sveltehas alayoutByThememap (accordionfor collapsible,chaptersfor permanently expanded). Addnewtheme.- 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 errorfrom the repo root.
That's it — one folder, two registry hooks, two Storybook entries.