Now I remember the theme
Some checks failed
ci / site (push) Has been cancelled
publish-image / publish (push) Has been cancelled

This commit is contained in:
2026-02-10 20:38:38 -05:00
parent 70710239c7
commit f50a828535
19 changed files with 321 additions and 304 deletions

View File

@@ -1,2 +1,2 @@
schema: spec-driven schema: spec-driven
created: 2026-02-10 created: 2026-02-11

View File

@@ -0,0 +1,62 @@
## 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.

View File

@@ -0,0 +1,25 @@
## Why
Theme choice is part of a user's identity and comfort on the site; returning visitors should land in the theme they previously selected. We also need measurement to understand whether the theme switcher is being used and which themes are preferred.
## What Changes
- Persist the user's selected theme across visits so returning users see the last-selected theme immediately.
- Add Umami tracking for theme selection changes so theme switch usage can be measured and segmented.
- Improve robustness of persistence by supporting either localStorage or a client-side cookie (cookie fallback when localStorage is unavailable).
## Capabilities
### New Capabilities
- (none)
### Modified Capabilities
- `site-theming`: Extend theme persistence requirements to explicitly cover returning visits and define acceptable client-side persistence mechanisms / fallback behavior.
- `analytics-umami`: Add a custom event emitted from client-side code for theme selection changes (using Umami's JS API when needed).
- `interaction-tracking-taxonomy`: Define the theme selection event name and required event properties (at minimum `target_id` and `placement`, plus theme metadata).
## Impact
- Frontend: update theme switcher behavior in `site/src/layouts/BaseLayout.astro` (persistence/fallback and event emission).
- Analytics: new Umami event(s) added; dashboards/filters can segment by selected theme and placement.
- Specs: update the modified capabilities above to reflect the new requirements.

View File

@@ -0,0 +1,21 @@
## ADDED Requirements
### Requirement: Theme switch tracking event
When Umami is enabled, the site MUST emit a custom event when the user changes theme via the theme switcher UI.
The site MUST emit the event using Umami's JavaScript API (`umami.track(...)`) so runtime properties can be included.
The event name MUST be `theme_switch`.
The emitted event MUST include, at minimum:
- `target_id`
- `placement`
- `theme`
#### Scenario: Theme switch emits event
- **WHEN** a user selects `high-contrast` in the theme switcher notch
- **THEN** the site emits a `theme_switch` event with `theme=high-contrast` and a stable `target_id`
#### Scenario: Missing Umami does not break switching
- **WHEN** Umami is not configured or the Umami script is not present
- **THEN** theme switching and persistence still work and no browser error is thrown

View File

@@ -0,0 +1,23 @@
## ADDED Requirements
### Requirement: Theme switch event taxonomy
The tracking taxonomy MUST define an event for theme switching.
The event name MUST be `theme_switch`.
The `theme_switch` event MUST include, at minimum:
- `target_id`
- `placement`
- `theme`
The event SHOULD include `prev_theme` when available.
The taxonomy MUST define the `target_id` namespace for theme switching as:
- `theme.switch.<theme>`
The taxonomy MUST define the `placement` value for the theme switcher notch as:
- `theme_notch`
#### Scenario: Theme switch target_id is deterministic
- **WHEN** a user selects `light` theme using the theme notch
- **THEN** the event is emitted with `target_id=theme.switch.light` and `placement=theme_notch`

View File

@@ -0,0 +1,25 @@
## ADDED Requirements
### Requirement: Theme persistence works across visits with fallback
The site MUST persist the user's theme selection across visits so returning users see the last-selected theme.
The site MUST use client-side persistence and MUST support a fallback mechanism:
- Primary: `localStorage`
- Fallback: a client-side cookie
The effective theme selection order MUST be:
1) Stored theme in `localStorage` (if available)
2) Stored theme in a cookie (if localStorage is unavailable)
3) Default selection using environment signals
#### Scenario: LocalStorage persists across a later visit
- **WHEN** a user selects `light` theme and later returns to the site in the same browser
- **THEN** the site initializes in `light` theme before first paint
#### Scenario: Cookie fallback is used when localStorage is unavailable
- **WHEN** the browser environment blocks `localStorage` access and the user selects `dark` theme
- **THEN** the theme is persisted using a client-side cookie and is restored on the next visit
#### Scenario: No persistence available falls back to defaults
- **WHEN** both `localStorage` and cookie persistence are unavailable
- **THEN** the site falls back to default theme selection using environment signals

