Now I remember the theme
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-02-10
|
||||
created: 2026-02-11
|
||||
62
openspec/changes/archive/2026-02-11-remember-theme/design.md
Normal file
62
openspec/changes/archive/2026-02-11-remember-theme/design.md
Normal 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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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`
|
||||
@@ -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
|
||||
17
openspec/changes/archive/2026-02-11-remember-theme/tasks.md
Normal file
17
openspec/changes/archive/2026-02-11-remember-theme/tasks.md
Normal 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
|
||||
@@ -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`?
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -19,6 +19,7 @@ When Umami is enabled, the site MUST support custom event emission for:
|
||||
- `outbound_click`
|
||||
- `media_preview`
|
||||
- `media_preview_close`
|
||||
- `theme_switch`
|
||||
- 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.
|
||||
@@ -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")
|
||||
- **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
|
||||
The site MUST support configuration of Umami parameters (at minimum: website ID and script URL) without requiring code changes.
|
||||
|
||||
|
||||
@@ -133,3 +133,25 @@ The taxonomy MUST prohibit including personally identifiable information (PII) i
|
||||
#### Scenario: Tracking includes only categorical metadata
|
||||
- **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
|
||||
|
||||
### 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`
|
||||
|
||||
@@ -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
|
||||
- **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
|
||||
If the user has not explicitly selected a theme, the site MUST choose a default theme using environment signals.
|
||||
|
||||
|
||||
@@ -343,12 +343,14 @@ textarea:focus-visible {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.02em;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.theme-notch-glyph svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.theme-notch-panel {
|
||||
position: absolute;
|
||||
right: 60px;
|
||||
|
||||
@@ -53,6 +53,7 @@ const canonicalUrl = `${siteUrl}${canonicalPath.startsWith("/") ? canonicalPath
|
||||
<script is:inline>
|
||||
(() => {
|
||||
const THEME_KEY = "site.theme";
|
||||
const COOKIE_KEY = "site_theme";
|
||||
|
||||
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 = () => {
|
||||
try {
|
||||
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");
|
||||
document.documentElement.dataset.theme = theme;
|
||||
})();
|
||||
@@ -203,7 +224,25 @@ const canonicalUrl = `${siteUrl}${canonicalPath.startsWith("/") ? canonicalPath
|
||||
aria-expanded="false"
|
||||
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>
|
||||
</aside>
|
||||
|
||||
@@ -285,13 +324,26 @@ const canonicalUrl = `${siteUrl}${canonicalPath.startsWith("/") ? canonicalPath
|
||||
<script is:inline>
|
||||
(() => {
|
||||
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 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 theme = validate(next);
|
||||
if (!theme) return;
|
||||
|
||||
const prevTheme = validate(document.documentElement.dataset.theme);
|
||||
if (prevTheme === theme) return;
|
||||
|
||||
if (opts && opts.withTransition) {
|
||||
document.documentElement.dataset.themeTransition = "on";
|
||||
}
|
||||
@@ -304,6 +356,26 @@ const canonicalUrl = `${siteUrl}${canonicalPath.startsWith("/") ? canonicalPath
|
||||
// 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) {
|
||||
window.setTimeout(() => {
|
||||
delete document.documentElement.dataset.themeTransition;
|
||||
|
||||
Reference in New Issue
Block a user