63 lines
3.4 KiB
Markdown
63 lines
3.4 KiB
Markdown
## Context
|
|
|
|
The site already supports theme selection via a floating notch and persists the user's choice using `localStorage` (key: `site.theme`). Theme selection affects the root document via `html[data-theme]` and is initialized before first paint via an inline head script.
|
|
|
|
Two gaps remain:
|
|
- Returning users should reliably see their last-selected theme even in environments where `localStorage` is unavailable.
|
|
- We need analytics to measure theme switcher usage and preferred themes.
|
|
|
|
The site uses Umami for analytics. Most interactions are tracked via `data-umami-event*` attributes, and runtime-only events use `window.umami.track(...)`.
|
|
|
|
## Goals / Non-Goals
|
|
|
|
**Goals:**
|
|
- Persist theme selection across visits with a robust fallback: `localStorage` primary, client-side cookie fallback.
|
|
- Apply the stored theme before first paint when possible.
|
|
- Emit a deterministic Umami event when a user changes theme via the theme notch.
|
|
|
|
**Non-Goals:**
|
|
- Server-side rendering of theme choice (the site is statically built; no request-time HTML variation).
|
|
- Tracking theme selection on initial page load (only user-initiated changes).
|
|
- Adding new UI beyond the existing theme notch.
|
|
|
|
## Decisions
|
|
|
|
1) Persistence mechanism and precedence
|
|
|
|
- **Decision**: Read theme preference in this order:
|
|
1. `localStorage` (`site.theme`)
|
|
2. Cookie (`site_theme`)
|
|
3. Environment signals (forced-colors, prefers-color-scheme)
|
|
- **Rationale**: `localStorage` is already in use and provides a stable primary store. Cookies provide a resilient fallback when storage access is blocked or throws.
|
|
- **Alternatives considered**:
|
|
- Cookie-only: simpler but unnecessary regression from existing behavior.
|
|
- URL param: not persistent and adds user-visible noise.
|
|
|
|
2) Cookie format and attributes
|
|
|
|
- **Decision**: Store `site_theme=<theme>` with `Max-Age=31536000`, `Path=/`, `SameSite=Lax`. Set `Secure` when running under HTTPS.
|
|
- **Rationale**: First-party cookie with long TTL provides continuity across visits. The cookie is readable from the inline head script for pre-paint initialization.
|
|
|
|
3) Analytics event shape
|
|
|
|
- **Decision**: Emit a custom Umami event via `window.umami.track("theme_switch", data)` only on user-initiated changes.
|
|
- **Event properties**:
|
|
- `target_id`: `theme.switch.<theme>`
|
|
- `placement`: `theme_notch`
|
|
- `theme`: new theme value (`dark` | `light` | `high-contrast`)
|
|
- `prev_theme`: previous theme value (same enum) when known
|
|
- **Rationale**: A dedicated event name makes reporting straightforward (no need to filter general `click`). Using `target_id`/`placement` keeps it compatible with the site's interaction taxonomy.
|
|
- **Alternatives considered**:
|
|
- Reuse `click` event: consistent, but mixes preference changes into general click reporting.
|
|
|
|
4) Avoid tracking initial theme restoration
|
|
|
|
- **Decision**: Do not emit `theme_switch` from the head theme-init script.
|
|
- **Rationale**: We want to measure explicit user interaction with the notch, not implicit restoration.
|
|
|
|
## Risks / Trade-offs
|
|
|
|
- Cookie and storage may both be blocked in restrictive environments → fallback to environment signals; no persistence.
|
|
- Umami may be disabled/unconfigured or not loaded at event time → guard with `typeof window.umami !== "undefined"` and keep behavior non-fatal.
|
|
- Using cookies introduces another persistence layer → must keep `localStorage` and cookie consistent on successful theme changes.
|