View File

@@ -0,0 +1,17 @@
## 1. Theme Persistence Across Visits
- [x] 1.1 Add cookie helpers in `site/src/layouts/BaseLayout.astro` to read/write a `site_theme` cookie (Path=/, SameSite=Lax, 1y TTL; Secure on HTTPS)
- [x] 1.2 Update the head theme-init script in `site/src/layouts/BaseLayout.astro` to prefer localStorage, then cookie, then environment signals
- [x] 1.3 Update the theme setter in `site/src/layouts/BaseLayout.astro` to keep localStorage and cookie in sync on user selection (cookie fallback when localStorage throws)
## 2. Umami Tracking For Theme Switch
- [x] 2.1 Define event emission on user-initiated theme changes using `window.umami.track("theme_switch", ...)` (guarded when Umami is missing)
- [x] 2.2 Use taxonomy fields for the event payload: `target_id=theme.switch.<theme>`, `placement=theme_notch`, include `theme` and `prev_theme` when available
- [x] 2.3 Ensure theme restoration on page load does NOT emit `theme_switch` (only explicit user interaction)
## 3. Verification
- [x] 3.1 Run `npm run build` and verify output includes cookie read fallback in the head init script
- [x] 3.2 Run `npm test` and ensure no new failures are introduced
- [x] 3.3 Manual: switch theme, reload, and confirm persistence; repeat with localStorage disabled to confirm cookie fallback

View File

