WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

ATB-53: Theme Resolution and Server-Side Token Injection — Design#

Status: Approved Linear: ATB-53 Branch: atb-59-theme-editor (active) → target main


Context#

Dependencies are complete:

  • ATB-51 (lexicons): Done
  • ATB-52 (CSS token system): Done
  • ATB-55 (theme read API): Done
  • ATB-57 (theme write API): Done

tokensToCss() and the two preset JSONs (neobrutal-light.json, neobrutal-dark.json) already exist in apps/web/src/. BaseLayout currently hardcodes neobrutal-light as a static module-level constant — this ticket makes it dynamic.


Architecture#

Middleware approach#

Hono middleware applied to webRoutes before all page routes. Uses Hono's typed Variables system (Hono<{ Variables: { theme: ResolvedTheme } }>). All route factories update to new Hono<WebAppEnv>() for end-to-end type safety.

Resolution waterfall#

1. User preference        — always null for now (TODO: populate when
                            space.atbb.membership.preferredTheme is added in Theme Phase 4)
2. Color scheme detection — atbb-color-scheme cookie → Sec-CH-Prefers-Color-Scheme hint → "light"
3. Forum default          — fetch GET /api/theme-policy, pick defaultLightThemeUri or
                            defaultDarkThemeUri, fetch full theme, CID integrity check
4. Hardcoded fallback     — FALLBACK_THEME (neobrutal-light tokens + Google Fonts URL)

Any error at any step catches, logs, and returns FALLBACK_THEME with the detected color scheme.

CID integrity check#

The theme policy stores an expected CID per available theme. When the resolution waterfall fetches a theme by rkey, it compares theme.cid (returned by GET /api/themes/:rkey) against the CID stored in policy.availableThemes. A mismatch means the theme record changed after the policy was last updated — fall back to hardcoded and log a warning. Requires adding cid to serializeThemeFull in the AppView (small change, column already exists).

Caching#

Not in scope for ATB-53. ATB-56 adds the in-memory caching layer with cache invalidation on theme writes. When ATB-56 is implemented, Vary: Cookie headers should be added to HTML responses to prevent CDN caching from serving the wrong theme to users.


Types#

// apps/web/src/lib/theme-resolution.ts

export type ResolvedTheme = {
  tokens: Record<string, string>;
  cssOverrides: string | null;
  fontUrls: string[] | null;
  colorScheme: "light" | "dark";
};

export type WebAppEnv = {
  Variables: { theme: ResolvedTheme };
};

export const FALLBACK_THEME: ResolvedTheme = {
  tokens: neobrutalLight,
  cssOverrides: null,
  fontUrls: ["https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;700&display=swap"],
  colorScheme: "light",
};

BaseLayout changes#

resolvedTheme: ResolvedTheme becomes a required prop. Removes:

  • Module-level ROOT_CSS constant and neobrutal-light import
  • Hardcoded Google Fonts <link> tags (moved to FALLBACK_THEME.fontUrls)

Adds:

  • <meta http-equiv="Accept-CH" content="Sec-CH-Prefers-Color-Scheme" />
  • Dynamic :root { ... } <style> block from tokensToCss(resolvedTheme.tokens)
  • resolvedTheme.fontUrls rendered as <link rel="stylesheet"> tags (with <link rel="preconnect"> to googleapis if any font URLs are present)
  • resolvedTheme.cssOverrides rendered as an additional <style> block if non-null

Files Changed#

File Change
apps/appview/src/routes/themes.ts Add cid to serializeThemeFull
apps/web/src/lib/theme-resolution.ts NewResolvedTheme, WebAppEnv, FALLBACK_THEME, detectColorScheme, parseRkeyFromUri, resolveTheme
apps/web/src/middleware/theme.ts New — Hono middleware wrapping resolveTheme
apps/web/src/routes/index.ts Add WebAppEnv type + createThemeMiddleware before routes
apps/web/src/layouts/base.tsx Dynamic theme prop, Accept-CH, font URLs, cssOverrides
apps/web/src/routes/home.tsx new Hono<WebAppEnv>(), pass theme to BaseLayout
apps/web/src/routes/boards.tsx Same
apps/web/src/routes/topics.tsx Same
apps/web/src/routes/login.tsx Same
apps/web/src/routes/new-topic.tsx Same
apps/web/src/routes/admin.tsx Same
apps/web/src/routes/not-found.tsx Same
apps/web/src/routes/auth.ts new Hono<WebAppEnv>() only (no HTML rendering)
apps/web/src/routes/mod.ts Same

Tests#

  • apps/web/src/lib/__tests__/theme-resolution.test.ts (new): all waterfall branches — policy fetch failure, missing default URI, CID mismatch, happy path light, happy path dark, network error fallback; detectColorScheme priority order
  • apps/web/src/layouts/__tests__/base.test.tsx: update to pass resolvedTheme prop; verify Accept-CH meta; verify cssOverrides block renders when present
  • apps/appview/src/routes/__tests__/themes.test.ts: assert cid field in GET /api/themes/:rkey response