@@ -1,92 +0,0 @@
## Context
Chrome Lighthouse runs against `https://santhoshj.com/` (desktop + mobile) report several audits that prevent a 100 score.
Inputs:
- Desktop report: `C:\Users\simpl\Downloads\santhoshj.com-20260210T182644.json`
- Mobile report: `C:\Users\simpl\Downloads\santhoshj.com-20260210T182538.json`
Key failing audits (non-exhaustive):
- Accessibility: `color-contrast`
- SEO: `robots-txt`, `crawlable-anchors`
- Best Practices: `inspector-issues` (Content Security Policy)
- Performance: `render-blocking-insight`, `image-delivery-insight`, `unminified-css`, `unused-css-rules`, `unused-javascript`, `cache-insight` (plus mobile LCP/TTI pressure)
Constraints:
- Site is a static Astro build served behind Docker Compose + reverse proxy.
- Some assets are third-party (YouTube thumbnails, CloudFront podcast images, Umami script). These can influence some performance/cache audits and must be handled carefully (reduce impact where possible, but avoid breaking content).
- Service worker script MUST remain at stable URL `/sw.js` and should not be versioned via query string.
## Goals / Non-Goals
**Goals:**
- Achieve a 100 Lighthouse rating on the homepage in all categories (Performance, Accessibility, Best Practices, SEO) using the audits provided.
- Make contrast compliant (WCAG AA) for secondary text and pill/chip labels.
- Ensure SEO hygiene:
- `robots.txt` includes a valid (absolute) sitemap URL
- interactive elements do not render anchor tags without `href`.
- Remove DevTools Issues panel findings related to CSP by implementing an explicit CSP baseline that matches site needs.
- Reduce render-blocking requests and improve asset delivery so mobile LCP/TTI is consistently fast.
**Non-Goals:**
- Redesigning the site's visual identity or typography scale.
- Removing all third-party content sources (YouTube thumbnails, podcast cover images) or analytics.
- Building a full PWA manifest/offline-first experience (out of scope).
## Decisions
1. **Contrast remediation via token-level CSS adjustments**
Rationale: Lighthouse flags specific selectors in card footers and pills. Fixing contrast at the token level (e.g., `--muted`, pill bg/fg) avoids per-component overrides and reduces regressions.
Alternatives:
- Component-local overrides (harder to maintain, easy to miss).
2. **Robots sitemap MUST be absolute**
Rationale: Lighthouse treats `Sitemap: /sitemap-index.xml` as invalid. Robots will be updated to point at the full absolute URL.
Alternatives:
- Switch to `sitemap.xml` only (not desired; site already emits sitemap-index).
3. **No anchor elements without href in rendered HTML**
Rationale: Lighthouse flags the media modal anchors because they exist at load time without `href` (populated later by JS). Fix by using buttons for non-navigational actions, and ensuring any `<a>` is rendered with a valid `href` in initial HTML (or not rendered until it has one).
Alternatives:
- Keep anchors and set `href="#"` (still crawlable but semantically wrong, and can degrade UX).
4. **CSP baseline implemented at the edge (reverse proxy), compatible with site JS**
Rationale: DevTools Issues panel reports CSP issues. Implement a CSP that matches current needs (site inline scripts, Umami, fonts, images, frames) and remove/avoid inline scripts where possible to keep CSP strict.
Alternatives:
- Avoid CSP entirely (does not resolve audit and leaves security posture ambiguous).
5. **Font delivery: prefer self-hosting to remove render-blocking third-party CSS**
Rationale: Lighthouse render-blocking points to Google Fonts stylesheet. Self-hosting WOFF2 and using `@font-face` reduces blocking and improves reliability.
Alternatives:
- Keep Google Fonts and rely on preload hints (still incurs third-party CSS request; harder to reach 100).
6. **CSS delivery: move global CSS into the build pipeline (minified output)**
Rationale: Lighthouse flags unminified/unused CSS. Keeping `global.css` as a raw file in `public/` makes it harder to guarantee minification/unused pruning. Prefer having Astro/Vite handle minification and (where possible) pruning.
Alternatives:
- Add a bespoke minify step for `public/styles/global.css` (works, but adds build complexity and can drift).
7. **Caching headers: stable URLs get revalidated; fingerprinted assets get long-lived caching**
Rationale: Lighthouse cache-lifetime audit penalizes short cache lifetimes on first-party CSS/JS. For assets that are not fingerprinted (e.g., `/sw.js`, possibly `/styles/global.css`), use `no-cache` or revalidation to avoid staleness. For fingerprinted build outputs, use long-lived caching.
Alternatives:
- Querystring versioning on SW (known pitfall; can break update chain).
## Risks / Trade-offs
- **[CSP breaks site behavior]** → Start with Report-Only CSP, verify in production, then enforce. Prefer eliminating inline scripts to avoid `unsafe-inline`.
- **[Self-hosting fonts changes appearance slightly]** → Keep the same Manrope font files and weights, verify typography visually.
- **[Optimizing images reduces perceived sharpness]** → Use responsive images and appropriate sizes; keep high-DPR support via srcset.
- **[Third-party cache lifetimes cannot be controlled]** → Focus on first-party cache headers and reduce critical path reliance on third-party where possible.
## Migration Plan
1. Reproduce Lighthouse findings from a clean Chrome profile (no extensions) for both mobile/desktop.
2. Apply fixes in small slices (contrast, robots/anchors, CSP, fonts, CSS pipeline/minify, image delivery).
3. Deploy behind the reverse proxy with Report-Only CSP first.
4. Re-run Lighthouse on production URL until 100 is reached and stable.
5. Enable enforced CSP after confirming no violations.
## Open Questions
- What exact CSP issue is being reported in the DevTools Issues panel (violation message)? Lighthouse only surfaces the issue type without sub-items.
- Do we want to keep Google Fonts or commit to self-hosting fonts for maximum Lighthouse consistency?
- For cache-lifetime scoring: do we want to introduce fingerprinted CSS output (preferred) or add explicit versioning for `/styles/global.css`?

View File

@@ -1,37 +0,0 @@
## Why
Increase technical robustness by remediating the specific issues flagged by Chrome Lighthouse so the primary surface (homepage) can achieve a 100 score across categories.
Lighthouse sources:
- `C:\Users\simpl\Downloads\santhoshj.com-20260210T182644.json` (desktop)
- `C:\Users\simpl\Downloads\santhoshj.com-20260210T182538.json` (mobile)
## What Changes
- Fix accessibility contrast failures (WCAG AA) for card metadata and pill chips.
- Fix SEO hygiene issues:
- `robots.txt` must reference a valid sitemap URL
- eliminate non-crawlable anchor markup that Lighthouse flags (e.g., anchors without `href` in modal UI)
- Eliminate Best Practices "Issues" panel findings related to Content Security Policy.
- Improve Performance audits that prevent a perfect score, primarily on mobile:
- optimize above-the-fold image delivery (thumbnails/covers)
- reduce render-blocking resources (font CSS)
- ensure CSS/JS delivery is optimized (minification and unused code reduction)
- improve cache lifetimes where applicable for first-party assets, and mitigate third-party cache lifetime penalties where feasible.
## Capabilities
### New Capabilities
- `asset-delivery-optimization`: Ensure critical assets (CSS/fonts/images) are delivered in a Lighthouse-friendly way (minified, non-blocking where possible, and appropriately cached) and that mobile LCP is consistently fast.
- `security-headers`: Define and implement a CSP baseline and related headers that eliminate DevTools "Issues" panel findings without breaking third-party integrations.
### Modified Capabilities
- `wcag-responsive-ui`: Strengthen the baseline to explicitly require sufficient text contrast for secondary/muted UI text and pill/chip labels so the site passes Lighthouse contrast checks.
- `seo-content-surface`: Strengthen robots + sitemap correctness and require that link-like UI uses crawlable markup (valid `href` when an anchor is used).
## Impact
- Affected UI/CSS: card footer metadata (`.muted`) and pill styling; modal CTA markup.
- Affected SEO assets: `robots.txt` sitemap line.
- Affected security posture: CSP and related headers (may require changes to how third-party scripts are loaded).
- Affected performance: image source/size strategy for thumbnails/covers; font delivery strategy; CSS/JS build pipeline and cache headers.

View File

@@ -1,47 +0,0 @@
## ADDED Requirements
### Requirement: Render-blocking resources are minimized
The site MUST minimize render-blocking resources on the critical path.
Font delivery MUST NOT rely on a render-blocking third-party stylesheet.
#### Scenario: Homepage avoids render-blocking font CSS
- **WHEN** Lighthouse audits the homepage
- **THEN** the Google Fonts stylesheet request is not present as a render-blocking resource (fonts are self-hosted or otherwise delivered without a blocking CSS request)
### Requirement: First-party CSS and JS are optimized for Lighthouse
First-party CSS and JS delivered to the browser MUST be minified in production builds.
The site MUST minimize unused CSS and unused JavaScript on the homepage.
#### Scenario: CSS is minified
- **WHEN** a production build is served
- **THEN** `styles/global.css` (or its replacement) is minified
#### Scenario: Homepage avoids unused JS penalties
- **WHEN** Lighthouse audits the homepage
- **THEN** the amount of unused JavaScript on initial load is below Lighthouse's failing threshold
### Requirement: Images are delivered efficiently
Images used on listing surfaces MUST be delivered in a size appropriate to their rendered dimensions.
For thumbnail-like images, the site SHOULD prefer image sources that support resizing or multiple resolutions when feasible.
#### Scenario: Podcast cover image is not oversized
- **WHEN** the homepage renders a podcast episode card
- **THEN** the fetched cover image size is reasonably close to the displayed size (no large wasted bytes flagged by Lighthouse)
### Requirement: Cache lifetimes are efficient for first-party assets
First-party static assets (CSS/JS/fonts/images served from the site origin) MUST be served with cache headers that enable efficient repeat visits.
Non-fingerprinted assets MUST be served with revalidation (e.g., `no-cache` or `max-age=0,must-revalidate`) to avoid staleness.
Fingerprinted assets (build outputs) MUST be served with a long-lived immutable cache policy.
#### Scenario: First-party CSS has efficient caching
- **WHEN** Lighthouse audits the homepage
- **THEN** first-party CSS cache lifetimes are not flagged as inefficient
#### Scenario: Service worker script is revalidated
- **WHEN** the browser checks `/sw.js` for updates
- **THEN** the HTTP cache is bypassed or revalidated so an updated service worker can be fetched promptly

View File

@@ -1,25 +0,0 @@
## ADDED Requirements
### Requirement: Content Security Policy baseline
The deployed site MUST include a Content Security Policy (CSP) that is compatible with the site's runtime behavior and third-party integrations.
The CSP MUST be strict enough to avoid DevTools Issues panel findings related to CSP and MUST NOT rely on a permissive `*` wildcard for script sources.
The CSP MUST allow:
- the site's own scripts and styles
- the configured analytics script origin (Umami)
- required image origins (e.g., YouTube thumbnail host and podcast image CDN)
- required frame origins (e.g., YouTube and Spotify embeds)
#### Scenario: No CSP issues logged
- **WHEN** a user loads the homepage in Chrome
- **THEN** no CSP-related issues are reported in the DevTools Issues panel
### Requirement: Avoid inline-script CSP violations
The site SHOULD minimize the use of inline scripts to avoid requiring `unsafe-inline` in CSP.
If inline scripts are necessary, the CSP MUST use a nonce-based or hash-based approach.
#### Scenario: Inline scripts do not require unsafe-inline
- **WHEN** the site is served with CSP enabled
- **THEN** the policy does not require `script-src 'unsafe-inline'` to function

View File

@@ -1,39 +0,0 @@
## MODIFIED Requirements
### Requirement: Sitemap and robots
The site MUST provide:
- `sitemap.xml` enumerating indexable pages
- `robots.txt` that allows indexing of indexable pages
The sitemap MUST include the blog surface routes:
- `/blog`
- blog post detail routes
- blog page detail routes
- blog category listing routes
`robots.txt` MUST include a `Sitemap:` directive with an absolute URL to the sitemap (or sitemap index) and MUST NOT use a relative sitemap URL.
#### Scenario: Sitemap is available
- **WHEN** a crawler requests `/sitemap.xml`
- **THEN** the server returns an XML sitemap listing `/`, `/videos`, `/podcast`, `/about`, and `/blog`
#### Scenario: Blog URLs appear in sitemap
- **WHEN** WordPress content is available in the cache at build time
- **THEN** the generated sitemap includes the blog detail URLs for those items
#### Scenario: robots.txt includes absolute sitemap URL
- **WHEN** a crawler requests `/robots.txt`
- **THEN** the response includes a `Sitemap:` directive with an absolute URL (e.g., `https://<domain>/sitemap-index.xml`) that Lighthouse and crawlers can parse
## ADDED Requirements
### Requirement: Crawlable link markup
The site MUST NOT render anchor elements (`<a>`) without an `href` attribute.
Interactive UI that does not navigate MUST use a `<button>` element (or equivalent role) instead of a placeholder anchor.
If an anchor is used for navigation, it MUST include a valid `href` in the initial HTML (not only populated later by client-side JavaScript).
#### Scenario: Modal CTAs are crawlable
- **WHEN** the homepage is loaded and the media modal markup exists in the DOM
- **THEN** any CTA anchors within the modal have valid `href` values, or are not rendered as anchors until an `href` is available

View File

@@ -1,18 +0,0 @@
## ADDED Requirements
### Requirement: Color contrast for secondary UI text and chips
The site MUST meet WCAG 2.2 AA contrast requirements for non-decorative text, including secondary/muted metadata text and pill/chip labels.
This includes (but is not limited to):
- card footer date and view-count text
- pill/chip labels (e.g., source labels)
The contrast ratio MUST be at least 4.5:1 for normal text.
#### Scenario: Card metadata contrast passes
- **WHEN** a content card is rendered with date and view-count metadata
- **THEN** the metadata text has a contrast ratio of at least 4.5:1 against its background
#### Scenario: Pill label contrast passes
- **WHEN** a pill/chip label is rendered (e.g., a source label)
- **THEN** the pill label text has a contrast ratio of at least 4.5:1 against the pill background

View File

@@ -1,39 +0,0 @@
## 1. Baseline And Repro
- [ ] 1.1 Run Lighthouse from a clean Chrome profile (no extensions) for both Mobile and Desktop and save reports (JSON)
- [ ] 1.2 Record current failing audits and their affected selectors/URLs (from the Lighthouse "details" tables)
## 2. Accessibility Contrast
- [ ] 2.1 Adjust global CSS tokens/styles so `.muted` card metadata meets 4.5:1 contrast on cards
- [ ] 2.2 Adjust pill/chip background + text colors to meet 4.5:1 contrast (e.g., `.pill` and source variants)
- [ ] 2.3 Re-run Lighthouse accessibility category and confirm `color-contrast` passes
## 3. SEO Hygiene (robots + crawlable links)
- [ ] 3.1 Update `site/public/robots.txt` to use an absolute sitemap URL (e.g., `Sitemap: https://santhoshj.com/sitemap-index.xml`)
- [ ] 3.2 Fix non-crawlable anchors in the media modal by ensuring anchors always have `href` in initial HTML or switching to buttons until navigable
- [ ] 3.3 Re-run Lighthouse SEO category and confirm `robots-txt` and `crawlable-anchors` pass
## 4. CSP / Best Practices
- [ ] 4.1 Identify the exact CSP-related DevTools Issue message (Chrome DevTools → Issues) and capture the text
- [ ] 4.2 Implement a CSP baseline at the reverse proxy/origin that allows required resources (self + Umami + image/frame origins) and avoids permissive wildcards
- [ ] 4.3 Reduce inline scripts that force `unsafe-inline` (move registration / modal scripts to external files or use nonce/hash approach)
- [ ] 4.4 Re-run Lighthouse Best Practices and confirm `inspector-issues` passes
## 5. Performance: Fonts, CSS/JS, And Images
- [ ] 5.1 Remove render-blocking third-party font stylesheet by self-hosting Manrope and loading via `@font-face`
- [ ] 5.2 Ensure production CSS is minified (move global CSS into the build pipeline or add a build minification step)
- [ ] 5.3 Reduce unused CSS on the homepage (prune unused selectors or split critical vs non-critical styles)
- [ ] 5.4 Reduce unused JS on the homepage (remove unnecessary scripts; ensure analytics is async/defer; avoid extra inline code)
- [ ] 5.5 Improve thumbnail image delivery (use responsive `srcset` / resized sources where feasible; avoid oversized podcast covers)
- [ ] 5.6 Improve cache lifetimes for first-party static assets (fingerprint + immutable cache for build assets; revalidate non-fingerprinted)
- [ ] 5.7 Re-run Lighthouse Performance (mobile + desktop) and confirm 100 score
## 6. Verification
- [ ] 6.1 Run `npm run build` and ensure build succeeds
- [ ] 6.2 Smoke test site locally (`npm run preview`) including modal, analytics script load, and service worker registration
- [ ] 6.3 Deploy and confirm production Lighthouse scores are 100/100/100/100

View File

@@ -19,6 +19,7 @@ When Umami is enabled, the site MUST support custom event emission for:
- `outbound_click` - `outbound_click`
- `media_preview` - `media_preview`
- `media_preview_close` - `media_preview_close`
- `theme_switch`
- a general click interaction event for all instrumented clickable items (per the site tracking taxonomy) - a general click interaction event for all instrumented clickable items (per the site tracking taxonomy)
Each emitted event MUST include enough properties to segment reports by platform and placement when applicable. Each emitted event MUST include enough properties to segment reports by platform and placement when applicable.
@@ -63,6 +64,26 @@ For content-related links (clickables representing a specific piece of content),
- **WHEN** a user clicks a CTA inside the media modal (e.g., "View on YouTube") - **WHEN** a user clicks a CTA inside the media modal (e.g., "View on YouTube")
- **THEN** the system emits a `cta_click` event with `target_id`, `placement=media_modal`, `platform`, and `target_url` - **THEN** the system emits a `cta_click` event with `target_id`, `placement=media_modal`, `platform`, and `target_url`
### Requirement: Theme switch tracking event
When Umami is enabled, the site MUST emit a custom event when the user changes theme via the theme switcher UI.
The site MUST emit the event using Umami's JavaScript API (`umami.track(...)`) so runtime properties can be included.
The event name MUST be `theme_switch`.
The emitted event MUST include, at minimum:
- `target_id`
- `placement`
- `theme`
#### Scenario: Theme switch emits event
- **WHEN** a user selects `high-contrast` in the theme switcher notch
- **THEN** the site emits a `theme_switch` event with `theme=high-contrast` and a stable `target_id`
#### Scenario: Missing Umami does not break switching
- **WHEN** Umami is not configured or the Umami script is not present
- **THEN** theme switching and persistence still work and no browser error is thrown
### Requirement: Environment configuration ### Requirement: Environment configuration
The site MUST support configuration of Umami parameters (at minimum: website ID and script URL) without requiring code changes. The site MUST support configuration of Umami parameters (at minimum: website ID and script URL) without requiring code changes.

View File

@@ -133,3 +133,25 @@ The taxonomy MUST prohibit including personally identifiable information (PII) i
#### Scenario: Tracking includes only categorical metadata #### Scenario: Tracking includes only categorical metadata
- **WHEN** tracking metadata is defined for a clickable item - **WHEN** tracking metadata is defined for a clickable item
- **THEN** it contains only categorical identifiers (ids, placements, domains) and does not include user-provided content - **THEN** it contains only categorical identifiers (ids, placements, domains) and does not include user-provided content
### Requirement: Theme switch event taxonomy
The tracking taxonomy MUST define an event for theme switching.
The event name MUST be `theme_switch`.
The `theme_switch` event MUST include, at minimum:
- `target_id`
- `placement`
- `theme`
The event MUST include `prev_theme` when it is available.
The taxonomy MUST define the `target_id` namespace for theme switching as:
- `theme.switch.<theme>`
The taxonomy MUST define the `placement` value for the theme switcher notch as:
- `theme_notch`
#### Scenario: Theme switch target_id is deterministic
- **WHEN** a user selects `light` theme using the theme notch
- **THEN** the event is emitted with `target_id=theme.switch.light` and `placement=theme_notch`

View File

@@ -33,6 +33,30 @@ Persistence MUST be stored locally in the browser (e.g., localStorage).
- **WHEN** the user selects `light` theme and reloads the page - **WHEN** the user selects `light` theme and reloads the page
- **THEN** the `light` theme remains active - **THEN** the `light` theme remains active
### Requirement: Theme persistence works across visits with fallback
The site MUST persist the user's theme selection across visits so returning users see the last-selected theme.
The site MUST use client-side persistence and MUST support a fallback mechanism:
- Primary: `localStorage`
- Fallback: a client-side cookie
The effective theme selection order MUST be:
1) Stored theme in `localStorage` (if available)
2) Stored theme in a cookie (if localStorage is unavailable)
3) Default selection using environment signals
#### Scenario: LocalStorage persists across a later visit
- **WHEN** a user selects `light` theme and later returns to the site in the same browser
- **THEN** the site initializes in `light` theme before first paint
#### Scenario: Cookie fallback is used when localStorage is unavailable
- **WHEN** the browser environment blocks `localStorage` access and the user selects `dark` theme
- **THEN** the theme is persisted using a client-side cookie and is restored on the next visit
#### Scenario: No persistence available falls back to defaults
- **WHEN** both `localStorage` and cookie persistence are unavailable
- **THEN** the site falls back to default theme selection using environment signals
### Requirement: Default theme selection ### Requirement: Default theme selection
If the user has not explicitly selected a theme, the site MUST choose a default theme using environment signals. If the user has not explicitly selected a theme, the site MUST choose a default theme using environment signals.

View File

@@ -343,12 +343,14 @@ textarea:focus-visible {
justify-content: center; justify-content: center;
width: 100%; width: 100%;
height: 100%; height: 100%;
font-weight: 900;
letter-spacing: -0.02em;
font-size: 13px;
line-height: 1; line-height: 1;
} }
.theme-notch-glyph svg {
width: 18px;
height: 18px;
}
.theme-notch-panel { .theme-notch-panel {
position: absolute; position: absolute;
right: 60px; right: 60px;

View File

@@ -53,6 +53,7 @@ const canonicalUrl = `${siteUrl}${canonicalPath.startsWith("/") ? canonicalPath
<script is:inline> <script is:inline>
(() => { (() => {
const THEME_KEY = "site.theme"; const THEME_KEY = "site.theme";
const COOKIE_KEY = "site_theme";
const validate = (v) => (v === "dark" || v === "light" || v === "high-contrast" ? v : null); const validate = (v) => (v === "dark" || v === "light" || v === "high-contrast" ? v : null);
@@ -64,6 +65,26 @@ const canonicalUrl = `${siteUrl}${canonicalPath.startsWith("/") ? canonicalPath
} }
}; };
const readCookie = () => {
try {
const parts = document.cookie ? document.cookie.split(";") : [];
for (let i = 0; i < parts.length; i++) {
const p = parts[i].trim();
if (!p) continue;
if (!p.startsWith(COOKIE_KEY + "=")) continue;
const raw = p.slice((COOKIE_KEY + "=").length);
try {
return validate(decodeURIComponent(raw));
} catch {
return validate(raw);
}
}
return null;
} catch {
return null;
}
};
const forcedColors = () => { const forcedColors = () => {
try { try {
return window.matchMedia && window.matchMedia("(forced-colors: active)").matches; return window.matchMedia && window.matchMedia("(forced-colors: active)").matches;
@@ -80,7 +101,7 @@ const canonicalUrl = `${siteUrl}${canonicalPath.startsWith("/") ? canonicalPath
} }
}; };
const stored = readStored(); const stored = readStored() || readCookie();
const theme = stored || (forcedColors() ? "high-contrast" : prefersLight() ? "light" : "dark"); const theme = stored || (forcedColors() ? "high-contrast" : prefersLight() ? "light" : "dark");
document.documentElement.dataset.theme = theme; document.documentElement.dataset.theme = theme;
})(); })();
@@ -203,7 +224,25 @@ const canonicalUrl = `${siteUrl}${canonicalPath.startsWith("/") ? canonicalPath
aria-expanded="false" aria-expanded="false"
data-theme-notch-handle data-theme-notch-handle
> >
<span class="theme-notch-glyph" aria-hidden="true">Theme</span> <span class="theme-notch-glyph" aria-hidden="true">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="4" y1="6" x2="20" y2="6"></line>
<line x1="4" y1="12" x2="20" y2="12"></line>
<line x1="4" y1="18" x2="20" y2="18"></line>
<circle cx="9" cy="6" r="2"></circle>
<circle cx="15" cy="12" r="2"></circle>
<circle cx="11" cy="18" r="2"></circle>
</svg>
</span>
</button> </button>
</aside> </aside>
@@ -285,13 +324,26 @@ const canonicalUrl = `${siteUrl}${canonicalPath.startsWith("/") ? canonicalPath
<script is:inline> <script is:inline>
(() => { (() => {
const THEME_KEY = "site.theme"; const THEME_KEY = "site.theme";
const COOKIE_KEY = "site_theme";
const ONE_YEAR_SECONDS = 31536000;
const validate = (v) => (v === "dark" || v === "light" || v === "high-contrast" ? v : null); const validate = (v) => (v === "dark" || v === "light" || v === "high-contrast" ? v : null);
const setCookieTheme = (theme) => {
const v = validate(theme);
if (!v) return;
const secure = window.location && window.location.protocol === "https:" ? "; Secure" : "";
const cookie = `${COOKIE_KEY}=${encodeURIComponent(v)}; Max-Age=${ONE_YEAR_SECONDS}; Path=/; SameSite=Lax${secure}`;
document.cookie = cookie;
};
const setTheme = (next, opts) => { const setTheme = (next, opts) => {
const theme = validate(next); const theme = validate(next);
if (!theme) return; if (!theme) return;
const prevTheme = validate(document.documentElement.dataset.theme);
if (prevTheme === theme) return;
if (opts && opts.withTransition) { if (opts && opts.withTransition) {
document.documentElement.dataset.themeTransition = "on"; document.documentElement.dataset.themeTransition = "on";
} }
@@ -304,6 +356,26 @@ const canonicalUrl = `${siteUrl}${canonicalPath.startsWith("/") ? canonicalPath
// ignore // ignore
} }
try {
setCookieTheme(theme);
} catch {
// ignore
}
try {
if (typeof window.umami !== "undefined") {
const payload = {
target_id: `theme.switch.${theme}`,
placement: "theme_notch",
theme,
};
if (prevTheme) payload.prev_theme = prevTheme;
window.umami.track("theme_switch", payload);
}
} catch {
// ignore
}
if (opts && opts.withTransition) { if (opts && opts.withTransition) {
window.setTimeout(() => { window.setTimeout(() => {
delete document.documentElement.dataset.themeTransition; delete document.documentElement.dataset.themeTransition;