Compare commits

30 Commits

Author SHA1 Message Date
cd38afc013 fix improve custom nginx error pages 2026-04-19 17:10:33 -04:00
67290567fb graphified the codebase 2026-04-19 16:40:44 -04:00
da5263cd73 Fix categories for multiple blog posts.
Some checks are pending
ci / site (push) Waiting to run
publish-image / publish (push) Waiting to run
2026-04-19 16:02:43 -04:00
d8b46cc6ca fix views count in content.json 2026-04-19 16:02:19 -04:00
4c481e0dc1 agents md 2026-04-19 15:57:24 -04:00
63db318ce0 commit from prod
Some checks failed
ci / site (push) Has been cancelled
publish-image / publish (push) Has been cancelled
2026-04-19 19:50:34 +00:00
d0dd9a0709 review 2026-04-11 03:29:19 -04:00
dda37a4969 mobile view issue
Some checks failed
publish-image / publish (push) Has been cancelled
ci / site (push) Has been cancelled
2026-02-10 23:55:50 -05:00
2b4c8a79e3 mobile view issue
Some checks failed
ci / site (push) Has been cancelled
publish-image / publish (push) Has been cancelled
2026-02-10 23:50:21 -05:00
65b51d573a nginx tuning
Some checks failed
ci / site (push) Has been cancelled
publish-image / publish (push) Has been cancelled
2026-02-10 23:32:56 -05:00
fd3ebc6115 nginx tuning
Some checks failed
ci / site (push) Has been cancelled
publish-image / publish (push) Has been cancelled
2026-02-10 23:29:50 -05:00
a6e40f8b54 final documentation 2026-02-10 23:13:37 -05:00
439b886a1b UI touch ups
Some checks failed
ci / site (push) Has been cancelled
publish-image / publish (push) Has been cancelled
2026-02-10 23:06:52 -05:00
07d8787972 lighthouse fixes 2026-02-10 22:37:29 -05:00
26a8c97841 issue with notch
Some checks failed
ci / site (push) Has been cancelled
publish-image / publish (push) Has been cancelled
2026-02-10 20:57:28 -05:00
f50a828535 Now I remember the theme
Some checks failed
ci / site (push) Has been cancelled
publish-image / publish (push) Has been cancelled
2026-02-10 20:38:38 -05:00
70710239c7 Theming done
Some checks failed
ci / site (push) Has been cancelled
publish-image / publish (push) Has been cancelled
2026-02-10 20:10:06 -05:00
6cb4d55241 SW implementation done
Some checks failed
ci / site (push) Has been cancelled
publish-image / publish (push) Has been cancelled
2026-02-10 18:32:24 -05:00
9fee6c8af7 SW still not fixed
Some checks failed
ci / site (push) Has been cancelled
publish-image / publish (push) Has been cancelled
2026-02-10 18:12:25 -05:00
daac2eec20 fix for SW
Some checks failed
ci / site (push) Has been cancelled
publish-image / publish (push) Has been cancelled
2026-02-10 18:02:37 -05:00
57ad560b01 fix for SR
Some checks failed
ci / site (push) Has been cancelled
publish-image / publish (push) Has been cancelled
2026-02-10 17:54:13 -05:00
5d07e57256 reduce bounce rate
Some checks failed
ci / site (push) Has been cancelled
publish-image / publish (push) Has been cancelled
2026-02-10 17:36:34 -05:00
ac3de3e142 lazy-loading done
Some checks failed
ci / site (push) Has been cancelled
publish-image / publish (push) Has been cancelled
2026-02-10 15:59:03 -05:00
7bd51837de add favicon
Some checks failed
ci / site (push) Has been cancelled
publish-image / publish (push) Has been cancelled
2026-02-10 07:09:12 -05:00
b61146b145 Service worker fix
Some checks failed
ci / site (push) Has been cancelled
publish-image / publish (push) Has been cancelled
2026-02-10 06:46:29 -05:00
7cb72b2746 Fix umami script not added
Some checks failed
ci / site (push) Has been cancelled
publish-image / publish (push) Has been cancelled
2026-02-10 06:17:21 -05:00
35afd9208f Cron ready refresh script
Some checks failed
ci / site (push) Has been cancelled
publish-image / publish (push) Has been cancelled
2026-02-10 05:32:05 -05:00
e2ef436d34 refresh content
Some checks failed
ci / site (push) Has been cancelled
publish-image / publish (push) Has been cancelled
2026-02-10 05:06:24 -05:00
1d3d68df7b last updated timezone changed
Some checks failed
ci / site (push) Has been cancelled
publish-image / publish (push) Has been cancelled
2026-02-10 05:03:07 -05:00
12e7eae95a Fix service workers
Some checks failed
ci / site (push) Has been cancelled
publish-image / publish (push) Has been cancelled
2026-02-10 04:54:05 -05:00
196 changed files with 177747 additions and 850 deletions

View File

@@ -0,0 +1,16 @@
{
"permissions": {
"allow": [
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__navigate_page",
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__performance_start_trace",
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__performance_analyze_insight",
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__evaluate_script",
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__emulate",
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__lighthouse_audit",
"Bash(node -e ':*)",
"Bash(node:*)",
"Bash(npx lighthouse:*)",
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__list_network_requests"
]
}
}

View File

@@ -23,3 +23,10 @@ jobs:
- run: npm run format:check
- run: npm test
- run: npm run build
- run: npm run verify:lighthouse
- uses: actions/upload-artifact@v4
if: always()
with:
name: lighthouse-reports
path: site/lighthouse-reports
if-no-files-found: warn

View File

@@ -66,3 +66,7 @@ jobs:
BUILD_SHA=${{ github.sha }}
BUILD_DATE=${{ github.run_started_at }}
BUILD_REF=${{ github.server_url }}/${{ github.repository }}
PUBLIC_SITE_URL=${{ secrets.PUBLIC_SITE_URL }}
PUBLIC_UMAMI_SCRIPT_URL=${{ secrets.PUBLIC_UMAMI_SCRIPT_URL }}
PUBLIC_UMAMI_WEBSITE_ID=${{ secrets.PUBLIC_UMAMI_WEBSITE_ID }}
PUBLIC_ENABLE_SW=${{ secrets.PUBLIC_ENABLE_SW }}

View File

@@ -0,0 +1,13 @@
{"timestamp":"2026-04-19T19:50:44.595Z","type":"auto_nudge_skipped","reason":"unmanaged_session"}
{"timestamp":"2026-04-19T19:58:20.085Z","type":"auto_nudge_skipped","reason":"unmanaged_session"}
{"timestamp":"2026-04-19T20:20:08.439Z","type":"auto_nudge_skipped","reason":"unmanaged_session"}
{"timestamp":"2026-04-19T20:20:19.104Z","type":"auto_nudge_skipped","reason":"unmanaged_session"}
{"timestamp":"2026-04-19T20:20:19.841Z","type":"auto_nudge_skipped","reason":"unmanaged_session"}
{"timestamp":"2026-04-19T20:21:03.727Z","type":"auto_nudge_skipped","reason":"unmanaged_session"}
{"timestamp":"2026-04-19T20:21:39.245Z","type":"auto_nudge_skipped","reason":"unmanaged_session"}
{"timestamp":"2026-04-19T20:26:48.792Z","type":"auto_nudge_skipped","reason":"unmanaged_session"}
{"timestamp":"2026-04-19T20:34:30.688Z","type":"auto_nudge_skipped","reason":"unmanaged_session"}
{"timestamp":"2026-04-19T20:36:09.732Z","type":"auto_nudge_skipped","reason":"unmanaged_session"}
{"timestamp":"2026-04-19T20:37:13.560Z","type":"auto_nudge_skipped","reason":"unmanaged_session"}
{"timestamp":"2026-04-19T20:38:03.706Z","type":"auto_nudge_skipped","reason":"unmanaged_session"}
{"timestamp":"2026-04-19T20:40:19.442Z","type":"auto_nudge_skipped","reason":"unmanaged_session"}

View File

@@ -0,0 +1,13 @@
{"timestamp":"2026-04-19T19:50:44.463Z","type":"agent-turn-complete","thread_id":"019da74a-1f5a-7792-8d11-332de1e9acd8","turn_id":"019da74a-cc13-7e13-b725-69e6539b1bd2","input_preview":"Generate a file named AGENTS.md that serves as a contributor guide for this repository.\r\nYour goal i","output_preview":"Created the contributor guide as requested.\n\n- Added a new root guide with the title `Repository Guidelines`, structured sections, and repo-specific instructions.\n- Tailored content to this projects "}
{"timestamp":"2026-04-19T19:58:19.942Z","type":"agent-turn-complete","thread_id":"019da74a-1f5a-7792-8d11-332de1e9acd8","turn_id":"019da752-6dac-74d0-b199-6ff08e742315","input_preview":"Generate a file named AGENTS.md that serves as a contributor guide for this repository.\r\nYour goal i; Help me resolve this issue: git pull origin main\nFrom https://git.phostrich.com/santhoshj/astro-webs","output_preview":"Your local `main` and remote `origin/main` both have new commits, and your Git is configured to only allow fast-forward pulls.\n\nUse one of these paths:\n\n1. **Merge (safest/easiest)**\n```bash\ngit check"}
{"timestamp":"2026-04-19T20:20:08.283Z","type":"agent-turn-complete","thread_id":"019da764-6890-7ce2-b7e4-4dd7d06c7c62","turn_id":"019da764-68c6-7131-8b58-e6979cf114fe","input_preview":"You are a graphify semantic extraction subagent. Read the listed files and extract a knowledge graph","output_preview":"{\"nodes\":[{\"id\":\"agents_repo_guidelines\",\"label\":\"Repository Guidelines\",\"type\":\"document\",\"source_file\":\"AGENTS.md\"},{\"id\":\"repo_site_astro_app\",\"label\":\"Astro app in site/\",\"type\":\"project_structure"}
{"timestamp":"2026-04-19T20:20:18.958Z","type":"agent-turn-complete","thread_id":"019da764-693c-7282-a0fa-0afef65c24d8","turn_id":"019da764-6968-7ba1-97c6-5803d010d84a","input_preview":"You are a graphify semantic extraction subagent. Read the listed files and extract a knowledge graph","output_preview":"{\"nodes\":[{\"id\":\"blogs_section_wordpress_content_source\",\"label\":\"wordpress-content-source\",\"type\":\"capability\"},{\"id\":\"blogs_section_blog_section_surface\",\"label\":\"blog-section-surface\",\"type\":\"capab"}
{"timestamp":"2026-04-19T20:20:19.694Z","type":"agent-turn-complete","thread_id":"019da764-69f7-7042-9f7a-68feb5630b82","turn_id":"019da764-6a24-76c1-876f-260512cfb6df","input_preview":"You are a graphify semantic extraction subagent. Read the listed files and extract a knowledge graph","output_preview":"{\"nodes\":[{\"id\":\"theme_persistence_change\",\"label\":\"Theme Persistence Change\",\"type\":\"change\"},{\"id\":\"typography_refresh_change\",\"label\":\"Typography Refresh Change\",\"type\":\"change\"},{\"id\":\"umami_error"}
{"timestamp":"2026-04-19T20:21:03.588Z","type":"agent-turn-complete","thread_id":"019da764-6977-7362-ac71-968e690b7cb3","turn_id":"019da764-69ae-7a30-9cc9-3ce5a6c8887e","input_preview":"You are a graphify semantic extraction subagent. Read the listed files and extract a knowledge graph","output_preview":"{\"nodes\":[{\"id\":\"change_lighthouse_fixes\",\"label\":\"2026-02-10 lighthouse fixes change\",\"type\":\"change\"},{\"id\":\"doc_lighthouse_baseline_audits\",\"label\":\"lighthouse baseline audits doc\",\"type\":\"document"}
{"timestamp":"2026-04-19T20:21:39.107Z","type":"agent-turn-complete","thread_id":"019da764-69bb-7f12-99b6-f1eac564766e","turn_id":"019da764-69e7-7a43-867d-cd48129d6637","input_preview":"You are a graphify semantic extraction subagent. Read the listed files and extract a knowledge graph","output_preview":"{\"nodes\":[{\"id\":\"chunk6_input_set\",\"label\":\"Chunk 6 Input Set\",\"type\":\"input_chunk\"},{\"id\":\"workspace_missing_sources\",\"label\":\"Workspace Missing Sources\",\"type\":\"review_flag\"},{\"id\":\"2026_02_11_video"}
{"timestamp":"2026-04-19T20:26:48.653Z","type":"agent-turn-complete","thread_id":"019da768-9a03-7f22-bbf7-7ef2c96a4e01","turn_id":"019da768-9ac9-7de0-ba23-5b3afd06de94","input_preview":"You are a graphify semantic extraction subagent. Read the listed files and extract a knowledge graph","output_preview":"{\"nodes\":[{\"id\":\"spec_seo_content_surface\",\"label\":\"SEO content surface spec\",\"type\":\"spec\"},{\"id\":\"spec_social_content_aggregation\",\"label\":\"Social content aggregation spec\",\"type\":\"spec\"},{\"id\":\"spe"}
{"timestamp":"2026-04-19T20:34:30.551Z","type":"agent-turn-complete","thread_id":"019da760-d06b-79e2-86bb-c856c5715244","turn_id":"019da761-00de-7c53-b694-155ddc71aeb3","input_preview":"$graphify","output_preview":"- Built the graph for `C:\\dev\\fast-website` and wrote the durable outputs to [graph.json](C:/dev/fast-website/graphify-out/graph.json), [graph.html](C:/dev/fast-website/graphify-out/graph.html), [GRAP"}
{"timestamp":"2026-04-19T20:36:09.595Z","type":"agent-turn-complete","thread_id":"019da760-d06b-79e2-86bb-c856c5715244","turn_id":"019da774-c11d-7c51-9966-d8a6f6c60f88","input_preview":"$graphify; Yes","output_preview":"The bridge is the shared concern around motion and cross-surface UX, not a literal “Youtube” dependency.\n\nPath:\n- `2026-02-10 lighthouse fixes change` → `media-modal capability` \n- `2026-02-10 lighth"}
{"timestamp":"2026-04-19T20:37:13.420Z","type":"agent-turn-complete","thread_id":"019da760-d06b-79e2-86bb-c856c5715244","turn_id":"019da776-08d7-7cf0-84e3-71ba76732047","input_preview":"$graphify; Yes; how do I ask a coding agent to refer to the graph?","output_preview":"Tell it explicitly what source of truth to use and what not to do.\n\nUse a prompt like:\n\n```text\nRefer to the graph in C:\\dev\\fast-website\\graphify-out\\graph.json and the report in C:\\dev\\fast-website\\"}
{"timestamp":"2026-04-19T20:38:03.567Z","type":"agent-turn-complete","thread_id":"019da760-d06b-79e2-86bb-c856c5715244","turn_id":"019da776-ca28-7760-9c25-31b0be695a3a","input_preview":"$graphify; Yes; how do I ask a coding agent to refer to the graph?; Yes","output_preview":"Use this:\n\n```text\nYou are working against a knowledge graph for this repo.\n\nPrimary sources:\n- C:\\dev\\fast-website\\graphify-out\\graph.json\n- C:\\dev\\fast-website\\graphify-out\\GRAPH_REPORT.md\n\nRules:\n-"}
{"timestamp":"2026-04-19T20:40:19.299Z","type":"agent-turn-complete","thread_id":"019da778-2cc4-74c3-b45d-969dc53a1911","turn_id":"019da778-b67b-7333-9738-3ba39cd71bc6","input_preview":"You are working against a knowledge graph for this repo.\n\n Primary sources:\n - C:\\dev\\fast-website","output_preview":"I have loaded the knowledge graph for `fast-website`. Here's what it contains:\n\n**Graph Stats**\n- 235 nodes · 221 edges · 57 communities\n- 89% extracted · 11% inferred relationships\n\n**Key Abstraction"}

8
.omx/metrics.json Normal file
View File

@@ -0,0 +1,8 @@
{
"total_turns": 13,
"session_turns": 13,
"last_activity": "2026-04-19T20:40:19.306Z",
"session_input_tokens": 0,
"session_output_tokens": 0,
"session_total_tokens": 0
}

View File

@@ -0,0 +1,6 @@
{
"last_turn_at": "2026-04-19T20:40:19.309Z",
"turn_count": 13,
"last_progress_at": "2026-04-19T20:40:19.309Z",
"last_agent_output": "I have loaded the knowledge graph for `fast-website`. Here's what it contains:\n\n**Graph Stats**\n- 23"
}

View File

@@ -0,0 +1,18 @@
{
"recent_turns": {
"019da74a-1f5a-7792-8d11-332de1e9acd8|019da74a-cc13-7e13-b725-69e6539b1bd2|agent-turn-complete": 1776628244462,
"019da74a-1f5a-7792-8d11-332de1e9acd8|019da752-6dac-74d0-b199-6ff08e742315|agent-turn-complete": 1776628699940,
"019da764-6890-7ce2-b7e4-4dd7d06c7c62|019da764-68c6-7131-8b58-e6979cf114fe|agent-turn-complete": 1776630008280,
"019da764-693c-7282-a0fa-0afef65c24d8|019da764-6968-7ba1-97c6-5803d010d84a|agent-turn-complete": 1776630018956,
"019da764-69f7-7042-9f7a-68feb5630b82|019da764-6a24-76c1-876f-260512cfb6df|agent-turn-complete": 1776630019692,
"019da764-6977-7362-ac71-968e690b7cb3|019da764-69ae-7a30-9cc9-3ce5a6c8887e|agent-turn-complete": 1776630063586,
"019da764-69bb-7f12-99b6-f1eac564766e|019da764-69e7-7a43-867d-cd48129d6637|agent-turn-complete": 1776630099105,
"019da768-9a03-7f22-bbf7-7ef2c96a4e01|019da768-9ac9-7de0-ba23-5b3afd06de94|agent-turn-complete": 1776630408651,
"019da760-d06b-79e2-86bb-c856c5715244|019da761-00de-7c53-b694-155ddc71aeb3|agent-turn-complete": 1776630870549,
"019da760-d06b-79e2-86bb-c856c5715244|019da774-c11d-7c51-9966-d8a6f6c60f88|agent-turn-complete": 1776630969593,
"019da760-d06b-79e2-86bb-c856c5715244|019da776-08d7-7cf0-84e3-71ba76732047|agent-turn-complete": 1776631033418,
"019da760-d06b-79e2-86bb-c856c5715244|019da776-ca28-7760-9c25-31b0be695a3a|agent-turn-complete": 1776631083565,
"019da778-2cc4-74c3-b45d-969dc53a1911|019da778-b67b-7333-9738-3ba39cd71bc6|agent-turn-complete": 1776631219297
},
"last_event_at": "2026-04-19T20:40:19.298Z"
}

View File

@@ -0,0 +1,5 @@
{
"last_nudged_by_team": {},
"last_idle_nudged_by_team": {},
"progress_by_team": {}
}

View File

@@ -0,0 +1,9 @@
{
"total_injections": 0,
"pane_counts": {},
"session_counts": {},
"recent_keys": {},
"last_injection_ts": 0,
"last_reason": "disabled",
"last_event_at": "2026-04-19T20:40:19.332Z"
}

43
AGENTS.md Normal file
View File

@@ -0,0 +1,43 @@
# Repository Guidelines
## Project Structure & Module Organization
This repository is centered on the Astro app in `site/`.
- `site/src/`: pages, layouts, components, and library code (`lib/ingest`, `lib/cache`, `lib/content`).
- `site/tests/`: Vitest suites and fixtures for ingestion, tracking attributes, and UI behavior.
- `site/content/`: curated inputs and build cache (`content/cache/content.json`).
- `deploy/`: production Docker and nginx runbooks/config.
- `openspec/`: active and archived feature specs; use these as behavior references.
## Build, Test, and Development Commands
Run commands from repo root with `npm -C site run <script>`.
- `npm -C site run dev`: start local Astro dev server (`http://localhost:4321`).
- `npm -C site run fetch-content`: refresh aggregated source content before local validation.
- `npm -C site run build`: build static output.
- `npm -C site run preview`: preview production build locally.
- `npm -C site run test`: run Vitest test suite.
- `npm -C site run typecheck`: run Astro/TypeScript checks.
- `npm -C site run format:check`: verify Prettier formatting.
- `npm -C site run verify:lighthouse`: run Lighthouse quality gate assertions.
## Coding Style & Naming Conventions
- TypeScript strict mode is enabled via `astro/tsconfigs/strict`.
- Use Prettier (`site/prettier.config.cjs`) for formatting; run `npm -C site run format`.
- Use 2-space indentation in JS/TS/Astro/CSS.
- Component/layout files use PascalCase (for example `ContentCard.astro`).
- Utility modules use lowercase/kebab-case or domain folders (for example `lib/ingest/youtube.ts`).
## Testing Guidelines
- Framework: Vitest (`npm -C site run test`).
- Place tests in `site/tests/` with `*.test.ts` naming.
- Add/adjust tests for every behavior change in ingestion, tracking attributes, routing, or layout logic.
- For performance or accessibility-sensitive changes, run `verify:lighthouse` and document any deviations.
## Commit & Pull Request Guidelines
- Prefer concise, imperative commit subjects that explain intent (for example: `fix mobile view issue`).
- Include a clear body when context is non-obvious: constraints, rejected options, and verification performed.
- PRs should include: summary, linked issue/spec, test evidence (`test`, `typecheck`, `build`), and screenshots for UI changes.
## Security & Configuration Tips
- Copy `site/.env.example` to `.env`; never commit secrets.
- Keep analytics and feature flags environment-driven (`PUBLIC_UMAMI_*`, `PUBLIC_ENABLE_SW`, `PUBLIC_ENABLE_NAV_HOVER_LINE`).
- Treat `site/content/cache/content.json` as generated build input; refresh via scripts instead of manual edits.

262
DIAGNOSIS_COMPLETE.md Normal file
View File

@@ -0,0 +1,262 @@
# Production Theme Notch Styling Issue - Complete Diagnostic Report
**Date**: 2026-02-10
**Status**: ✅ ROOT CAUSE IDENTIFIED
**Severity**: CRITICAL
---
## Quick Summary
The theme notch component on production (https://santhoshj.com) appears as plain buttons in normal document flow instead of a fixed, positioned element in the top-right corner.
**Root Cause**: The production server is serving a **STALE CSS file** (17,628 bytes) that was built BEFORE the theme-notch CSS rules were added. The local build correctly generates a 24,279 byte CSS file with all rules.
**Fix**: Rebuild and redeploy the Docker image with the latest source code.
---
## Evidence
### 1. CSS File Size Mismatch
| Metric | Local Build | Production | Status |
|--------|-------------|-----------|--------|
| File size | 24,279 bytes | ~17,628 bytes | ❌ MISMATCH |
| Difference | - | -6,651 bytes (27% smaller) | ❌ CRITICAL |
| theme-notch rules | 30 matches | 0 matches | ❌ MISSING |
### 2. CSS Rules Comparison
**Local CSS (site/dist/styles/global.css):**
```css
.theme-notch {
position: fixed;
top: var(--theme-notch-top);
right: max(8px, env(safe-area-inset-right));
z-index: 12;
display: flex;
align-items: center;
gap: 10px;
pointer-events: none;
}
.theme-notch-panel {
position: absolute;
right: 60px;
top: 0;
display: grid;
gap: 6px;
padding: 12px;
border-radius: 16px;
border: 1px solid var(--stroke-mid);
background: var(--surface-1);
opacity: 0;
transform: translateX(10px) scale(0.98);
visibility: hidden;
pointer-events: none;
transition: opacity 160ms ease, transform 160ms ease, visibility 0s linear 160ms;
}
.theme-notch-handle { ... }
.theme-notch-glyph { ... }
.theme-notch-option { ... }
.theme-notch-dot { ... }
/* + media queries and forced-colors rules */
```
**Production CSS (https://santhoshj.com/styles/global.css):**
```
❌ ZERO matches for .theme-notch*
```
### 3. Computed Styles on Production
| Property | Actual | Expected | Status |
|----------|--------|----------|--------|
| .theme-notch position | static | fixed | ❌ WRONG |
| .theme-notch top | auto | 84px | ❌ WRONG |
| .theme-notch right | auto | max(8px, env(...)) | ❌ WRONG |
| .theme-notch z-index | auto | 12 | ❌ WRONG |
| .theme-notch-panel position | static | absolute | ❌ WRONG |
| .theme-notch-panel display | block | grid | ❌ WRONG |
| .theme-notch-panel opacity | 1 | 0 (initially) | ❌ WRONG |
| .theme-notch-panel visibility | visible | hidden (initially) | ❌ WRONG |
### 4. HTML Markup Status
**Present and correct on both local and production:**
- `<aside class="theme-notch" aria-label="Theme" data-theme-notch="" data-open="false">`
- `<div class="theme-notch-panel" id="theme-notch-panel" role="radiogroup" aria-label="Theme selector">`
- `<button class="theme-notch-option" data-theme-option="dark|light|contrast" role="radio" aria-checked="...">`
- SVG glyph present in `.theme-notch-glyph`
### 5. JavaScript Execution
**Inline scripts ARE executing** (no CSP block):
- Theme initialization script runs
- Service worker registration attempted
- Navigation toggle script runs
- Image lazy-load script runs
**Console errors (unrelated to theme-notch CSS):**
- `[ERROR] An unknown error occurred when fetching the script` (service worker)
- `[ERROR] Failed to load resource: net::ERR_INSUFFICIENT_RESOURCES` (analytics)
### 6. Network & Caching
-`/styles/global.css` returns **[200] OK**
- ✅ File loads successfully
- ❌ Content is stale/outdated
---
## What's Working
✅ HTML markup is present and correct
✅ JavaScript is executing without CSP errors
✅ Local development (`npm run dev`) works perfectly
✅ Local build (`npm run build`) generates correct CSS
✅ Network requests succeed
✅ Accessibility tree shows correct structure
---
## What's Broken
❌ Production CSS file is missing theme-notch rules
❌ CSS file size doesn't match local build
❌ Theme notch appears inline instead of fixed position
❌ Theme notch dropdown panel is not hidden initially
❌ Theme notch styling is completely absent
---
## Root Cause Analysis
### The Problem
The production Docker image contains an **OLDER version of the CSS file** that was built **BEFORE the theme-notch CSS rules were added** to the source.
### Why This Happened
1. The theme-notch CSS rules exist in the source (`site/public/styles/global.css`)
2. The local build correctly includes them in the output (`site/dist/styles/global.css` - 24,279 bytes)
3. **BUT** the production Docker image contains an older version of the CSS file (~17,628 bytes)
4. This suggests:
- The Docker image was built from an older commit
- OR the CSS file was not updated in the Docker image
- OR there's a stale build artifact being served
### This is a DEPLOYMENT/CACHING issue, not a build issue
---
## Verification Performed
✅ Navigated to https://santhoshj.com and /blog
✅ Captured console errors and warnings
✅ Checked network requests (global.css loads with [200])
✅ Fetched and parsed production CSS file
✅ Searched for `.theme-notch` selectors (0 matches found)
✅ Inspected computed styles via browser DevTools
✅ Verified HTML markup is present and correct
✅ Confirmed inline scripts execute without CSP errors
✅ Compared with local source CSS (rules present locally)
✅ Ran local build and verified CSS includes theme-notch rules
✅ Compared file sizes (24,279 bytes local vs ~17,628 bytes production)
---
## Recommended Fix
### Option 1: Rebuild and redeploy Docker image (RECOMMENDED)
```bash
# In the project root
docker compose build --no-cache
docker compose push
# Deploy new image to production
```
### Option 2: If using CI/CD
- Ensure the latest commit is being built
- Verify the Docker build includes the latest `site/dist/` output
- Check that the image is being pushed with the correct tag
- Redeploy the image to production
### Option 3: Quick verification before deploying
```bash
# SSH into production server
# Check CSS file size
ls -lh /usr/share/nginx/html/styles/global.css
# Should show ~24,279 bytes (not ~17,628)
# Verify theme-notch rules exist
grep -c "theme-notch" /usr/share/nginx/html/styles/global.css
# Should show 30+ matches (not 0)
```
---
## Expected Result After Fix
✅ Theme notch appears as a fixed button in top-right corner
✅ Clicking reveals a dropdown panel with Dark/Light/Contrast options
✅ Smooth animations and hover effects work
✅ CSS file size matches local build (~24,279 bytes)
✅ CSS file contains 30+ matches for "theme-notch"
✅ Computed styles show correct positioning and display properties
---
## Files Involved
| File | Purpose | Status |
|------|---------|--------|
| `site/public/styles/global.css` | Source CSS | ✅ Contains theme-notch rules (lines 306-457) |
| `site/dist/styles/global.css` | Local build output | ✅ 24,279 bytes, 30 theme-notch matches |
| `https://santhoshj.com/styles/global.css` | Production | ❌ ~17,628 bytes, 0 theme-notch matches |
| `Dockerfile` | Docker build | ⚠️ Copies from `site/dist/` |
| `site/src/components/BaseLayout.astro` | HTML template | ✅ Renders theme-notch markup |
---
## Summary Table
| Aspect | Local | Production | Status |
|--------|-------|-----------|--------|
| CSS file exists | ✅ Yes | ✅ Yes | OK |
| File size | 24,279 bytes | ~17,628 bytes | ❌ MISMATCH |
| theme-notch rules | 30 matches | 0 matches | ❌ MISSING |
| HTML markup | ✅ Present | ✅ Present | OK |
| JavaScript | ✅ Executing | ✅ Executing | OK |
| Computed styles | ✅ Correct | ❌ Wrong | BROKEN |
| Visual result | ✅ Fixed notch | ❌ Inline buttons | BROKEN |
---
## Conclusion
### The issue is NOT:
- ❌ CSP blocking inline scripts
- ❌ Missing HTML markup
- ❌ JavaScript not executing
- ❌ Browser caching
### The issue IS:
-**Production CSS file missing `.theme-notch` rules**
-**Stale Docker image serving outdated CSS**
-**Deployment/caching issue, not a build issue**
### Action Required:
**Rebuild and redeploy Docker image with latest source code.**
---
## Next Steps
1. **Verify the fix**: Rebuild Docker image with latest source
2. **Deploy**: Pu

162
DIAGNOSIS_SUMMARY.txt Normal file
View File

@@ -0,0 +1,162 @@
================================================================================
PRODUCTION THEME NOTCH STYLING ISSUE - EXECUTIVE SUMMARY
================================================================================
ISSUE: Theme notch appears as plain buttons in normal document flow on production
(https://santhoshj.com) instead of a fixed, positioned element in the
top-right corner with a dropdown panel.
ROOT CAUSE: Production server is serving a STALE CSS file (17,628 bytes) that
was built BEFORE the theme-notch CSS rules were added. The local
build correctly generates a 24,279 byte CSS file with all rules.
SEVERITY: CRITICAL - Component is completely unstyled and non-functional
================================================================================
EVIDENCE SUMMARY
================================================================================
1. CSS FILE SIZE MISMATCH
Local build: 24,279 bytes ✅ (includes theme-notch rules)
Production: ~17,628 bytes ❌ (missing theme-notch rules)
Difference: ~6,651 bytes (27% smaller)
2. THEME-NOTCH RULES
Local CSS: 30 matches for "theme-notch" ✅
Production CSS: 0 matches for "theme-notch" ❌
3. COMPUTED STYLES ON PRODUCTION
.theme-notch position: static (WRONG - should be: fixed)
.theme-notch top: auto (WRONG - should be: 84px)
.theme-notch right: auto (WRONG - should be: max(8px, env(...)))
.theme-notch z-index: auto (WRONG - should be: 12)
.theme-notch-panel display: block (WRONG - should be: grid)
.theme-notch-panel opacity: 1 (WRONG - should be: 0 initially)
4. HTML MARKUP
✅ Present and correct on both local and production
✅ All classes and attributes in place
✅ SVG glyph present
5. JAVASCRIPT EXECUTION
✅ Inline scripts ARE executing (no CSP block)
✅ Theme initialization script runs
✅ No JavaScript errors related to theme-notch
6. NETWORK & CACHING
✅ /styles/global.css returns [200] OK
✅ File loads successfully
❌ Content is stale/outdated
================================================================================
WHAT'S WORKING
================================================================================
✅ HTML markup is present and correct
✅ JavaScript is executing without CSP errors
✅ Local development (npm run dev) works perfectly
✅ Local build (npm run build) generates correct CSS
✅ Network requests succeed
✅ Accessibility tree shows correct structure
================================================================================
WHAT'S BROKEN
================================================================================
❌ Production CSS file is missing theme-notch rules
❌ CSS file size doesn't match local build
❌ Theme notch appears inline instead of fixed position
❌ Theme notch dropdown panel is not hidden initially
❌ Theme notch styling is completely absent
================================================================================
ROOT CAUSE ANALYSIS
================================================================================
The production Docker image contains an OLDER version of the CSS file that was
built BEFORE the theme-notch CSS rules were added to the source.
This is a DEPLOYMENT/CACHING issue, not a build issue.
Possible causes:
1. Docker image was built from an older commit
2. CSS file was not updated in the Docker image
3. Stale build artifact is being served
4. CI/CD pipeline didn't rebuild the image with latest source
================================================================================
VERIFICATION PERFORMED
================================================================================
✅ Navigated to https://santhoshj.com and /blog
✅ Captured console errors and warnings
✅ Checked network requests (global.css loads with [200])
✅ Fetched and parsed production CSS file
✅ Searched for .theme-notch selectors (0 matches found)
✅ Inspected computed styles via browser DevTools
✅ Verified HTML markup is present and correct
✅ Confirmed inline scripts execute without CSP errors
✅ Compared with local source CSS (rules present locally)
✅ Ran local build and verified CSS includes theme-notch rules
✅ Compared file sizes (24,279 bytes local vs ~17,628 bytes production)
================================================================================
RECOMMENDED FIX
================================================================================
OPTION 1: Rebuild and redeploy Docker image
$ docker compose build --no-cache
$ docker compose push
# Deploy new image to production
OPTION 2: If using CI/CD
- Ensure latest commit is being built
- Verify Docker build includes latest site/dist/ output
- Check image is pushed with correct tag
- Redeploy image to production
OPTION 3: Quick verification before deploying
- SSH into production server
- Check CSS file size: should be ~24,279 bytes (not ~17,628)
- Verify /styles/global.css contains "theme-notch" (30+ matches)
- If not, redeploy the Docker image
================================================================================
EXPECTED RESULT AFTER FIX
================================================================================
✅ Theme notch appears as a fixed button in top-right corner
✅ Clicking reveals a dropdown panel with Dark/Light/Contrast options
✅ Smooth animations and hover effects work
✅ CSS file size matches local build (~24,279 bytes)
✅ CSS file contains 30+ matches for "theme-notch"
✅ Computed styles show correct positioning and display properties
================================================================================
FILES INVOLVED
================================================================================
Source: site/public/styles/global.css (lines 306-457)
Production: https://santhoshj.com/styles/global.css (missing rules)
Build: Dockerfile and npm run build process
HTML: site/src/components/BaseLayout.astro (renders theme-notch markup)
Local dist: site/dist/styles/global.css (24,279 bytes - CORRECT)
================================================================================
CONCLUSION
================================================================================
The issue is NOT:
❌ CSP blocking inline scripts
❌ Missing HTML markup
❌ JavaScript not executing
❌ Browser caching
The issue IS:
✅ Production CSS file missing .theme-notch rules
✅ Stale Docker image serving outdated CSS
✅ Deployment/caching issue, not a build issue
ACTION REQUIRED: Rebuild and redeploy Docker image with latest source code.
================================================================================

View File

@@ -2,6 +2,21 @@ FROM node:24-alpine AS builder
WORKDIR /app/site
ARG PUBLIC_ENABLE_SW=true
ENV PUBLIC_ENABLE_SW=$PUBLIC_ENABLE_SW
ARG PUBLIC_ENABLE_NAV_HOVER_LINE=true
ENV PUBLIC_ENABLE_NAV_HOVER_LINE=$PUBLIC_ENABLE_NAV_HOVER_LINE
ARG PUBLIC_ASSET_VERSION
ENV PUBLIC_ASSET_VERSION=$PUBLIC_ASSET_VERSION
# Public, build-time config (baked into static HTML via Astro/Vite).
ARG PUBLIC_SITE_URL
ARG PUBLIC_UMAMI_SCRIPT_URL
ARG PUBLIC_UMAMI_WEBSITE_ID
ENV PUBLIC_SITE_URL=$PUBLIC_SITE_URL
ENV PUBLIC_UMAMI_SCRIPT_URL=$PUBLIC_UMAMI_SCRIPT_URL
ENV PUBLIC_UMAMI_WEBSITE_ID=$PUBLIC_UMAMI_WEBSITE_ID
COPY site/package.json site/package-lock.json ./
RUN npm ci --no-audit --no-fund
@@ -9,6 +24,7 @@ COPY site/ ./
# Content is fetched before build (typically in CI) and committed into the build context at
# `site/content/cache/content.json`. If env vars aren't configured, the fetch step gracefully
# skips sources and/or uses last-known-good cache.
RUN npm run build
FROM nginx:1.27-alpine

294
PRODUCTION_DIAGNOSIS.md Normal file
View File

@@ -0,0 +1,294 @@
# Production Theme Notch Styling Issue - Diagnostic Report
**Date**: 2026-02-10
**Status**: CRITICAL - CSS rules missing in production
---
## Executive Summary
The theme notch component appears as plain buttons in normal document flow on production (https://santhoshj.com) because **the CSS rules for `.theme-notch` and related selectors are completely absent from the served `/styles/global.css` file**.
---
## Evidence
### 1. CSS File Status
- **Local file**: `site/public/styles/global.css`**CONTAINS** `.theme-notch` rules (lines 306-457)
- **Production file**: `https://santhoshj.com/styles/global.css`**MISSING** `.theme-notch` rules
- **File size**: Local = 1215 lines; Production = ~17,628 bytes (109 CSS rules)
- **Network status**: [200] OK - file loads successfully
### 2. CSS Rules Missing in Production
**Local CSS (lines 306-457) includes:**
```css
.theme-notch {
position: fixed;
top: var(--theme-notch-top);
right: max(8px, env(safe-area-inset-right));
z-index: 12;
display: flex;
align-items: center;
gap: 10px;
pointer-events: none;
}
.theme-notch-panel {
position: absolute;
right: 60px;
top: 0;
display: grid;
gap: 6px;
padding: 12px;
border-radius: 16px;
border: 1px solid var(--stroke-mid);
background: var(--surface-1);
opacity: 0;
transform: translateX(10px) scale(0.98);
visibility: hidden;
pointer-events: none;
transition: opacity 160ms ease, transform 160ms ease, visibility 0s linear 160ms;
}
.theme-notch-handle { ... }
.theme-notch-glyph { ... }
.theme-notch-option { ... }
.theme-notch-dot { ... }
/* + media queries and forced-colors rules */
```
**Production CSS**: ❌ **ZERO** matches for `.theme-notch*` selectors
### 3. Computed Styles on Production
**Actual computed styles (from browser DevTools):**
```
.theme-notch:
position: static (WRONG - should be: fixed)
top: auto (WRONG - should be: 84px)
right: auto (WRONG - should be: max(8px, env(safe-area-inset-right)))
display: block (CORRECT - but only by accident)
opacity: 1 (CORRECT - but only by accident)
visibility: visible (CORRECT - but only by accident)
z-index: auto (WRONG - should be: 12)
.theme-notch-panel:
position: static (WRONG - should be: absolute)
top: auto (WRONG - should be: 0)
right: auto (WRONG - should be: 60px)
display: block (WRONG - should be: grid)
opacity: 1 (WRONG - should be: 0 initially)
visibility: visible (WRONG - should be: hidden initially)
```
### 4. HTML Markup Status
**Present and correct:**
- `<aside class="theme-notch" aria-label="Theme" data-theme-notch="" data-open="false">`
- `<div class="theme-notch-panel" id="theme-notch-panel" role="radiogroup" aria-label="Theme selector">`
- `<button class="theme-notch-option" data-theme-option="dark|light|contrast" role="radio" aria-checked="...">`
- SVG glyph present in `.theme-notch-glyph`
### 5. JavaScript Execution
**Inline scripts ARE executing** (no CSP block):
- Theme initialization script runs
- Service worker registration attempted (fails due to unrelated script fetch error)
- Navigation toggle script runs
- Image lazy-load script runs
**Console errors (unrelated to theme-notch CSS):**
- `[ERROR] An unknown error occurred when fetching the script` (service worker)
- `[ERROR] Failed to load resource: net::ERR_INSUFFICIENT_RESOURCES` (analytics)
### 6. Network & Caching
- `/styles/global.css` returns **[200] OK**
- No cache headers preventing updates detected
- File loads successfully but content is stale/incomplete
---
## Root Cause Analysis
### Hypothesis: Build/Deployment Issue
The production CSS file is **missing the theme-notch rules entirely**, suggesting:
1. **Build process did not include the CSS** - The CSS file was generated/minified without the theme-notch section
2. **Stale build artifact** - An older version of global.css is being served
3. **CSS file truncation** - The build process may have failed to include lines 306-457 from the source
4. **Incorrect build output** - The CSS bundler/minifier may have stripped unused rules (unlikely, as the HTML uses the classes)
### Why It Works Locally
- `npm run dev` serves the source CSS directly from `site/public/styles/global.css`
- All rules are present and applied correctly
- Theme notch appears as a fixed, positioned element in the top-right corner
### Why It Fails in Production
- Docker build likely runs `npm run build` which may:
- Minify/bundle CSS differently
- Use a different CSS source file
- Strip rules based on unused CSS detection
- Output to a different location than `site/public/styles/global.css`
---
## Visual Impact
**Expected (Local):**
- Theme notch appears as a fixed button in top-right corner
- Clicking reveals a dropdown panel with Dark/Light/Contrast options
- Smooth animations and hover effects
**Actual (Production):**
- Theme notch buttons appear inline in normal document flow
- No positioning, no dropdown panel
- Buttons are unstyled (only inherit default browser styles)
- Appears as plain radio buttons in the accessibility tree
---
## Verification Steps Performed
✅ Navigated to https://santhoshj.com and /blog
✅ Captured console errors and warnings
✅ Checked network requests (global.css loads with [200])
✅ Fetched and parsed production CSS file
✅ Searched for `.theme-notch` selectors (0 matches)
✅ Inspected computed styles via browser DevTools
✅ Verified HTML markup is present and correct
✅ Confirmed inline scripts execute without CSP errors
✅ Compared with local source CSS (rules present locally)
---
## Recommended Actions
1. **Immediate**: Check the Docker build process
- Verify `npm run build` output includes theme-notch CSS
- Check if CSS is being minified/bundled to a different file
- Ensure `site/public/styles/global.css` is copied to the Docker image
2. **Verify**: Rebuild and redeploy
- Run `npm run build` locally and inspect the output CSS
- Check if `dist/styles/global.css` (or equivalent) contains theme-notch rules
- Verify the Docker image includes the correct CSS file
3. **Debug**: Add build logging
- Log CSS file size before/after minification
- Verify the CSS source file is being read correctly
- Check for any build warnings about unused CSS
4. **Test**: Validate production deployment
- After fix, verify `/styles/global.css` contains theme-notch rules
- Check computed styles in production browser DevTools
- Confirm theme notch appears in correct position
---
## Files Involved
- **Source**: `site/public/styles/global.css` (lines 306-457)
- **Production**: `https://santhoshj.com/styles/global.css` (missing rules)
- **Build**: `Dockerfile` and `npm run build` process
- **HTML**: `site/src/components/BaseLayout.astro` (renders theme-notch markup)
---
## Conclusion
**The issue is NOT:**
- ❌ CSP blocking inline scripts
- ❌ Missing HTML markup
- ❌ JavaScript not executing
- ❌ Browser caching (file loads fresh)
**The issue IS:**
-**Production CSS file missing `.theme-notch` rules**
-**Build/deployment process not including complete CSS**
The fix requires investigating the Docker build and CSS bundling process to ensure all CSS rules are included in the production output.
---
## CRITICAL UPDATE: Root Cause Identified
### Build Verification Results
**Local Build (npm run build):**
- ✅ CSS file generated: `site/dist/styles/global.css`
- ✅ File size: **24,279 bytes**
- ✅ Theme-notch rules: **30 matches** found
- ✅ All CSS rules present and correct
**Production Server:**
- ❌ CSS file served: `https://santhoshj.com/styles/global.css`
- ❌ File size: **~17,628 bytes** (SMALLER than local build)
- ❌ Theme-notch rules: **0 matches** found
- ❌ CSS is STALE/OUTDATED
### Conclusion
**The production server is serving an OLD version of the CSS file that was built BEFORE the theme-notch CSS rules were added to the source.**
This is a **deployment/caching issue**, not a build issue.
### Why This Happened
1. The theme-notch CSS rules exist in the source (`site/public/styles/global.css`)
2. The local build correctly includes them in the output (`site/dist/styles/global.css`)
3. **BUT** the production Docker image contains an older version of the CSS file
4. This suggests:
- The Docker image was built from an older commit
- OR the CSS file was not updated in the Docker image
- OR there's a caching layer serving stale content
### Fix Required
**Option 1: Rebuild and redeploy the Docker image**
```bash
docker compose build --no-cache
docker compose push
# Deploy new image to production
```
**Option 2: If using CI/CD**
- Ensure the latest commit is being built
- Verify the Docker build includes the latest `site/dist/` output
- Check that the image is being pushed with the correct tag
- Redeploy the image to production
**Option 3: Quick verification**
- SSH into production server
- Check the CSS file size: should be ~24,279 bytes (not ~17,628)
- Verify `/styles/global.css` contains "theme-notch" (30+ matches)
- If not, redeploy the Docker image
---
## Summary Table
| Aspect | Local | Production | Status |
|--------|-------|-----------|--------|
| CSS file exists | ✅ Yes | ✅ Yes | OK |
| File size | 24,279 bytes | ~17,628 bytes | ❌ MISMATCH |
| theme-notch rules | 30 matches | 0 matches | ❌ MISSING |
| HTML markup | ✅ Present | ✅ Present | OK |
| JavaScript | ✅ Executing | ✅ Executing | OK |
| Computed styles | ✅ Correct | ❌ Wrong | BROKEN |
| Visual result | ✅ Fixed notch | ❌ Inline buttons | BROKEN |
---
## Next Steps
1. **Verify the fix**: Rebuild Docker image with latest source
2. **Deploy**: Push new image to production
3. **Test**: Verify `/styles/global.css` is ~24,279 bytes and contains theme-notch rules
4. **Validate**: Check production site - theme notch should appear in top-right corner
5. **Monitor**: Watch for any other stale assets

View File

@@ -2,6 +2,84 @@
Lightweight, SEO-first website for SanthoshJ that aggregates YouTube + Instagram + podcast content and tracks conversion events via Umami.
## Specs (OpenSpec)
This repo uses OpenSpec (schema: `spec-driven`) to document and ship features.
- Active specs live in `openspec/specs/<spec-name>/spec.md`
- Completed initiatives (proposal/design/tasks + delta specs) live in `openspec/changes/archive/<date>-<change-name>/`
Key public flags (documented in `site/.env.example`):
- `PUBLIC_SITE_URL`: canonical URL base
- `PUBLIC_ENABLE_SW`: set to `"false"` to disable service worker registration
- `PUBLIC_ENABLE_NAV_HOVER_LINE`: set to `"false"` to disable decorative hover-line styling
### Spec Index (Active)
Content + ingestion:
- `social-content-aggregation` (`openspec/specs/social-content-aggregation/spec.md`): normalize YouTube/Instagram/podcast items and refresh via cached ingestion
- `homepage-content-modules` (`openspec/specs/homepage-content-modules/spec.md`): homepage module ordering, newest feed, high-performing videos, and Instagram omission when empty
- `wordpress-content-source` (`openspec/specs/wordpress-content-source/spec.md`): fetch WordPress posts/pages/categories via `wp-json` and write to build cache
- `blog-section-surface` (`openspec/specs/blog-section-surface/spec.md`): `/blog` surface (index/category/detail) + Umami-instrumented navigation
- `seo-content-surface` (`openspec/specs/seo-content-surface/spec.md`): indexable pages, canonical URLs, sitemap + robots, and JSON-LD expectations
Analytics + tracking:
- `interaction-tracking-taxonomy` (`openspec/specs/interaction-tracking-taxonomy/spec.md`): required Umami event attributes, `target_id` namespaces, and modal interaction events
- `analytics-umami` (`openspec/specs/analytics-umami/spec.md`): Umami enable/disable behavior + supported custom events
- `conversion-ctas` (`openspec/specs/conversion-ctas/spec.md`): reusable CTA surface + UTM support + `cta_click` tracking
UI + UX shell:
- `wcag-responsive-ui` (`openspec/specs/wcag-responsive-ui/spec.md`): responsive nav shell, focus-visible baseline, reduced motion, and semantic element rules
- `card-layout-system` (`openspec/specs/card-layout-system/spec.md`): standardized card information architecture + modal-trigger behavior for video/podcast cards
- `media-modal` (`openspec/specs/media-modal/spec.md`): `<dialog>`-based media preview modal requirements (focus, playback stop, crawlable CTAs)
- `image-lazy-loading` (`openspec/specs/image-lazy-loading/spec.md`): shimmer placeholders + fade-in + failure handling + reduced-motion behavior
- `site-theming` (`openspec/specs/site-theming/spec.md`): `data-theme` application, defaults, and persistence rules (localStorage + cookie fallback)
- `theme-switcher-notch` (`openspec/specs/theme-switcher-notch/spec.md`): floating theme notch placement, interaction, and accessibility requirements
- `navbar-branding` (`openspec/specs/navbar-branding/spec.md`): header logo + centered brand layout
- `nav-hover-line` (`openspec/specs/nav-hover-line/spec.md`): decorative hover-line treatment for header titles + key surface titles (flagged)
Performance + deployment:
- `service-worker-performance` (`openspec/specs/service-worker-performance/spec.md`): production SW registration + runtime caching + safe updates
- `responsive-image-delivery` (`openspec/specs/responsive-image-delivery/spec.md`): explicit dimensions + deterministic image behavior for quality gates
- `lighthouse-quality-gate` (`openspec/specs/lighthouse-quality-gate/spec.md`): deterministic Lighthouse runner + 100-score assertion
- `docker-content-refresh` (`openspec/specs/docker-content-refresh/spec.md`): Docker-only host refresh/update workflow (no Node.js on server)
- `cache-layer` (`openspec/specs/cache-layer/spec.md`): Redis-backed shared cache + TTL + manual cache clear
### Quality Gates
- Site build: `npm -C site run build`
- Lighthouse gate docs: `site/docs/lighthouse.md` (scripts: `npm -C site run lighthouse:run`, `npm -C site run verify:lighthouse`)
### Completed Initiatives (Archived)
Each archived initiative includes `proposal.md`, `design.md`, `tasks.md`, and any delta specs used for that change.
| Change | Focus |
|---|---|
| `2026-02-10-dynamic-homepage-social-acquisition` | Initial SEO-first site: content aggregation, homepage modules, CTAs, and analytics |
| `2026-02-10-better-tracking` | Site-wide click tracking taxonomy aligned to Umami |
| `2026-02-10-custom-events-umami` | Expand Umami custom event coverage + standardize event properties |
| `2026-02-10-blog-umami-fix` | Fix and verify blog surface Umami instrumentation |
| `2026-02-10-better-cache` | Add Redis cache layer + TTL + manual clear; wire into ingestion |
| `2026-02-10-card-layout` | Standardize card layout across surfaces |
| `2026-02-10-lazy-loading` | Add shimmer placeholders and reduced-motion-safe image loading UX |
| `2026-02-10-blogs-section` | Add WordPress-backed blog section + routes + secondary nav |
| `2026-02-10-hide-ig-if-no-data` | Omit Instagram module when dataset is empty |
| `2026-02-10-reduce-bounce-rate` | Add in-page media modal previews for video/podcast cards |
| `2026-02-10-service-workers` | Add service worker caching for repeat-visit performance |
| `2026-02-10-deploy-without-node` | Docker-only host refresh/update workflow |
| `2026-02-10-fix-sub-pages` | Fix static serving so `/videos`, `/podcast`, `/about` do not 404 |
| `2026-02-10-wcag-responsive` | WCAG baseline + responsive nav shell + typography/background fixes |
| `2026-02-10-lighthouse-fixes` | Lighthouse cleanup + deterministic 100/100/100/100 gate |
| `2026-02-11-dch-theming` | Dark/light/high-contrast themes + theme notch UI |
| `2026-02-11-remember-theme` | Persist theme across visits + emit theme switch tracking |
| `2026-02-11-final-touches` | Header branding polish + hover-line treatment behind env flag |
## Local Setup
```bash

Binary file not shown.

BIN
blog-screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 KiB

BIN
csp-validation-final.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 826 KiB

View File

@@ -2,13 +2,48 @@ server {
listen 80;
server_name _;
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 1024;
gzip_types text/plain text/css text/javascript application/javascript application/json application/xml application/rss+xml image/svg+xml;
root /usr/share/nginx/html;
index index.html;
# Custom error pages
error_page 404 /404.html;
error_page 500 502 503 504 /500.html;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.instagram.com https://*.instagram.com https://cloud.umami.is https://*.umami.is https://wa.santhoshj.com; style-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' https: data: blob:; media-src 'self' https://anchor.fm https://*.anchor.fm https://d3ctxlq1ktw2nl.cloudfront.net; connect-src 'self' https://cloud.umami.is https://*.umami.is https://wa.santhoshj.com; frame-src https://www.youtube.com https://open.spotify.com https://www.instagram.com https://*.instagram.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
# Static assets
location = /sw.js {
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
try_files $uri =404;
}
location ~* \.(?:avif|bmp|gif|ico|jpe?g|png|svg|webp|)$ {
expires 180d;
add_header Cache-Control "public, max-age=15552000" always;
try_files $uri =404;
}
# Ensure error pages are served without redirect
location = /404.html {
internal;
}
location = /500.html {
internal;
}
location / {
# Serve directory index pages without requiring a trailing slash.
# This fixes /videos (and similar) resolving to /videos/index.html.
try_files $uri $uri/index.html $uri/ =404;
}
}
}

View File

@@ -6,6 +6,9 @@ The deployment model is:
- CI builds and publishes a Docker image containing the built static site
- the server updates by pulling that image and restarting the service
Build-time config (examples):
- `PUBLIC_ENABLE_SW=true|false` toggles service worker registration in the generated HTML (defaults to `true` in the Docker build).
### Prerequisites
- Docker + Docker Compose plugin available on the host
@@ -81,4 +84,3 @@ The pull should fail, but the current service should still be running:
docker compose -f deploy/docker-compose.prod.yml ps
curl -fsS http://localhost:8080/ > /dev/null
```

View File

@@ -4,8 +4,20 @@ services:
build:
context: .
dockerfile: Dockerfile
args:
# Build-time toggle for service worker registration in the generated static HTML.
PUBLIC_ENABLE_SW: ${PUBLIC_ENABLE_SW:-true}
PUBLIC_ENABLE_NAV_HOVER_LINE: ${PUBLIC_ENABLE_NAV_HOVER_LINE:-true}
PUBLIC_ASSET_VERSION: ${PUBLIC_ASSET_VERSION:-}
# Public, build-time config baked into the HTML.
PUBLIC_SITE_URL: ${PUBLIC_SITE_URL:-}
PUBLIC_UMAMI_SCRIPT_URL: ${PUBLIC_UMAMI_SCRIPT_URL:-}
PUBLIC_UMAMI_WEBSITE_ID: ${PUBLIC_UMAMI_WEBSITE_ID:-}
ports:
- "8080:80"
networks:
- fast-website-network
redis:
image: redis:7-alpine
@@ -17,3 +29,9 @@ services:
interval: 5s
timeout: 3s
retries: 20
networks:
- fast-website-network
networks:
fast-website-network:
driver: bridge

View File

@@ -0,0 +1,425 @@
# Graph Report - . (2026-04-19)
## Corpus Check
- 177 files · ~197,362 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 235 nodes · 221 edges · 57 communities detected
- Extraction: 89% EXTRACTED · 11% INFERRED · 0% AMBIGUOUS · INFERRED: 25 edges (avg confidence: 0.76)
- Token cost: 0 input · 0 output
## Community Hubs (Navigation)
- [[_COMMUNITY_Layout Persistence Branding|Layout Persistence Branding]]
- [[_COMMUNITY_Concept Modal Youtube|Concept Modal Youtube]]
- [[_COMMUNITY_Main Getingestconfigfromenv Getpublicconfig|Main Getingestconfigfromenv Getpublicconfig]]
- [[_COMMUNITY_Wordpress Decodeentities Fetchallpages|Wordpress Decodeentities Fetchallpages]]
- [[_COMMUNITY_Cache Main Createcachefromenv|Cache Main Createcachefromenv]]
- [[_COMMUNITY_Highperformingyoutubevideos Instagramposts Newestitems|Highperformingyoutubevideos Instagramposts Newestitems]]
- [[_COMMUNITY_Compute Sleep Cachedcompute|Compute Sleep Cachedcompute]]
- [[_COMMUNITY_Cache Slug Build|Cache Slug Build]]
- [[_COMMUNITY_Image Placeholder Load|Image Placeholder Load]]
- [[_COMMUNITY_Website Repository Guidelines|Website Repository Guidelines]]
- [[_COMMUNITY_Youtube Fetchyoutubeviaapi Fetchyoutubeviarss|Youtube Fetchyoutubeviaapi Fetchyoutubeviarss]]
- [[_COMMUNITY_Cacheputsafe Isget Isimagerequest|Cacheputsafe Isget Isimagerequest]]
- [[_COMMUNITY_Production Notch Styling|Production Notch Styling]]
- [[_COMMUNITY_Astro Blogpostcard Layout|Astro Blogpostcard Layout]]
- [[_COMMUNITY_Getcachepath Readcontentcache Verify|Getcachepath Readcontentcache Verify]]
- [[_COMMUNITY_Fetchpodcastrss Normalizepodcastfeeditems Striphtml|Fetchpodcastrss Normalizepodcastfeeditems Striphtml]]
- [[_COMMUNITY_Target Data Umami|Target Data Umami]]
- [[_COMMUNITY_Screenshot Review Santhoshj|Screenshot Review Santhoshj]]
- [[_COMMUNITY_Tests Listed Files|Tests Listed Files]]
- [[_COMMUNITY_Service Worker Critical|Service Worker Critical]]
- [[_COMMUNITY_Withutm|Withutm]]
- [[_COMMUNITY_Creatememorycache Memory Cache|Creatememorycache Memory Cache]]
- [[_COMMUNITY_Readfeaturedvideoids Curation|Readfeaturedvideoids Curation]]
- [[_COMMUNITY_Read Umami Attributes|Read Umami Attributes]]
- [[_COMMUNITY_Read Layout Test|Read Layout Test]]
- [[_COMMUNITY_Read Title Type|Read Title Type]]
- [[_COMMUNITY_Umami Attributes Test|Umami Attributes Test]]
- [[_COMMUNITY_Wcag Responsive Shell|Wcag Responsive Shell]]
- [[_COMMUNITY_Normalized Schema Wordpress|Normalized Schema Wordpress]]
- [[_COMMUNITY_Passwords Rationale Revocable|Passwords Rationale Revocable]]
- [[_COMMUNITY_Astro Config|Astro Config]]
- [[_COMMUNITY_Community 31|Community 31]]
- [[_COMMUNITY_Links|Links]]
- [[_COMMUNITY_Types|Types]]
- [[_COMMUNITY_Types|Types]]
- [[_COMMUNITY_Test|Test]]
- [[_COMMUNITY_Ingest Test|Ingest Test]]
- [[_COMMUNITY_Desktop Header Screenshot|Desktop Header Screenshot]]
- [[_COMMUNITY_Mobile Header Screenshot|Mobile Header Screenshot]]
- [[_COMMUNITY_Surface|Surface]]
- [[_COMMUNITY_Rationale Structure Keeps|Rationale Structure Keeps]]
- [[_COMMUNITY_Contentitem Summary Optional|Contentitem Summary Optional]]
- [[_COMMUNITY_Standardcard Astro|Standardcard Astro]]
- [[_COMMUNITY_Post Slug Astro|Post Slug Astro]]
- [[_COMMUNITY_Slug Astro|Slug Astro]]
- [[_COMMUNITY_Point Quality Gate|Point Quality Gate]]
- [[_COMMUNITY_Bounce Rate Reduction|Bounce Rate Reduction]]
- [[_COMMUNITY_Preview Modal Instead|Preview Modal Instead]]
- [[_COMMUNITY_Umami Preview Event|Umami Preview Event]]
- [[_COMMUNITY_Modal Tracking Click|Modal Tracking Click]]
- [[_COMMUNITY_Precedence Localstorage Cookie|Precedence Localstorage Cookie]]
- [[_COMMUNITY_Cookie Fallback Long|Cookie Fallback Long]]
- [[_COMMUNITY_Switch Event Automatic|Switch Event Automatic]]
- [[_COMMUNITY_Surface|Surface]]
- [[_COMMUNITY_Social Aggregation|Social Aggregation]]
- [[_COMMUNITY_Homepage Modules|Homepage Modules]]
- [[_COMMUNITY_Conversion Ctas|Conversion Ctas]]
## God Nodes (most connected - your core abstractions)
1. `2026-02-10 lighthouse fixes change` - 13 edges
2. `main()` - 10 edges
3. `createCacheFromEnv()` - 8 edges
4. `media-modal capability` - 7 edges
5. `run()` - 6 edges
6. `normalizeWordpressPost()` - 6 edges
7. `fast-website` - 6 edges
8. `Three-theme system (dark/light/high-contrast)` - 6 edges
9. `log()` - 5 edges
10. `normalizeWordpressPage()` - 5 edges
## Surprising Connections (you probably didn't know these)
- `Favicon SVG stylized S mark` --supports_branding--> `SEO-first content aggregation website` [INFERRED]
site/public/favicon.svg → README.md
- `run()` --calls--> `log()` [INFERRED]
site\scripts\run-lighthouse.ts → site\scripts\fetch-content.ts
- `createCacheFromEnv()` --calls--> `log()` [INFERRED]
site\src\lib\cache\index.ts → site\scripts\fetch-content.ts
- `main()` --calls--> `getIngestConfigFromEnv()` [INFERRED]
site\scripts\fetch-content.ts → site\src\lib\config.ts
- `main()` --calls--> `createCacheFromEnv()` [INFERRED]
site\scripts\fetch-content.ts → site\src\lib\cache\index.ts
## Hyperedges (group relationships)
- **Theme notch incident bundle** — doc_diag_complete, doc_diag_summary, doc_prod_diagnosis, issue_theme_notch_production_unstyled, cause_stale_production_css, fix_rebuild_redeploy_docker [INFERRED 0.90]
- **Lighthouse gate contract** — capability_lighthouse_quality_gate, concept_lighthouse_gate_100, concept_theme_system, concept_service_worker_caching, concept_layout_shift_prevention [EXTRACTED 0.90]
- **Media modal implementation bundle** — capability_media_modal, concept_native_dialog_modal, concept_iframe_src_reset, concept_cards_as_buttons, concept_data_attributes_modal, concept_modal_embed_urls, concept_wcag_modal_accessibility, concept_umami_media_preview, concept_modal_cta_tracking [EXTRACTED 0.90]
- **Theme persistence bundle** — change_remember_theme, concept_theme_persistence, concept_theme_precedence_order, concept_site_theme_cookie, concept_theme_switch_event, concept_theme_switch_payload, concept_no_restore_event [EXTRACTED 0.90]
## Communities
### Community 0 - "Layout Persistence Branding"
Cohesion: 0.08
Nodes (25): lighthouse-quality-gate capability, Accessible semantic controls, deterministic lighthouse gate, Layout shift prevention, prefers-reduced-motion handling, theme persistence across visits, Theme persistence with localStorage/cookie fallback, theme_switch umami event (+17 more)
### Community 1 - "Concept Modal Youtube"
Cohesion: 0.12
Nodes (18): media-modal capability, 2026-02-10 lighthouse fixes change, youtube iframe api endpoint, video/podcast cards as button triggers, concept crawlable anchors, concept csp header alignment, concept dark theme contrast tokens, data-* attribute flow from cards to modal (+10 more)
### Community 2 - "Main Getingestconfigfromenv Getpublicconfig"
Cohesion: 0.18
Nodes (11): getIngestConfigFromEnv(), dedupe(), log(), main(), normalizeSpotifyEpisodeUrl(), readPodcastSpotifyOverrideMap(), writeAtomic(), readInstagramEmbedPosts() (+3 more)
### Community 3 - "Wordpress Decodeentities Fetchallpages"
Cohesion: 0.27
Nodes (10): decodeEntities(), fetchWordpressContent(), getAuthHeaders(), mapCategoryIds(), mapFeaturedImageUrl(), normalizeWordpressCategory(), normalizeWordpressPage(), normalizeWordpressPost() (+2 more)
### Community 4 - "Cache Main Createcachefromenv"
Cohesion: 0.21
Nodes (7): log(), main(), createCacheFromEnv(), createNoopCache(), createRedisCache(), resolveDefaultTtlSecondsFromEnv(), resolveRedisUrlFromEnv()
### Community 5 - "Highperformingyoutubevideos Instagramposts Newestitems"
Cohesion: 0.2
Nodes (4): highPerformingYoutubeVideos(), wordpressPosts(), wordpressPostsByCategorySlug(), youtubeVideos()
### Community 6 - "Compute Sleep Cachedcompute"
Cohesion: 0.25
Nodes (7): compute(), sleep(), cachedCompute(), getArg(), hasFlag(), run(), startPreviewServerIfNeeded()
### Community 7 - "Cache Slug Build"
Cohesion: 0.18
Nodes (11): blog-section-surface, build-time ingestion into content cache, site/content/cache/content.json, site/scripts/fetch-content.ts, Rationale: build-time cache keeps site fast and crawlable, /blog/category/<slug> route, /blog route, /blog/page/<slug> route (+3 more)
### Community 8 - "Image Placeholder Load"
Cohesion: 0.2
Nodes (10): BaseLayout inline image load script, static placeholder on image error, fade-in on image load, image-lazy-loading, .img-error, .img-loading, .img-shimmer-wrap, no CLS from placeholder (+2 more)
### Community 9 - "Website Repository Guidelines"
Cohesion: 0.22
Nodes (9): Repository Guidelines, SVG uses prefers-color-scheme dark fill override, Favicon SVG stylized S mark, lighthouse-quality-gate, nav-hover-line, theme-switcher-notch, OpenSpec schema spec-driven, fast-website (+1 more)
### Community 10 - "Youtube Fetchyoutubeviaapi Fetchyoutubeviarss"
Cohesion: 0.36
Nodes (6): fetchYoutubeViaApi(), fetchYoutubeViaRss(), normalizeYoutubeApiVideos(), normalizeYoutubeRssFeedItems(), stripHtml(), truncate()
### Community 11 - "Cacheputsafe Isget Isimagerequest"
Cohesion: 0.33
Nodes (0):
### Community 12 - "Production Notch Styling"
Cohesion: 0.33
Nodes (6): Stale production CSS missing theme-notch rules, Production Theme Notch Styling Issue - Complete Diagnostic Report, Production Theme Notch Styling Issue - Executive Summary, Production Theme Notch Styling Issue - Diagnostic Report, Rebuild and redeploy Docker image, Theme notch unstyled on production
### Community 13 - "Astro Blogpostcard Layout"
Cohesion: 0.33
Nodes (4): card-layout-system, views shown only when available, shared Card component, standard footer/meta row
### Community 14 - "Getcachepath Readcontentcache Verify"
Cohesion: 0.5
Nodes (3): getCachePath(), readContentCache(), main()
### Community 15 - "Fetchpodcastrss Normalizepodcastfeeditems Striphtml"
Cohesion: 0.6
Nodes (4): fetchPodcastRss(), normalizePodcastFeedItems(), stripHtml(), truncate()
### Community 16 - "Target Data Umami"
Cohesion: 0.5
Nodes (4): data-umami-event attributes, placement, target_id, target_url
### Community 17 - "Screenshot Review Santhoshj"
Cohesion: 0.67
Nodes (3): Performance Review — santhoshj.com, Blog page screenshot, Homepage screenshot
### Community 18 - "Tests Listed Files"
Cohesion: 0.67
Nodes (3): Listed spec files missing in current workspace, site/tests/content.spec.ts, site/tests/yaml.spec.ts
### Community 19 - "Service Worker Critical"
Cohesion: 0.67
Nodes (3): Critical-asset cache busting, Service worker caching lifecycle, Service worker performance spec
### Community 20 - "Withutm"
Cohesion: 1.0
Nodes (0):
### Community 21 - "Creatememorycache Memory Cache"
Cohesion: 1.0
Nodes (0):
### Community 22 - "Readfeaturedvideoids Curation"
Cohesion: 1.0
Nodes (0):
### Community 23 - "Read Umami Attributes"
Cohesion: 1.0
Nodes (0):
### Community 24 - "Read Layout Test"
Cohesion: 1.0
Nodes (0):
### Community 25 - "Read Title Type"
Cohesion: 1.0
Nodes (0):
### Community 26 - "Umami Attributes Test"
Cohesion: 1.0
Nodes (0):
### Community 27 - "Wcag Responsive Shell"
Cohesion: 1.0
Nodes (0):
### Community 28 - "Normalized Schema Wordpress"
Cohesion: 1.0
Nodes (2): normalized wordpress internal schema, Rationale: normalized schema keeps UI simple and consistent
### Community 29 - "Passwords Rationale Revocable"
Cohesion: 1.0
Nodes (2): Rationale: app passwords are revocable and safer operationally, WordPress application passwords
### Community 30 - "Astro Config"
Cohesion: 1.0
Nodes (0):
### Community 31 - "Community 31"
Cohesion: 1.0
Nodes (0):
### Community 32 - "Links"
Cohesion: 1.0
Nodes (0):
### Community 33 - "Types"
Cohesion: 1.0
Nodes (0):
### Community 34 - "Types"
Cohesion: 1.0
Nodes (0):
### Community 35 - "Test"
Cohesion: 1.0
Nodes (0):
### Community 36 - "Ingest Test"
Cohesion: 1.0
Nodes (0):
### Community 37 - "Desktop Header Screenshot"
Cohesion: 1.0
Nodes (1): Desktop header screenshot
### Community 38 - "Mobile Header Screenshot"
Cohesion: 1.0
Nodes (1): Mobile header screenshot
### Community 39 - "Surface"
Cohesion: 1.0
Nodes (1): seo-content-surface
### Community 40 - "Rationale Structure Keeps"
Cohesion: 1.0
Nodes (1): Rationale: route structure keeps URLs clear and stable
### Community 41 - "Contentitem Summary Optional"
Cohesion: 1.0
Nodes (1): ContentItem.summary optional field
### Community 42 - "Standardcard Astro"
Cohesion: 1.0
Nodes (0):
### Community 43 - "Post Slug Astro"
Cohesion: 1.0
Nodes (1): blog/post/[slug].astro
### Community 44 - "Slug Astro"
Cohesion: 1.0
Nodes (1): blog/page/[slug].astro
### Community 45 - "Point Quality Gate"
Cohesion: 1.0
Nodes (1): Lighthouse 100-point quality gate
### Community 46 - "Bounce Rate Reduction"
Cohesion: 1.0
Nodes (1): Bounce rate reduction intent
### Community 47 - "Preview Modal Instead"
Cohesion: 1.0
Nodes (1): on-site media preview modal instead of outbound navigation
### Community 48 - "Umami Preview Event"
Cohesion: 1.0
Nodes (1): umami media_preview event taxonomy
### Community 49 - "Modal Tracking Click"
Cohesion: 1.0
Nodes (1): modal CTA tracking via cta_click taxonomy
### Community 50 - "Precedence Localstorage Cookie"
Cohesion: 1.0
Nodes (1): theme precedence localStorage -> cookie -> environment signals
### Community 51 - "Cookie Fallback Long"
Cohesion: 1.0
Nodes (1): site_theme cookie fallback with long TTL
### Community 52 - "Switch Event Automatic"
Cohesion: 1.0
Nodes (1): no theme_switch event on automatic restoration
### Community 53 - "Surface"
Cohesion: 1.0
Nodes (1): SEO content surface spec
### Community 54 - "Social Aggregation"
Cohesion: 1.0
Nodes (1): Social content aggregation spec
### Community 55 - "Homepage Modules"
Cohesion: 1.0
Nodes (1): Homepage content modules spec
### Community 56 - "Conversion Ctas"
Cohesion: 1.0
Nodes (1): Conversion CTAs spec
## Knowledge Gaps
- **78 isolated node(s):** `Repository Guidelines`, `OpenSpec schema spec-driven`, `theme-switcher-notch`, `nav-hover-line`, `lighthouse-quality-gate` (+73 more)
These have ≤1 connection - possible missing edges or undocumented components.
- **Thin community `Withutm`** (2 nodes): `url.ts`, `withUtm()`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Creatememorycache Memory Cache`** (2 nodes): `createMemoryCache()`, `memory-cache.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Readfeaturedvideoids Curation`** (2 nodes): `readFeaturedVideoIds()`, `curation.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Read Umami Attributes`** (2 nodes): `read()`, `blog-umami-attributes.test.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Read Layout Test`** (2 nodes): `read()`, `card-layout.test.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Read Title Type`** (2 nodes): `read()`, `content-title-type-attributes.test.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Umami Attributes Test`** (2 nodes): `umami-attributes.test.ts`, `read()`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Wcag Responsive Shell`** (2 nodes): `wcag-responsive-shell.test.ts`, `read()`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Normalized Schema Wordpress`** (2 nodes): `normalized wordpress internal schema`, `Rationale: normalized schema keeps UI simple and consistent`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Passwords Rationale Revocable`** (2 nodes): `Rationale: app passwords are revocable and safer operationally`, `WordPress application passwords`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Astro Config`** (1 nodes): `astro.config.mjs`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 31`** (1 nodes): `env.d.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Links`** (1 nodes): `links.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Types`** (1 nodes): `types.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Types`** (1 nodes): `types.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Test`** (1 nodes): `blog-nav.test.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Ingest Test`** (1 nodes): `ingest.test.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Desktop Header Screenshot`** (1 nodes): `Desktop header screenshot`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Mobile Header Screenshot`** (1 nodes): `Mobile header screenshot`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Surface`** (1 nodes): `seo-content-surface`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Rationale Structure Keeps`** (1 nodes): `Rationale: route structure keeps URLs clear and stable`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Contentitem Summary Optional`** (1 nodes): `ContentItem.summary optional field`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Standardcard Astro`** (1 nodes): `StandardCard.astro`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Post Slug Astro`** (1 nodes): `blog/post/[slug].astro`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Slug Astro`** (1 nodes): `blog/page/[slug].astro`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Point Quality Gate`** (1 nodes): `Lighthouse 100-point quality gate`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Bounce Rate Reduction`** (1 nodes): `Bounce rate reduction intent`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Preview Modal Instead`** (1 nodes): `on-site media preview modal instead of outbound navigation`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Umami Preview Event`** (1 nodes): `umami media_preview event taxonomy`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Modal Tracking Click`** (1 nodes): `modal CTA tracking via cta_click taxonomy`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Precedence Localstorage Cookie`** (1 nodes): `theme precedence localStorage -> cookie -> environment signals`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Cookie Fallback Long`** (1 nodes): `site_theme cookie fallback with long TTL`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Switch Event Automatic`** (1 nodes): `no theme_switch event on automatic restoration`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Surface`** (1 nodes): `SEO content surface spec`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Social Aggregation`** (1 nodes): `Social content aggregation spec`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Homepage Modules`** (1 nodes): `Homepage content modules spec`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Conversion Ctas`** (1 nodes): `Conversion CTAs spec`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
- **Why does `2026-02-10 lighthouse fixes change` connect `Concept Modal Youtube` to `Layout Persistence Branding`?**
_High betweenness centrality (0.020) - this node is a cross-community bridge._
- **Why does `prefers-reduced-motion handling` connect `Layout Persistence Branding` to `Concept Modal Youtube`?**
_High betweenness centrality (0.017) - this node is a cross-community bridge._
- **Why does `main()` connect `Main Getingestconfigfromenv Getpublicconfig` to `Cache Main Createcachefromenv`, `Compute Sleep Cachedcompute`?**
_High betweenness centrality (0.012) - this node is a cross-community bridge._
- **Are the 4 inferred relationships involving `main()` (e.g. with `getIngestConfigFromEnv()` and `createCacheFromEnv()`) actually correct?**
_`main()` has 4 INFERRED edges - model-reasoned connections that need verification._
- **Are the 7 inferred relationships involving `createCacheFromEnv()` (e.g. with `main()` and `main()`) actually correct?**
_`createCacheFromEnv()` has 7 INFERRED edges - model-reasoned connections that need verification._
- **Are the 2 inferred relationships involving `run()` (e.g. with `sleep()` and `log()`) actually correct?**
_`run()` has 2 INFERRED edges - model-reasoned connections that need verification._
- **What connects `Repository Guidelines`, `OpenSpec schema spec-driven`, `theme-switcher-notch` to the rest of the system?**
_78 weakly-connected nodes found - possible documentation gaps or missing edges._

12
graphify-out/cost.json Normal file
View File

@@ -0,0 +1,12 @@
{
"runs": [
{
"date": "2026-04-19T00:00:00Z",
"input_tokens": 0,
"output_tokens": 0,
"files": 177
}
],
"total_input_tokens": 0,
"total_output_tokens": 0
}

257
graphify-out/graph.html Normal file

File diff suppressed because one or more lines are too long

4697
graphify-out/graph.json Normal file

File diff suppressed because it is too large Load Diff

179
graphify-out/manifest.json Normal file
View File

@@ -0,0 +1,179 @@
{
"site\\astro.config.mjs": 1770696052.5299394,
"site\\public\\sw.js": 1770778835.0382543,
"site\\scripts\\cache-clear.ts": 1770704069.523524,
"site\\scripts\\fetch-content.ts": 1770759379.7095494,
"site\\scripts\\run-lighthouse.ts": 1770779378.6530933,
"site\\scripts\\verify-blog-build.ts": 1770703147.2500439,
"site\\scripts\\verify-umami-in-dist.ts": 1770722100.1253102,
"site\\src\\env.d.ts": 1770781865.8927536,
"site\\src\\lib\\config.ts": 1770781915.8184347,
"site\\src\\lib\\links.ts": 1770760446.5389628,
"site\\src\\lib\\url.ts": 1770696052.7901123,
"site\\src\\lib\\cache\\index.ts": 1770704027.8143616,
"site\\src\\lib\\cache\\memoize.ts": 1770704096.3146253,
"site\\src\\lib\\cache\\memory-cache.ts": 1770704090.5106878,
"site\\src\\lib\\cache\\noop-cache.ts": 1770704019.2556648,
"site\\src\\lib\\cache\\redis-cache.ts": 1770704014.1144633,
"site\\src\\lib\\content\\cache.ts": 1770702913.9393668,
"site\\src\\lib\\content\\curation.ts": 1770696052.7545974,
"site\\src\\lib\\content\\selectors.ts": 1770702925.4313648,
"site\\src\\lib\\content\\types.ts": 1770759237.107753,
"site\\src\\lib\\ingest\\instagram.ts": 1770696052.7691603,
"site\\src\\lib\\ingest\\podcast.ts": 1770759246.3350432,
"site\\src\\lib\\ingest\\types.ts": 1770696052.7765744,
"site\\src\\lib\\ingest\\wordpress.ts": 1770702898.2534895,
"site\\src\\lib\\ingest\\youtube.ts": 1770707408.2922978,
"site\\tests\\blog-nav.test.ts": 1770703139.8473601,
"site\\tests\\blog-umami-attributes.test.ts": 1770707507.5898702,
"site\\tests\\cache-wrapper.test.ts": 1770704106.4231348,
"site\\tests\\card-layout.test.ts": 1770778848.318161,
"site\\tests\\content-title-type-attributes.test.ts": 1770707512.7382674,
"site\\tests\\ingest.test.ts": 1770696052.869126,
"site\\tests\\umami-attributes.test.ts": 1770757650.2056983,
"site\\tests\\wcag-responsive-shell.test.ts": 1770778855.4394329,
"AGENTS.md": 1776628237.9663427,
"DIAGNOSIS_COMPLETE.md": 1770774350.9478524,
"DIAGNOSIS_SUMMARY.txt": 1770774329.1455705,
"PRODUCTION_DIAGNOSIS.md": 1770774313.8073833,
"README.md": 1770783181.1709754,
"review-notes.md": 1775892139.263028,
"deploy\\runbook.md": 1770716901.5902822,
"openspec\\changes\\archive\\2026-02-10-better-cache\\design.md": 1770703824.4651365,
"openspec\\changes\\archive\\2026-02-10-better-cache\\proposal.md": 1770703732.19924,
"openspec\\changes\\archive\\2026-02-10-better-cache\\tasks.md": 1770704138.6287496,
"openspec\\changes\\archive\\2026-02-10-better-cache\\specs\\cache-layer\\spec.md": 1770703833.315094,
"openspec\\changes\\archive\\2026-02-10-better-cache\\specs\\social-content-aggregation\\spec.md": 1770703840.6947522,
"openspec\\changes\\archive\\2026-02-10-better-cache\\specs\\wordpress-content-source\\spec.md": 1770703850.2886736,
"openspec\\changes\\archive\\2026-02-10-better-tracking\\design.md": 1770705507.058264,
"openspec\\changes\\archive\\2026-02-10-better-tracking\\proposal.md": 1770705406.445285,
"openspec\\changes\\archive\\2026-02-10-better-tracking\\tasks.md": 1770705742.4905672,
"openspec\\changes\\archive\\2026-02-10-better-tracking\\specs\\analytics-umami\\spec.md": 1770705488.2900243,
"openspec\\changes\\archive\\2026-02-10-better-tracking\\specs\\interaction-tracking-taxonomy\\spec.md": 1770705478.2533162,
"openspec\\changes\\archive\\2026-02-10-blog-umami-fix\\design.md": 1770704655.2887678,
"openspec\\changes\\archive\\2026-02-10-blog-umami-fix\\proposal.md": 1770704562.6725414,
"openspec\\changes\\archive\\2026-02-10-blog-umami-fix\\tasks.md": 1770705110.210762,
"openspec\\changes\\archive\\2026-02-10-blog-umami-fix\\specs\\analytics-umami\\spec.md": 1770704664.1777132,
"openspec\\changes\\archive\\2026-02-10-blog-umami-fix\\specs\\blog-section-surface\\spec.md": 1770704682.900847,
"openspec\\changes\\archive\\2026-02-10-blog-umami-fix\\specs\\interaction-tracking-taxonomy\\spec.md": 1770704671.2466474,
"openspec\\changes\\archive\\2026-02-10-blogs-section\\design.md": 1770702604.1822934,
"openspec\\changes\\archive\\2026-02-10-blogs-section\\proposal.md": 1770702501.7027605,
"openspec\\changes\\archive\\2026-02-10-blogs-section\\tasks.md": 1770703159.5544684,
"openspec\\changes\\archive\\2026-02-10-blogs-section\\specs\\blog-section-surface\\spec.md": 1770702645.5590143,
"openspec\\changes\\archive\\2026-02-10-blogs-section\\specs\\seo-content-surface\\spec.md": 1770702651.325966,
"openspec\\changes\\archive\\2026-02-10-blogs-section\\specs\\wordpress-content-source\\spec.md": 1770702632.988938,
"openspec\\changes\\archive\\2026-02-10-card-layout\\design.md": 1770707076.6786592,
"openspec\\changes\\archive\\2026-02-10-card-layout\\proposal.md": 1770706949.8167002,
"openspec\\changes\\archive\\2026-02-10-card-layout\\tasks.md": 1770707574.8295856,
"openspec\\changes\\archive\\2026-02-10-card-layout\\specs\\blog-section-surface\\spec.md": 1770709072.656981,
"openspec\\changes\\archive\\2026-02-10-card-layout\\specs\\card-layout-system\\spec.md": 1770709066.1405613,
"openspec\\changes\\archive\\2026-02-10-card-layout\\specs\\social-content-aggregation\\spec.md": 1770707042.9422977,
"openspec\\changes\\archive\\2026-02-10-custom-events-umami\\design.md": 1770698186.0620773,
"openspec\\changes\\archive\\2026-02-10-custom-events-umami\\proposal.md": 1770698096.4864,
"openspec\\changes\\archive\\2026-02-10-custom-events-umami\\tasks.md": 1770699377.160174,
"openspec\\changes\\archive\\2026-02-10-custom-events-umami\\specs\\analytics-umami\\spec.md": 1770699508.9692678,
"openspec\\changes\\archive\\2026-02-10-custom-events-umami\\specs\\conversion-ctas\\spec.md": 1770699508.970271,
"openspec\\changes\\archive\\2026-02-10-custom-events-umami\\specs\\interaction-tracking-taxonomy\\spec.md": 1770699508.970271,
"openspec\\changes\\archive\\2026-02-10-deploy-without-node\\design.md": 1770707374.7418158,
"openspec\\changes\\archive\\2026-02-10-deploy-without-node\\proposal.md": 1770707214.776635,
"openspec\\changes\\archive\\2026-02-10-deploy-without-node\\tasks.md": 1770709577.2629378,
"openspec\\changes\\archive\\2026-02-10-deploy-without-node\\specs\\docker-content-refresh\\spec.md": 1770707356.346542,
"openspec\\changes\\archive\\2026-02-10-dynamic-homepage-social-acquisition\\design.md": 1770694717.0404153,
"openspec\\changes\\archive\\2026-02-10-dynamic-homepage-social-acquisition\\proposal.md": 1770694008.3186584,
"openspec\\changes\\archive\\2026-02-10-dynamic-homepage-social-acquisition\\tasks.md": 1770696978.4067898,
"openspec\\changes\\archive\\2026-02-10-dynamic-homepage-social-acquisition\\specs\\analytics-umami\\spec.md": 1770694818.6898234,
"openspec\\changes\\archive\\2026-02-10-dynamic-homepage-social-acquisition\\specs\\conversion-ctas\\spec.md": 1770694818.689301,
"openspec\\changes\\archive\\2026-02-10-dynamic-homepage-social-acquisition\\specs\\homepage-content-modules\\spec.md": 1770694818.6882658,
"openspec\\changes\\archive\\2026-02-10-dynamic-homepage-social-acquisition\\specs\\seo-content-surface\\spec.md": 1770694818.689301,
"openspec\\changes\\archive\\2026-02-10-dynamic-homepage-social-acquisition\\specs\\social-content-aggregation\\spec.md": 1770694818.6882658,
"openspec\\changes\\archive\\2026-02-10-fix-sub-pages\\design.md": 1770700011.169449,
"openspec\\changes\\archive\\2026-02-10-fix-sub-pages\\proposal.md": 1770699997.097304,
"openspec\\changes\\archive\\2026-02-10-fix-sub-pages\\tasks.md": 1770700170.4931765,
"openspec\\changes\\archive\\2026-02-10-fix-sub-pages\\specs\\seo-content-surface\\spec.md": 1770700019.211279,
"openspec\\changes\\archive\\2026-02-10-hide-ig-if-no-data\\design.md": 1770701301.1565921,
"openspec\\changes\\archive\\2026-02-10-hide-ig-if-no-data\\proposal.md": 1770701242.1272779,
"openspec\\changes\\archive\\2026-02-10-hide-ig-if-no-data\\tasks.md": 1770701782.6076858,
"openspec\\changes\\archive\\2026-02-10-hide-ig-if-no-data\\specs\\homepage-content-modules\\spec.md": 1770701314.463359,
"openspec\\changes\\archive\\2026-02-10-lazy-loading\\design.md": 1770754464.6107624,
"openspec\\changes\\archive\\2026-02-10-lazy-loading\\proposal.md": 1770754317.0704117,
"openspec\\changes\\archive\\2026-02-10-lazy-loading\\tasks.md": 1770756003.4601965,
"openspec\\changes\\archive\\2026-02-10-lazy-loading\\specs\\card-layout-system\\spec.md": 1770754502.9136808,
"openspec\\changes\\archive\\2026-02-10-lazy-loading\\specs\\image-lazy-loading\\spec.md": 1770754492.779088,
"openspec\\changes\\archive\\2026-02-10-lighthouse-fixes\\baseline-audits.md": 1770778202.0321891,
"openspec\\changes\\archive\\2026-02-10-lighthouse-fixes\\design.md": 1770776916.8084137,
"openspec\\changes\\archive\\2026-02-10-lighthouse-fixes\\proposal.md": 1770776761.5123682,
"openspec\\changes\\archive\\2026-02-10-lighthouse-fixes\\tasks.md": 1770780194.3872564,
"openspec\\changes\\archive\\2026-02-10-lighthouse-fixes\\specs\\lighthouse-quality-gate\\spec.md": 1770777148.4286618,
"openspec\\changes\\archive\\2026-02-10-lighthouse-fixes\\specs\\media-modal\\spec.md": 1770777148.4321685,
"openspec\\changes\\archive\\2026-02-10-lighthouse-fixes\\specs\\responsive-image-delivery\\spec.md": 1770777148.4301648,
"openspec\\changes\\archive\\2026-02-10-lighthouse-fixes\\specs\\seo-content-surface\\spec.md": 1770777148.4311683,
"openspec\\changes\\archive\\2026-02-10-lighthouse-fixes\\specs\\service-worker-performance\\spec.md": 1770777148.4341688,
"openspec\\changes\\archive\\2026-02-10-lighthouse-fixes\\specs\\site-theming\\spec.md": 1770777148.4331682,
"openspec\\changes\\archive\\2026-02-10-lighthouse-fixes\\specs\\wcag-responsive-ui\\spec.md": 1770777148.4331682,
"openspec\\changes\\archive\\2026-02-10-reduce-bounce-rate\\design.md": 1770762430.7436118,
"openspec\\changes\\archive\\2026-02-10-reduce-bounce-rate\\proposal.md": 1770762430.7230804,
"openspec\\changes\\archive\\2026-02-10-reduce-bounce-rate\\tasks.md": 1770762388.7942297,
"openspec\\changes\\archive\\2026-02-10-reduce-bounce-rate\\specs\\analytics-umami\\spec.md": 1770751643.803513,
"openspec\\changes\\archive\\2026-02-10-reduce-bounce-rate\\specs\\card-layout-system\\spec.md": 1770751609.4006913,
"openspec\\changes\\archive\\2026-02-10-reduce-bounce-rate\\specs\\conversion-ctas\\spec.md": 1770762298.293718,
"openspec\\changes\\archive\\2026-02-10-reduce-bounce-rate\\specs\\interaction-tracking-taxonomy\\spec.md": 1770762310.9267697,
"openspec\\changes\\archive\\2026-02-10-reduce-bounce-rate\\specs\\media-modal\\spec.md": 1770762325.8561277,
"openspec\\changes\\archive\\2026-02-10-service-workers\\design.md": 1770716121.2041433,
"openspec\\changes\\archive\\2026-02-10-service-workers\\proposal.md": 1770716121.2041433,
"openspec\\changes\\archive\\2026-02-10-service-workers\\tasks.md": 1770716818.0964458,
"openspec\\changes\\archive\\2026-02-10-service-workers\\specs\\service-worker-performance\\spec.md": 1770716121.2059233,
"openspec\\changes\\archive\\2026-02-10-wcag-responsive\\design.md": 1770710504.0920277,
"openspec\\changes\\archive\\2026-02-10-wcag-responsive\\proposal.md": 1770710381.4593272,
"openspec\\changes\\archive\\2026-02-10-wcag-responsive\\tasks.md": 1770711283.7843459,
"openspec\\changes\\archive\\2026-02-10-wcag-responsive\\specs\\wcag-responsive-ui\\spec.md": 1770710523.4914804,
"openspec\\changes\\archive\\2026-02-11-dch-theming\\design.md": 1770767108.971421,
"openspec\\changes\\archive\\2026-02-11-dch-theming\\proposal.md": 1770766934.7293966,
"openspec\\changes\\archive\\2026-02-11-dch-theming\\tasks.md": 1770771885.6166718,
"openspec\\changes\\archive\\2026-02-11-dch-theming\\specs\\site-theming\\spec.md": 1770767154.674113,
"openspec\\changes\\archive\\2026-02-11-dch-theming\\specs\\theme-switcher-notch\\spec.md": 1770767168.3042498,
"openspec\\changes\\archive\\2026-02-11-dch-theming\\specs\\wcag-responsive-ui\\spec.md": 1770767178.9348993,
"openspec\\changes\\archive\\2026-02-11-final-touches\\design.md": 1770782754.2948294,
"openspec\\changes\\archive\\2026-02-11-final-touches\\proposal.md": 1770782748.2698598,
"openspec\\changes\\archive\\2026-02-11-final-touches\\tasks.md": 1770782741.2076168,
"openspec\\changes\\archive\\2026-02-11-final-touches\\specs\\nav-hover-line\\spec.md": 1770782735.06448,
"openspec\\changes\\archive\\2026-02-11-final-touches\\specs\\navbar-branding\\spec.md": 1770781643.923366,
"openspec\\changes\\archive\\2026-02-11-remember-theme\\design.md": 1770772744.6321404,
"openspec\\changes\\archive\\2026-02-11-remember-theme\\proposal.md": 1770772417.4151812,
"openspec\\changes\\archive\\2026-02-11-remember-theme\\tasks.md": 1770773835.9645731,
"openspec\\changes\\archive\\2026-02-11-remember-theme\\specs\\analytics-umami\\spec.md": 1770772771.2684093,
"openspec\\changes\\archive\\2026-02-11-remember-theme\\specs\\interaction-tracking-taxonomy\\spec.md": 1770772779.25797,
"openspec\\changes\\archive\\2026-02-11-remember-theme\\specs\\site-theming\\spec.md": 1770772762.8810704,
"openspec\\specs\\analytics-umami\\spec.md": 1770773611.7136278,
"openspec\\specs\\blog-section-surface\\spec.md": 1770708953.3703814,
"openspec\\specs\\cache-layer\\spec.md": 1770704360.452241,
"openspec\\specs\\card-layout-system\\spec.md": 1770762704.0072663,
"openspec\\specs\\conversion-ctas\\spec.md": 1770762600.9134228,
"openspec\\specs\\docker-content-refresh\\spec.md": 1770709866.7621095,
"openspec\\specs\\homepage-content-modules\\spec.md": 1770703572.5904553,
"openspec\\specs\\image-lazy-loading\\spec.md": 1770756060.4065144,
"openspec\\specs\\interaction-tracking-taxonomy\\spec.md": 1770773621.7074296,
"openspec\\specs\\lighthouse-quality-gate\\spec.md": 1770780751.8868713,
"openspec\\specs\\media-modal\\spec.md": 1770780711.707662,
"openspec\\specs\\nav-hover-line\\spec.md": 1770782770.284241,
"openspec\\specs\\navbar-branding\\spec.md": 1770782770.2854366,
"openspec\\specs\\responsive-image-delivery\\spec.md": 1770780751.8878753,
"openspec\\specs\\seo-content-surface\\spec.md": 1770780729.5234754,
"openspec\\specs\\service-worker-performance\\spec.md": 1770780629.3490484,
"openspec\\specs\\site-theming\\spec.md": 1770780660.5313509,
"openspec\\specs\\social-content-aggregation\\spec.md": 1770708963.4293706,
"openspec\\specs\\theme-switcher-notch\\spec.md": 1770772046.8918262,
"openspec\\specs\\wcag-responsive-ui\\spec.md": 1770780642.3173912,
"openspec\\specs\\wordpress-content-source\\spec.md": 1770704349.2461488,
"site\\README.md": 1770695414.2709694,
"site\\docs\\lighthouse.md": 1770779458.022776,
"site\\public\\robots.txt": 1770777495.024902,
"blog-screenshot.png": 1770774244.9107263,
"csp-validation-final.png": 1770779783.4508164,
"header-desktop-1366x768.png": 1770782205.9854114,
"header-hover-effect-desktop.png": 1770782268.1240194,
"header-hover-videos.png": 1770782218.7184997,
"header-mobile-390x844.png": 1770782209.5139008,
"homepage-screenshot.png": 1770774200.8950064,
"site\\public\\favicon.png": 1770724544.5309453,
"site\\public\\favicon.svg": 1770725206.2187872
}

BIN
header-desktop-1366x768.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

BIN
header-hover-videos.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

BIN
header-mobile-390x844.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

BIN
homepage-screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 800 KiB

View File

@@ -0,0 +1,74 @@
## Context
The site is a static Astro SSG with no framework islands — all client-side interactivity uses vanilla JS via `<script is:inline>` tags. Images appear in two surfaces:
1. **Card thumbnails**`StandardCard.astro` renders `<img src={imageUrl} alt="" loading="lazy" />` inside a `.card-media` div. When there's no image, a static `.card-placeholder` div fills the space. The image area is fixed at 180px height (200px on mobile).
2. **Blog featured images**`blog/post/[slug].astro` and `blog/page/[slug].astro` render `<img loading="lazy" />` inline with a max-height of 420px.
Currently `loading="lazy"` defers fetching until the image nears the viewport, but the image area is blank/transparent until download completes, causing a jarring pop-in.
The site's dark theme (`--bg0: #0b1020`) means the shimmer must be a subtle light-on-dark gradient sweep — not a grey-on-white pattern.
## Goals / Non-Goals
**Goals:**
- Show an animated shimmer placeholder while images load, matching the site's dark aesthetic
- Fade in the actual image smoothly once loaded
- Handle image load errors gracefully (keep placeholder visible)
- Respect `prefers-reduced-motion` (suppress shimmer animation)
- Zero new dependencies — CSS keyframes + vanilla JS only
**Non-Goals:**
- Generating low-quality image placeholders (LQIP/BlurHash) at build time — this requires a build pipeline change and image processing dependency
- Lazy loading changes to the `loading` attribute strategy — `loading="lazy"` is already set and works correctly
- Image format optimization (WebP/AVIF conversion) — separate concern
- Placeholder for Instagram embeds — those use a third-party embed script with its own loading state
## Decisions
### 1. CSS shimmer via `@keyframes` on a pseudo-element
**Decision:** The shimmer effect uses a CSS `@keyframes` animation on a `::before` pseudo-element of the image wrapper. The pseudo-element displays a translucent gradient that sweeps left-to-right.
**Rationale:** Pure CSS, no JS needed for the animation. Pseudo-elements avoid extra DOM nodes. The existing `.card-placeholder` already fills the image area — the shimmer just adds motion on top of it.
**Alternatives considered:**
- SVG animated placeholder: More complex markup, no visual benefit for a simple shimmer.
- JS-driven animation (`requestAnimationFrame`): Heavier, no benefit over CSS keyframes.
- `<canvas>` BlurHash: Requires a build-time hash generation pipeline — overkill for a shimmer.
### 2. Image starts hidden, fades in on `load` event
**Decision:** The `<img>` element renders with `opacity: 0` (via a CSS class like `.img-loading`). A small inline `<script>` listens for the `load` event on each image and removes the loading class, triggering a CSS `opacity` transition to fade in. The shimmer placeholder sits behind the image and is naturally hidden once the image becomes opaque.
**Rationale:** The `load` event is the reliable signal that the image is ready to display. `opacity` transition is GPU-composited and smooth. No layout shift because the image dimensions are fixed by the container (`.card-media img` has `height: 180px; object-fit: cover`).
**Implementation detail:** Images may already be cached by the browser (instant load). The script must handle the case where `img.complete && img.naturalWidth > 0` on page load — immediately remove the loading class without waiting for the `load` event.
**Alternatives considered:**
- `IntersectionObserver` to defer `src` assignment: Already handled by `loading="lazy"` — doubling up adds complexity for no benefit.
- CSS-only `:not([src])` or `content-visibility`: No reliable CSS-only way to detect "image has loaded."
### 3. Single reusable wrapper pattern
**Decision:** Wrap each `<img>` in a container element with class `.img-shimmer-wrap`. This wrapper gets the shimmer pseudo-element and positions the image on top. The same wrapper pattern is used for both card thumbnails and blog featured images.
**Rationale:** One CSS class, one script, works everywhere. The wrapper inherits the size from its parent (`.card-media` for cards, inline styles for blog images), so no per-surface sizing logic is needed.
### 4. Error handling: keep placeholder visible
**Decision:** On image `error` event, the loading class stays on (image remains `opacity: 0`), so the shimmer/placeholder remains visible. The shimmer animation stops (replaced by a static placeholder state) to avoid an endlessly-animating error state.
**Rationale:** A static placeholder is better UX than a broken image icon or an infinite shimmer. The user sees a clean grey block instead of a broken experience.
### 5. Reduced motion: static placeholder, no shimmer sweep
**Decision:** The existing global CSS rule (`@media (prefers-reduced-motion: reduce)`) already suppresses all animations and transitions to near-zero duration. The shimmer animation inherits this behavior automatically — no additional media query needed.
**Rationale:** The global rule (`animation-duration: 0.001ms !important`) already covers all keyframe animations. The shimmer becomes a static gradient overlay, which is still a valid loading indicator without motion.
## Risks / Trade-offs
- **[Layout shift on blog images]** → Blog featured images use inline `max-height: 420px` but no explicit `width`/`height` attributes, which could cause minor CLS. Mitigation: the wrapper inherits `width: 100%` and `max-height: 420px` from the existing inline styles, so the placeholder reserves the correct space. No change in CLS from current behavior.
- **[Already-cached images flash shimmer]** → If images are in the browser cache, they load near-instantly, but the shimmer might flicker briefly. Mitigation: the script checks `img.complete` on DOM ready and immediately reveals cached images — no visible shimmer for cached images.
- **[Increased CSS size]** → The shimmer keyframes and wrapper styles add ~30 lines to `global.css`. Mitigation: negligible impact on a static site with a single CSS file.

View File

@@ -0,0 +1,27 @@
## Why
All images on the site use `loading="lazy"` but render as blank space until the browser finishes downloading them. On slower connections or pages with many cards (homepage has 20+ cards), the user sees empty grey rectangles that pop into view abruptly. Adding a shimmer/skeleton placeholder while images load gives the perception of a faster, more polished page — the same pattern used by LinkedIn, YouTube, and AMP pages.
## What Changes
- **Shimmer placeholder for card thumbnails.** The `.card-media` area displays an animated skeleton shimmer while the `<img>` is loading. Once the image loads, it fades in over the shimmer. If the image fails to load, the placeholder remains visible (graceful degradation).
- **Shimmer placeholder for blog featured images.** Blog post and page detail pages show the same shimmer treatment on the hero/featured image while it loads.
- **Reduced-motion support.** The shimmer animation is suppressed when `prefers-reduced-motion: reduce` is active — the placeholder shows as a static block instead of animating.
## Capabilities
### New Capabilities
- `image-lazy-loading`: Shimmer/skeleton placeholder system for images that displays an animated loading state while images download, fades in the image on load, and handles load failures gracefully.
### Modified Capabilities
- `card-layout-system`: Card image area gains shimmer placeholder behavior during image loading (visual enhancement, no layout changes).
## Impact
- **Components**: `StandardCard.astro` — the `<img>` element needs a wrapper or sibling element for the shimmer, plus a script to detect load/error events.
- **Pages**: `blog/post/[slug].astro`, `blog/page/[slug].astro` — featured images get the same shimmer treatment.
- **CSS**: `global.css` — new shimmer animation keyframes and placeholder styles.
- **JS**: Small inline script to listen for image `load`/`error` events and toggle visibility classes.
- **No backend changes.** No data model, ingestion, or caching changes.
- **No new dependencies.** Pure CSS animation + vanilla JS event listeners.
- **Accessibility**: Shimmer respects `prefers-reduced-motion` (existing global rule covers animation suppression). No content or semantic changes.

View File

@@ -0,0 +1,35 @@
## MODIFIED Requirements
### Requirement: Standard card information architecture
All content cards rendered by the site MUST use a standardized layout so cards across different surfaces look consistent.
The standard card layout MUST be:
- featured image displayed prominently at the top (when available), with a shimmer placeholder visible while the image loads
- title
- summary/excerpt text, trimmed to a fixed maximum length
- footer row showing:
- publish date on the left
- views when available (if omitted, the footer MUST still render cleanly)
- the content source label (e.g., `youtube`, `podcast`, `blog`)
If a field is not available (for example, views for some sources), the card MUST still render cleanly with that field omitted.
#### Scenario: Card renders with all fields
- **WHEN** a content item has an image, title, summary, publish date, views, and source
- **THEN** the card renders those fields in the standard card layout order
#### Scenario: Card renders without views
- **WHEN** a content item has no views data
- **THEN** the card renders the footer bar with date + source and omits views without breaking the layout
#### Scenario: Card renders without featured image
- **WHEN** a content item has no featured image
- **THEN** the card renders a placeholder media area and still renders the remaining fields
#### Scenario: Card image shows shimmer while loading
- **WHEN** a content item has an image URL and the image has not yet loaded
- **THEN** the card media area displays an animated shimmer placeholder until the image loads and fades in
#### Scenario: Card image load failure shows static placeholder
- **WHEN** a content item has an image URL but the image fails to load
- **THEN** the card media area displays a static placeholder (no broken image icon) and the card remains visually intact

View File

@@ -0,0 +1,50 @@
## ADDED Requirements
### Requirement: Shimmer placeholder while images load
Every site image that uses `loading="lazy"` MUST display an animated shimmer placeholder in its container while the image is downloading.
The shimmer MUST be a translucent gradient sweep animation that matches the site's dark theme.
The shimmer MUST be visible from the moment the page renders until the image finishes loading.
#### Scenario: Image loads successfully on slow connection
- **WHEN** a page renders with a lazy-loaded image and the image takes time to download
- **THEN** the image container displays an animated shimmer placeholder until the image finishes loading
#### Scenario: Image loads from browser cache
- **WHEN** a page renders with a lazy-loaded image that is already in the browser cache
- **THEN** the image displays immediately with no visible shimmer flicker
### Requirement: Fade-in transition on image load
When a lazy-loaded image finishes downloading, it MUST fade in smoothly over the shimmer placeholder using a CSS opacity transition.
The fade-in duration MUST be short enough to feel responsive (no longer than 300ms).
#### Scenario: Image completes loading
- **WHEN** a lazy-loaded image finishes downloading
- **THEN** the image fades in over approximately 200300ms, replacing the shimmer placeholder
### Requirement: Graceful degradation on image load failure
If a lazy-loaded image fails to load (network error, 404, etc.), the shimmer animation MUST stop and the placeholder MUST remain visible as a static block.
The page MUST NOT display a broken image icon.
#### Scenario: Image fails to load
- **WHEN** a lazy-loaded image triggers an error event (e.g., 404 or network failure)
- **THEN** the shimmer animation stops and the container displays a static placeholder background instead of a broken image icon
### Requirement: Reduced motion support for shimmer
The shimmer animation MUST be suppressed when the user has `prefers-reduced-motion: reduce` enabled.
When motion is reduced, the placeholder MUST still be visible as a static block (no animation), maintaining the loading indicator without motion.
#### Scenario: User has reduced motion enabled
- **WHEN** a user with `prefers-reduced-motion: reduce` views a page with lazy-loaded images
- **THEN** the placeholder is visible as a static block without any sweeping animation
### Requirement: No layout shift from shimmer
The shimmer placeholder MUST NOT cause any cumulative layout shift (CLS). The placeholder MUST occupy the exact same dimensions as the image it replaces.
#### Scenario: Placeholder matches image dimensions
- **WHEN** a page renders with a shimmer placeholder for a card thumbnail
- **THEN** the placeholder occupies the same width and height as the image area (e.g., 100% width × 180px height for card thumbnails) with no layout shift when the image loads

View File

@@ -0,0 +1,34 @@
## 1. CSS shimmer styles
- [x] 1.1 Add `@keyframes shimmer` animation to `global.css` — a translucent gradient sweep (left-to-right) that works on the dark theme background (`--bg0`/`--bg1` palette).
- [x] 1.2 Add `.img-shimmer-wrap` class to `global.css``position: relative; overflow: hidden;` container that inherits dimensions from its parent. Add a `::before` pseudo-element with the shimmer animation (full width/height, absolute positioned, translucent light gradient).
- [x] 1.3 Add `.img-loading` class to `global.css` — sets `opacity: 0` on the `<img>` element. Add transition: `opacity 250ms ease`.
- [x] 1.4 Add `.img-error` class to `global.css` — stops the shimmer animation on the wrapper (`animation: none` on `::before`) so the placeholder displays as a static block.
- [x] 1.5 Verify the existing `@media (prefers-reduced-motion: reduce)` rule in `global.css` already suppresses the shimmer `@keyframes` animation (it should — the global rule sets `animation-duration: 0.001ms !important`).
## 2. Card thumbnail shimmer
- [x] 2.1 Update `StandardCard.astro` — wrap the existing `<img>` in a `<div class="img-shimmer-wrap">`. Add class `img-loading` to the `<img>` element. Keep the existing `.card-placeholder` fallback for cards with no image (no shimmer needed there).
- [x] 2.2 Ensure the `.img-shimmer-wrap` inside `.card-media` inherits the correct dimensions (`width: 100%; height: 180px` on desktop, `200px` on mobile) without causing layout shift.
## 3. Blog featured image shimmer
- [x] 3.1 Update `blog/post/[slug].astro` — wrap the featured `<img>` in a `<div class="img-shimmer-wrap">` with matching inline styles (`width: 100%; max-height: 420px; border-radius: 16px; overflow: hidden;`). Add class `img-loading` to the `<img>`.
- [x] 3.2 Update `blog/page/[slug].astro` — same shimmer wrapper treatment as blog posts.
## 4. Image load/error script
- [x] 4.1 Add an inline `<script is:inline>` in `BaseLayout.astro` (or a shared location) that runs on DOM ready. For every `img.img-loading` element: if `img.complete && img.naturalWidth > 0`, remove `img-loading` immediately (cached images). Otherwise, add `load` event listener to remove `img-loading` and `error` event listener to add `img-error` to the wrapper.
- [x] 4.2 Verify the script handles dynamically added images (not currently needed — SSG renders all images server-side — but ensure the script runs after all DOM is ready).
## 5. Verification
- [x] 5.1 Run `npm run build` in `site/` and verify no build errors.
- [x] 5.2 Run `npm test` in `site/` and verify all existing tests pass (1 pre-existing failure in blog-nav.test.ts unrelated to this change — expects `/about` nav link that doesn't exist).
- [x] 5.3 Manual smoke test: throttle network in DevTools (Slow 3G), load homepage — verify shimmer appears on card thumbnails, images fade in on load.
- [x] 5.4 Manual smoke test: load a blog post with a featured image on throttled network — verify shimmer and fade-in.
- [x] 5.5 Manual smoke test: break an image URL temporarily — verify static placeholder shows (no broken image icon, no infinite shimmer).
- [x] 5.6 Manual smoke test: enable `prefers-reduced-motion: reduce` in browser/OS settings — verify shimmer animation is suppressed (static placeholder, no sweep).
- [x] 5.7 Verify no cumulative layout shift: compare card dimensions before and after the change using DevTools Layout Shift overlay.
Note: Tasks 5.35.7 require manual browser testing with DevTools network throttling.

View File

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

View File

@@ -0,0 +1,42 @@
# Lighthouse Baseline (From Provided Reports)
Source reports used:
- `C:\Users\simpl\Downloads\santhoshj.com-light-mobile.json`
- `C:\Users\simpl\Downloads\santhoshj.com-dark-mobile.json`
- `C:\Users\simpl\Downloads\santhoshj.com-hc-mobile.json`
- `C:\Users\simpl\Downloads\santhoshj.com-hc-desktop.json`
- `C:\Users\simpl\Downloads\santhoshj.com-light-desktop.json`
- `C:\Users\simpl\Downloads\santhoshj.com-dark-desktop.json`
## Category Scores (Baseline)
- `light-mobile`: performance 92, accessibility 100, best-practices 96, seo 85
- `dark-mobile`: performance 83, accessibility 95, best-practices 96, seo 85
- `hc-mobile`: performance 84, accessibility 100, best-practices 96, seo 85
- `hc-desktop`: performance 98, accessibility 100, best-practices 96, seo 85
- `light-desktop`: performance 99, accessibility 100, best-practices 96, seo 85
- `dark-desktop`: performance 99, accessibility 95, best-practices 96, seo 85
## Failing Audits Seen In 6/6 Reports
- `unminified-css`
- `unused-css-rules`
- `unused-javascript`
- `inspector-issues`
- `crawlable-anchors`
- `robots-txt`
- `cache-insight`
- `forced-reflow-insight`
- `image-delivery-insight`
- `network-dependency-tree-insight`
- `render-blocking-insight`
## Intermittent / Variant-Specific Fails
- `color-contrast` (2/6)
- `interactive` (3/6)
## Environmental Warning In Reports
- Chrome extension interference warning appears in reports. This is why the new gate requires clean profile runs.

View File

@@ -0,0 +1,144 @@
## Context
The production site is built as static output and served via nginx (Docker image). Lighthouse runs (mobile/desktop, light/dark/high-contrast) report repeated misses across Performance, SEO, Best Practices, and (in dark theme) Accessibility.
Notable characteristics from the current system:
- SEO is mostly static-file driven (`site/public/robots.txt`, sitemap output) and must be correct for the deployed domain.
- Global styling is served from `site/public/styles/global.css`, which bypasses typical bundler optimizations (minification, pruning) unless explicitly piped through the build.
- A service worker exists (`site/public/sw.js`) and is involved in caching shell assets; cache behavior must not cause stale critical assets after deploy.
- The site uses environment-driven configuration and has multiple theme modes (light/dark/high-contrast) that must remain visually consistent and accessible.
The goal is a deterministic, repeatable Lighthouse gate that can hit 100s in a clean run environment.
## Goals / Non-Goals
**Goals:**
- Reach 100 Lighthouse scores (Performance, Accessibility, Best Practices, SEO) for the defined set of URLs, form factors, and theme variants.
- Fix SEO correctness issues that are unambiguous (e.g., `robots.txt` sitemap URL must be absolute for the production domain).
- Ensure all user-visible navigational CTAs and modal actions are implemented as crawlable anchors (real `href`s) when they represent links.
- Eliminate dark theme contrast regressions by tightening theme token choices and/or component-level styles.
- Reduce initial render blocking work (fonts + CSS) to improve mobile performance.
- Reduce CSS overhead by ensuring the global stylesheet is minified and sized appropriately for production.
- Make Lighthouse runs deterministic (clean Chrome profile, fixed throttling/UA) and enforce a quality gate in CI or a scripted check.
**Non-Goals:**
- Redesigning the visual aesthetic.
- Building a general-purpose CSS framework migration.
- Perfect scores when Lighthouse is run with extensions or non-deterministic settings.
- Solving third-party performance/caching issues by weakening metrics (the goal is to reduce third-party dependence where it blocks 100s, or scope the gate to first-party pages that can be made deterministic).
## Decisions
### 1) Define the Lighthouse gate upfront
Decision: treat 100s as a contract under a specific, documented run configuration.
- URLs: start with the production home page and any key landing/content pages that are stable and representative.
- Variants: mobile/desktop x light/dark/high-contrast.
- Run environment: headless Chrome in CI (no extensions), consistent throttling, at least N runs per variant with median selection.
Rationale: Lighthouse scores are otherwise too sensitive to environment noise to serve as a reliable build gate.
Alternative considered: manual Lighthouse checks. Rejected because it does not prevent regressions.
### 2) Fix SEO static outputs in `public/`
Decision: keep `robots.txt` and sitemap-related outputs as static files, but ensure correctness for production.
- Make the sitemap reference in `site/public/robots.txt` absolute (not a relative path).
Rationale: This is required for Lighthouse SEO audits and is the least invasive change.
Alternative considered: generate robots dynamically. Rejected as unnecessary complexity.
### 3) Enforce crawlable links in modal/CTA surfaces
Decision: if an element semantically represents navigation (internal or outbound), it must be an `<a href="...">`.
- For modal components, ensure the CTA is rendered as an anchor with a real destination URL when appropriate.
- If the interaction is not navigation (e.g., close modal), use a `<button>` and ensure it is accessible.
Rationale: Lighthouse flags anchors without `href` as non-crawlable. Using correct semantics also improves accessibility.
Alternative considered: keep `<a>` without `href` and add JS click handlers. Rejected (SEO + a11y regression).
### 4) Process `global.css` through a production pipeline
Decision: stop serving the main global stylesheet as an unprocessed static asset.
Options:
- Import `global.css` from the Astro entry/layout so Vite can minify it for production builds.
- If the site intentionally keeps a standalone global CSS file, add an explicit build step to minify it and (optionally) run a basic unused rule trimming strategy.
Rationale: Lighthouse currently flags unminified CSS and unused CSS rules; bundling/minification is the most straightforward fix.
Alternative considered: leave it in `public/` and accept lower scores. Rejected (goal is 100s).
### 5) Font loading strategy optimized for mobile performance
Decision: make font loading non-blocking and deterministic.
- Prefer self-hosted fonts or Astro's recommended font strategy.
- Use `font-display: swap` and consider `preload` for critical weights.
- Avoid unnecessary font variants.
Rationale: render-blocking and late text rendering negatively impact Performance and perceived load.
Alternative considered: keep current remote font loading. Rejected if it remains render-blocking.
### 6) Theme token constraints for contrast
Decision: fix contrast failures at the token level first, with targeted overrides only when needed.
- Identify failing color pairs (foreground/background) in dark mode.
- Update tokens in `site/public/styles/global.css` so common surfaces always meet contrast expectations.
Rationale: token-level fixes prevent regressions across multiple components.
Alternative considered: per-component overrides only. Rejected as brittle.
### 7) CSP / Best Practices compliance
Decision: address Lighthouse Best Practices issues by aligning nginx headers with modern expectations without breaking the site.
- Keep security headers in `deploy/nginx.conf`.
- If inline scripts/styles exist, prefer refactoring to external files to avoid unsafe CSP allowances; otherwise use a nonce/hash strategy.
Rationale: strong CSP improves security posture and can satisfy Lighthouse findings, but must be implemented in a way compatible with the build output.
Alternative considered: disable CSP to appease some checks. Rejected.
### 8) Images: reduce third-party variability
Decision: prioritize first-party control of above-the-fold images.
- Use Astro image tooling for locally hosted images.
- For third-party images that block 100s (e.g., external thumbnails), prefer either self-hosting (if allowed) or avoid rendering them above the fold during initial load.
Rationale: image delivery and caching audits can be impossible to satisfy if key images are served from third parties with unknown caching/format behavior.
Alternative considered: ignore image findings. Rejected if they prevent the 100 gate.
## Risks / Trade-offs
- [Lighthouse instability] Scores vary across runs/environments -> Mitigation: lock run configuration and use a clean profile in CI.
- [Build pipeline change risk] Moving `global.css` into bundling may change selector precedence -> Mitigation: diff visual output across themes and add a lightweight snapshot/DOM test where possible.
- [CSP breakage] Tight CSP can block analytics or inline scripts -> Mitigation: inventory scripts, move to external, or add nonce/hash strategy.
- [Third-party dependencies] External media assets may prevent 100 -> Mitigation: reduce third-party assets on gated pages or self-host where permissible.
- [Service worker caching] SW can serve stale assets after deploy -> Mitigation: version critical assets and disable caching for SW + critical CSS, ensure SW update flow.
## Migration Plan
- Implement fixes behind minimal, incremental commits (SEO correctness, crawlable anchors, contrast tokens, CSS pipeline/font loading, CSP).
- Deploy with cache-busting in place so critical style updates reach clients quickly.
- Rollback: revert to a known-good image tag/digest and confirm SW cache versioning does not pin stale assets.
## Open Questions
- Which URLs are included in the Lighthouse gate (home only vs additional pages)?
- Are 100 scores required even when pages include third-party embeds/thumbnails, or should we scope the gate to pages that can be made first-party deterministic?
- Should schema.org structured data be implemented for all pages or only key landing/content types?

View File

@@ -0,0 +1,38 @@
## Why
Chrome Lighthouse runs against the production site show repeated misses across Performance, SEO, Best Practices, and (in dark theme) Accessibility. These gaps reduce technical robustness and make it hard to trust deploy-to-deploy quality.
We want the site to be measurably correct and fast under a repeatable, automated Lighthouse configuration, and to treat Lighthouse regressions as build-blocking.
## What Changes
- Make SEO outputs Lighthouse-clean by fixing `robots.txt` validity (absolute sitemap URL) and ensuring key links are crawlable.
- For better SEO, implement schema based on https://schema.org/docs/full.html
- If it helps performance and SEO, minify relevant assets.
- Remove non-crawlable anchor patterns (anchors without `href`) in the media modal and CTA surfaces.
- Fix dark theme contrast failures so text and links meet WCAG intent and Lighthouse `color-contrast` passes.
- Reduce render-blocking work in the initial load path (fonts + global stylesheet) to improve mobile Performance.
- Reduce CSS overhead by moving `global.css` through the build/minification pipeline and trimming unused rules.
- Eliminate forced reflow hotspots caused by synchronous layout reads/writes in scroll/resize handlers.
- Improve image delivery (size, format, and caching) to address Lighthouse image savings and cache lifetime findings.
- Add a deterministic Lighthouse quality gate (CI or scripted) that runs against known URLs, form factors, and themes and requires 100s in a clean environment (no extensions).
## Capabilities
### New Capabilities
- `lighthouse-quality-gate`: Define the target Lighthouse configuration (URLs, mobile/desktop, themes, run environment) and require 100/100/100/100 with report artifacts.
- `responsive-image-delivery`: Deliver appropriately sized images and modern formats (or controlled proxies/self-hosting) for key surfaces so image delivery audits pass consistently.
### Modified Capabilities
- `seo-content-surface`: Ensure `robots.txt` is valid and sitemap references are correct for the deployed domain; ensure Lighthouse SEO crawlability audits pass.
- `media-modal`: Ensure modal fallback and CTA links are crawlable anchors (real `href`s) and remain accessible.
- `wcag-responsive-ui`: Tighten requirements to prevent Lighthouse accessibility regressions (contrast and keyboard/focus behavior).
- `site-theming`: Ensure dark theme tokens never produce low-contrast text-on-light backgrounds on primary surfaces.
- `service-worker-performance`: Ensure caching behavior does not cause stale critical assets after deploy, and align cache strategy/headers with Lighthouse expectations where feasible.
## Impact
- Affected areas include: `site/public/robots.txt`, `site/src/layouts/BaseLayout.astro`, `site/src/components/MediaModal.astro`, `site/public/styles/global.css`, `site/public/sw.js`, and `deploy/nginx.conf`.
- This change may require build-pipeline adjustments (CSS processing/minification, image processing or controlled caching/proxying) and CI scripting to run Lighthouse deterministically.
- Some Lighthouse findings are attributable to third-party resources (e.g., YouTube thumbnails, externally hosted podcast art) and/or local Chrome extensions; hitting a strict 100 may require reducing third-party dependence and running audits in a clean profile.
- Coding agent may run out of context window. In that case, compact the context and retry with chunking.

View File

@@ -0,0 +1,46 @@
## ADDED Requirements
### Requirement: Deterministic Lighthouse configuration
The project MUST define a deterministic Lighthouse run configuration that specifies:
- a fixed list of URLs to audit
- mobile and desktop runs
- theme variants: `light`, `dark`, and `high-contrast`
- a clean run environment (no browser extensions)
- throttling / device emulation settings
The configuration MUST be checked into the repository.
#### Scenario: Lighthouse config is version-controlled
- **WHEN** a developer checks the repository
- **THEN** a Lighthouse configuration file/script exists and is referenced by a documented command
### Requirement: Lighthouse gate enforces perfect scores
The project MUST provide a command that runs Lighthouse for the configured URLs and variants and fails if any category score is below 100:
- Performance
- Accessibility
- Best Practices
- SEO
The command MUST output the reports as build artifacts (HTML and/or JSON) to a deterministic output directory.
#### Scenario: Gate fails on a regression
- **WHEN** Lighthouse runs and any audited variant scores 99 in any category
- **THEN** the command exits non-zero and reports which URL/variant failed
#### Scenario: Gate produces artifacts
- **WHEN** Lighthouse runs successfully
- **THEN** the reports are written to the configured output directory for later inspection
### Requirement: Repeatable scoring rule
The Lighthouse gate MUST define a repeatable scoring rule to reduce run-to-run noise.
At minimum, it MUST document one of the following:
- single-run with fixed throttling and clean environment, or
- multiple runs per variant with a deterministic selection rule (e.g., median)
#### Scenario: Run-to-run method is documented
- **WHEN** a developer runs the gate locally or in CI
- **THEN** the method for selecting/reporting scores is explicitly documented

View File

@@ -0,0 +1,49 @@
## MODIFIED Requirements
### Requirement: Media modal dialog
The site MUST provide a modal dialog that displays embedded media (YouTube video or podcast episode) when a user clicks a video or podcast content card on a listing surface (homepage, `/videos`, `/podcast`).
The modal MUST render the following elements in order:
- A header row with the content title on the left and a close button on the right
- An embedded media player (YouTube iframe for videos; Spotify embed for podcast episodes when the URL is a Spotify URL; otherwise an in-modal audio player when an `audioUrl` is available)
- The full description/summary text (not truncated)
- The publish date and view count (when available)
- A "Subscribe on YouTube" / "Follow on Spotify" CTA and a "View on YouTube" / "Listen on Spotify" CTA
All modal CTAs that represent navigation MUST be implemented as crawlable anchors:
- Each CTA MUST be an `<a>` element with a non-empty `href` attribute.
- The UI MUST NOT render placeholder `<a>` elements without `href` in the initial HTML.
- If CTA destinations are not known until a user selects an item, the CTA UI MUST be rendered as non-anchor elements until the destinations are known.
#### Scenario: User clicks a YouTube video card
- **WHEN** a user clicks a video content card on any listing surface
- **THEN** a modal dialog opens displaying a YouTube iframe embed, the video title, full description, date, view count (if available), and CTAs for "Subscribe on YouTube" and "View on YouTube"
#### Scenario: User clicks a podcast episode card with a Spotify URL
- **WHEN** a user clicks a podcast content card whose URL is a Spotify URL
- **THEN** a modal dialog opens displaying a Spotify episode embed, the episode title, full description, date, and CTAs for "Follow on Spotify" and "Listen on Spotify"
#### Scenario: User clicks a podcast episode card with a non-Spotify URL
- **WHEN** a user clicks a podcast content card whose URL is not a Spotify URL
- **THEN** the modal dialog opens displaying the episode metadata (title, description, date) and either:
- an in-modal audio player when an `audioUrl` is available
- otherwise, a "Listen on Spotify" outbound link
#### Scenario: Modal renders with missing optional fields
- **WHEN** a content item has no view count or no summary
- **THEN** the modal MUST still render cleanly with those fields omitted
#### Scenario: Modal CTAs are crawlable anchors
- **WHEN** the modal is present in the DOM (before any user interaction)
- **THEN** the document contains no `<a>` elements in the modal that are missing `href`
## ADDED Requirements
### Requirement: Embed fallback is a link only when a destination is available
If an embed fallback is presented as a link to an external page, it MUST be an anchor with a valid `href`. If no destination is available, the fallback MUST be hidden or rendered as non-link text.
#### Scenario: Embed fallback does not render a non-crawlable anchor
- **WHEN** the modal is rendered before any item selection
- **THEN** the embed fallback is not rendered as an anchor without `href`

View File

@@ -0,0 +1,29 @@
## ADDED Requirements
### Requirement: Card thumbnails have explicit dimensions
All card thumbnail images MUST include explicit `width` and `height` attributes matching the rendered aspect ratio.
#### Scenario: Thumbnail dimensions are present
- **WHEN** a crawler or browser loads a listing surface with content cards
- **THEN** each card thumbnail `<img>` includes `width` and `height` attributes
### Requirement: Above-the-fold imagery is optimized
The site MUST ensure above-the-fold images are optimized for performance:
- avoid unnecessarily large image payloads for the requested viewport
- prefer modern image formats when first-party controlled
- avoid layout shift caused by late image dimension discovery
#### Scenario: Above-the-fold images do not cause layout shift
- **WHEN** a user loads the home page on a mobile viewport
- **THEN** the hero/top content area does not shift vertically due to image loading
### Requirement: Third-party image variability is controlled for the Lighthouse gate
If a gated page depends on third-party images (e.g., external thumbnails), the gate MUST either:
- ensure those images do not block reaching 100s by design (e.g., not above-the-fold, lazy-loaded), or
- provide a first-party controlled alternative for gated pages.
#### Scenario: Gated pages avoid third-party image volatility
- **WHEN** Lighthouse runs against the gated URL set
- **THEN** third-party image delivery does not prevent meeting the required scores

View File

@@ -0,0 +1,39 @@
## MODIFIED Requirements
### Requirement: Sitemap and robots
The site MUST provide:
- `sitemap-index.xml` enumerating indexable pages (and/or referencing sitemap shards)
- `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 reference the sitemap using an absolute URL for the production domain.
#### Scenario: Sitemap index is available
- **WHEN** a crawler requests `/sitemap-index.xml`
- **THEN** the server returns an XML sitemap index (or 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 references sitemap with absolute URL
- **WHEN** a crawler requests `/robots.txt`
- **THEN** the response contains a `Sitemap:` line with an absolute URL to `/sitemap-index.xml`
## ADDED Requirements
### Requirement: Organization and website structured data
The home page SHOULD include JSON-LD structured data for the site and its owner/organization.
If present, the JSON-LD MUST be valid JSON and MUST use a recognized schema type.
#### Scenario: Home page includes valid JSON-LD
- **WHEN** a crawler requests `/`
- **THEN** the HTML contains a JSON-LD script tag that parses as valid JSON

View File

@@ -0,0 +1,17 @@
## ADDED Requirements
### Requirement: Critical assets do not remain stale after deploy
The service worker and server caching strategy MUST ensure critical shell assets (including the global stylesheet and service worker script) do not remain stale across deploys.
The implementation MUST include at least one cache-busting mechanism for critical assets, such as:
- content-hashed asset filenames, or
- an asset version query suffix that changes per deploy
#### Scenario: New deploy updates critical CSS
- **WHEN** a new deploy is released and a returning user loads the site
- **THEN** the user receives the updated global stylesheet without requiring a manual hard refresh
#### Scenario: Service worker updates predictably
- **WHEN** a new deploy is released
- **THEN** the browser can retrieve the updated service worker script and activate it without being pinned by long-lived caches

View File

@@ -0,0 +1,41 @@
## ADDED Requirements
### Requirement: Theme tokens meet contrast intent
For each supported theme (`dark`, `light`, `high-contrast`), the theme token pairs used for primary text and primary surfaces MUST meet WCAG 2.2 AA contrast intent.
At minimum:
- primary body text on the primary background MUST be high-contrast
- link text on the primary background MUST be distinguishable and meet contrast intent
- secondary labels on the primary background MUST remain readable
#### Scenario: Dark theme text is readable
- **WHEN** `data-theme="dark"` is active
- **THEN** primary text remains readable against primary surfaces without low-contrast combinations
#### Scenario: Dark theme links are readable
- **WHEN** `data-theme="dark"` is active
- **THEN** links in common surfaces are readable against their background
## MODIFIED Requirements
### Requirement: Site themes
The site MUST support three themes:
- `dark`
- `light`
- `high-contrast`
Themes MUST be applied by setting a `data-theme` attribute on the root document element (`<html>`).
#### Scenario: Dark theme active
- **WHEN** `data-theme="dark"` is set on `<html>`
- **THEN** the site's background, text, and component styling reflect the dark palette
#### Scenario: Light theme active
- **WHEN** `data-theme="light"` is set on `<html>`
- **THEN** the site's background, text, and component styling reflect the light palette
#### Scenario: High contrast theme active
- **WHEN** `data-theme="high-contrast"` is set on `<html>`
- **THEN** the site uses a high-contrast palette with a clearly visible focus ring and high-contrast borders

View File

@@ -0,0 +1,21 @@
## ADDED Requirements
### Requirement: Interactive elements use correct semantics
Interactive elements MUST use correct semantic elements:
- Navigation MUST use anchors (`<a>`) with valid `href`.
- Actions that do not navigate MUST use `<button>`.
The site MUST NOT render anchor elements that lack an `href` attribute.
#### Scenario: Modal triggers are buttons
- **WHEN** a content card opens the media modal
- **THEN** the card is a `<button>` element and is keyboard operable
#### Scenario: Navigation CTAs are anchors
- **WHEN** a user sees a CTA that navigates to another page
- **THEN** the CTA is an `<a>` element with a valid `href`
#### Scenario: No non-crawlable anchors exist
- **WHEN** a crawler inspects the rendered HTML
- **THEN** there are no `<a>` elements without `href`

View File

@@ -0,0 +1,52 @@
## 1. Baseline And Repro
- [x] 1.1 Add a repeatable Lighthouse run command (local) that outputs reports for mobile/desktop x light/dark/high-contrast
- [x] 1.2 Document the Lighthouse run environment requirements (clean profile / no extensions) and scoring rule (single vs median)
- [x] 1.3 Capture current failing audits in a checked-in note (from the provided JSON reports) to validate fixes one-by-one
## 2. SEO Correctness
- [x] 2.1 Update `site/public/robots.txt` to reference the sitemap with an absolute URL to `/sitemap-index.xml`
- [x] 2.2 Verify `site/astro.config.mjs` has `site` set correctly in production via `PUBLIC_SITE_URL` and that sitemap output is present after build
- [x] 2.3 Add/verify JSON-LD structured data on the all pages (valid JSON, recognized schema type)
## 3. Crawlable Anchors (Media Modal)
- [x] 3.1 Update `site/src/components/MediaModal.astro` so no modal anchors are rendered without `href` in the initial HTML
- [x] 3.2 Ensure modal CTAs that navigate are always `<a href="...">` and actions that do not navigate are `<button>`
- [x] 3.3 Re-run Lighthouse SEO audit to confirm `crawlable-anchors` passes
## 4. CSS Processing And Render Blocking
- [x] 4.1 Move `site/public/styles/global.css` into the Astro/Vite build pipeline (so production output is minified and cache-busted)
- [x] 4.2 Update `site/src/layouts/BaseLayout.astro` to load the built CSS output and remove any no-longer-needed manual cache-busting for CSS
- [x] 4.3 Adjust `site/public/sw.js` shell precache list to match the new CSS delivery strategy
- [x] 4.4 Confirm Lighthouse no longer reports `unminified-css` and materially reduces `unused-css-rules` for the primary pages
## 5. Fonts
- [x] 5.1 Replace render-blocking Google Fonts with a self-hosted font strategy (woff2 + `font-display`)
- [x] 5.2 Preload only the critical font files/weights needed for above-the-fold rendering
- [x] 5.3 Re-run Lighthouse to confirm improved `render-blocking` findings and LCP trend
## 6. Images
- [x] 6.1 Add explicit `width`/`height` for card thumbnail images (and any other frequently used images missing dimensions)
- [x] 6.2 Ensure above-the-fold imagery on gated pages is deterministic and does not rely on third-party delivery for a perfect Lighthouse gate
- [x] 6.3 Re-run Lighthouse to validate reduced CLS and improved image-related savings audits
## 7. Service Worker / Cache Robustness
- [x] 7.1 Verify critical asset cache-busting is effective across deploys (SW + global CSS) and does not pin stale styles
- [x] 7.2 Confirm nginx cache headers are correct for `/sw.js` (no-store) and any critical CSS entrypoint
## 8. Security Headers (Best Practices)
- [x] 8.1 Add baseline security headers in `deploy/nginx.conf` (CSP, nosniff, frame-ancestors / X-Frame-Options, referrer-policy, permissions-policy)
- [x] 8.2 Choose a CSP tier (moderate vs hash-based) and ensure the site functions with no CSP console violations in the normal path
- [x] 8.3 Re-run Lighthouse Best Practices to confirm CSP-related inspector issues are resolved
## 9. CI Quality Gate
- [x] 9.1 Add a CI job that runs the Lighthouse command and uploads reports as artifacts
- [x] 9.2 Make CI fail if any configured URL/variant scores below 100 in any category

View File

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

View File

@@ -0,0 +1,109 @@
## Context
The site is a static Astro site (SSG) with no framework islands — all client-side interactivity uses vanilla JS via `<script is:inline>` tags. The existing design system is a dark-theme glassmorphism palette (CSS custom properties in `global.css`). Video and podcast content is rendered through `ContentCard.astro``StandardCard.astro`, which currently produce `<a>` tags linking directly to YouTube/Spotify (`target="_blank"`). There are no existing modal/dialog components in the codebase.
Content data comes from a pre-built JSON cache (`ContentItem` type) that contains `id`, `source`, `url`, `title`, `summary`, `publishedAt`, `thumbnailUrl`, and optional `metrics.views`. For podcasts, the cache may also include an `audioUrl` (from RSS enclosure) to enable a first-party audio fallback when a Spotify embed URL is not available.
Umami analytics primarily uses declarative `data-umami-event-*` attributes on HTML elements. For interactions that need runtime-derived properties (e.g., modal close method), the implementation may use `umami.track()`.
## Goals / Non-Goals
**Goals:**
- Keep users on-site when they click video/podcast content cards by opening an embedded player in a modal instead of navigating to YouTube/Spotify
- Stop media playback reliably when the modal is dismissed (close button, Escape key, backdrop click)
- Maintain the existing card visual layout — only the interaction target changes
- Track modal interactions (open, close, CTA clicks) via Umami using the existing `data-umami-event-*` attribute pattern
- Maintain WCAG 2.2 AA accessibility (focus trap, keyboard support, aria attributes)
**Non-Goals:**
- Changing the detail pages (`/videos/[id]`, `/podcast/[id]`) — they remain as-is for SEO/sharing
- Introducing a JS framework (React/Preact/Svelte) — the modal uses vanilla JS consistent with the rest of the project
- Instagram card behavior — Instagram cards are unaffected (they already use a different embed approach)
- Live view count fetching from YouTube API at modal-open time — views come from the pre-built cache. Live counts are a future enhancement
- Picture-in-picture or mini-player functionality
- Autoplay — the user must initiate playback within the embed
## Decisions
### 1. Use native `<dialog>` element for the modal
**Decision:** Use the HTML `<dialog>` element with `showModal()` / `close()`.
**Rationale:** The native `<dialog>` provides built-in backdrop, focus trapping, Escape-to-close, and `aria-modal` semantics for free. No framework dependency needed, consistent with the project's vanilla-JS approach. Browser support is universal in evergreen browsers.
**Alternatives considered:**
- Custom `<div>` with manual focus trap + backdrop: More code, more accessibility bugs, no benefit.
- Astro island with React/Preact: Adds a build dependency and hydration cost for a single component. Overkill for a modal.
### 2. Destroy iframe on close to stop playback
**Decision:** When the modal closes, remove the iframe `src` (set to `about:blank` or remove the iframe entirely) rather than using the YouTube IFrame API or Spotify SDK to pause.
**Rationale:** Both YouTube and Spotify embeds load third-party scripts that are outside our control. The YouTube IFrame API requires loading `https://www.youtube.com/iframe_api` and managing a `YT.Player` instance. Spotify has no public embed API for programmatic pause. Removing/blanking the iframe src is the simplest, most reliable way to guarantee playback stops — no race conditions, no API loading, works for both platforms identically.
**Alternatives considered:**
- YouTube IFrame API `player.stopVideo()`: Requires async API load, player ready callbacks, and doesn't work for Spotify. Two different code paths for two platforms.
- `postMessage` to iframe: Relies on undocumented/unstable message formats per platform. Fragile.
### 3. Cards become `<button>` triggers instead of `<a>` links
**Decision:** For video/podcast content cards, change the root element from `<a href="..." target="_blank">` to a `<button>` (or a clickable `<div>` with `role="button" tabindex="0"`) that opens the modal. The card visual stays the same.
**Rationale:** The card no longer navigates anywhere — it opens a modal. Semantically, this is a button action, not a link. Using a `<button>` (or button role) is correct for accessibility and avoids the confusion of an `<a>` that doesn't navigate.
**Implementation note:** `StandardCard.astro` currently renders an `<a>`. A new prop (e.g., `as="button"`) or a separate `StandardCardButton.astro` wrapper can handle this. The card must carry the `ContentItem` data as `data-*` attributes so the modal script can read them on click.
**Alternatives considered:**
- Keep as `<a>` with `href="#"` and `preventDefault`: Semantically incorrect, accessibility tools may announce it as a link.
- Wrap card in both `<a>` (for SEO/no-JS fallback) and add JS override: Adds complexity. The detail pages already serve the SEO purpose.
### 4. Data flow via `data-*` attributes on card elements
**Decision:** Cards embed the `ContentItem` data they need (id, source, url, title, summary, publishedAt, thumbnailUrl, views) as `data-*` attributes on the card element. The modal script reads these attributes on click to populate the modal.
**Rationale:** The site is SSG — no runtime data store or state management. `data-*` attributes are the standard Astro pattern for passing server-rendered data to inline scripts (same approach used for nav toggle with `data-nav-toggle`, `data-open`). No serialization overhead, no global state, works with any number of cards.
**Alternatives considered:**
- Global JSON blob in a `<script>` tag: Works but couples the modal to a specific data shape and page. `data-*` is more composable.
- Re-fetch from cache at runtime: The site is static. There's no runtime API to call.
### 5. Embed URL construction
**Decision:**
- **YouTube:** Extract video ID from `item.url` (pattern: `youtube.com/watch?v={id}`) → embed URL: `https://www.youtube.com/embed/{id}?rel=0&modestbranding=1`
- **Spotify/Podcast:** When a Spotify episode ID is available, embed using `https://open.spotify.com/embed/episode/{id}` with Spotify's recommended iframe allowlist and sizing (fixed height ~232px). If a Spotify embed URL is not available, fall back to an in-modal HTML5 `<audio>` player when an `audioUrl` is present, otherwise show an outbound "Listen on Spotify" link.
**Rationale:** YouTube embed URLs have a stable, well-documented format. Spotify embed works for episodes hosted on Spotify. For non-Spotify podcast URLs, embedding isn't universally possible, so the modal gracefully degrades to showing metadata + a link.
In practice, many podcast feeds provide a creator/profile URL (or a podcasters.spotify.com URL) plus an MP3 enclosure, but not the canonical `open.spotify.com/episode/{id}` URL needed for a Spotify embed. The `audioUrl` fallback keeps playback on-site in that case.
### 6. Umami event taxonomy for modal interactions
**Decision:** Introduce a new event name `media_preview` for card clicks that open the modal (replacing `outbound_click`). Add distinct events for modal interactions:
| Interaction | Event Name | Key Properties |
|---|---|---|
| Card click → modal opens | `media_preview` | `target_id`, `placement`, `title`, `type`, `source` |
| Modal close (X / Escape / backdrop) | `media_preview_close` | `target_id`, `close_method` (`button` / `escape` / `backdrop`) |
| "View on YouTube" / "Listen on Spotify" CTA | `cta_click` | `target_id`, `placement=media_modal`, `platform`, `target_url` |
| "Subscribe on YouTube" / "Follow on Spotify" CTA | `cta_click` | `target_id`, `placement=media_modal`, `platform`, `target_url` |
**Rationale:** `outbound_click` is no longer accurate — the user isn't leaving the site. `media_preview` describes what actually happens. Close-method tracking (`close_method` property) allows measuring whether users engage with the content or immediately dismiss. CTA clicks inside the modal reuse the existing `cta_click` event name with `placement=media_modal` for consistency with the CTA tracking taxonomy.
### 7. Modal layout mirrors the card but expanded
**Decision:** The modal content area uses the same information architecture as the card (image/embed → title → summary → footer) but expanded:
1. **Header row:** Title (left) + Close button (right)
2. **Embed area:** YouTube iframe or Spotify embed (16:9 aspect ratio for video, fixed height for podcast)
3. **Body:** Full description/summary (not truncated), date, view count
4. **Footer CTAs:** "Subscribe on YouTube" / "Follow on Spotify" + "View on YouTube" / "Listen on Spotify"
**Rationale:** This mirrors the user's request for "an extended version of the card layout." The familiar structure reduces cognitive load.
## Risks / Trade-offs
- **[Spotify embed availability]** → Not all podcast episodes have Spotify episode URLs. Mitigation: use first-party `<audio>` playback from RSS enclosure (`audioUrl`) when available; otherwise show a "Listen on Spotify" outbound link. Optional: maintain a small override map (`content/podcast-spotify-map.json`) to supply episode embed URLs when desired.
- **[Third-party embed loading time]** → YouTube/Spotify iframes add load time. Mitigation: iframes are only loaded when the modal opens (lazy). A loading placeholder is shown until the iframe loads.
- **[No-JS fallback]** → If JavaScript is disabled, the `<button>` cards won't open a modal. Mitigation: Include a `<noscript>` hint or make the card a link as a no-JS fallback (progressive enhancement). The detail pages (`/videos/[id]`, `/podcast/[id]`) always exist as a non-JS path.
- **[Content Security Policy]** → Embedding YouTube/Spotify requires `frame-src` to allow these domains. Mitigation: verify CSP headers (if any) allow `youtube.com` and `spotify.com` iframe sources. Currently no CSP is set, so no issue.
- **[Modal on mobile]** → Full-screen modal on small viewports may feel heavy. Mitigation: on mobile breakpoints, the modal takes full viewport width with reduced padding, and the embed scales responsively via `aspect-ratio` or percentage-based sizing.

View File

@@ -0,0 +1,37 @@
## Why
Content cards on the homepage, videos page, and podcast page currently link directly to YouTube/Spotify as outbound links (`target="_blank"`). Every click immediately sends the user off-site, inflating bounce rate and cutting short time-on-site. Embedding media in an in-page modal keeps users on the site longer while still providing clear CTAs to the canonical platform when they're ready.
## What Changes
- **Content cards become modal triggers instead of outbound links.** Clicking a video or podcast card on the homepage, `/videos`, or `/podcast` opens a modal dialog with an embedded player — the user stays on-site.
- **New modal dialog component.** An accessible modal overlay that renders:
- Title (top-left) and a close button (top-right)
- Embedded YouTube iframe or Spotify embed player (based on content source)
- Description / summary text
- Date and view count (live count if YouTube API key is configured)
- "Subscribe on YouTube" / "Follow on Spotify" CTA and "View on YouTube" / "Listen on Spotify" CTA
- **Playback lifecycle tied to modal.** Closing the modal (close button or `Escape` key) MUST stop media playback — no audio/video continues after the modal is dismissed.
- **Umami event taxonomy update.** Card clicks on listing pages are no longer outbound — event names change from `outbound_click` to a new interaction event. New events are added for modal open, modal close, follow CTA, view-on-platform CTA, and (stretch) engagement duration.
## Capabilities
### New Capabilities
- `media-modal`: Accessible modal dialog component that embeds YouTube/Spotify players, displays content metadata, provides platform CTAs, and manages playback lifecycle (stop on close/escape).
### Modified Capabilities
- `card-layout-system`: Cards for video/podcast content become modal triggers instead of outbound `<a>` links. The card visual layout stays the same; the interaction target changes.
- `interaction-tracking-taxonomy`: Card clicks are no longer `outbound_click` — a new event category is needed for in-page media previews. New tracked interactions: modal open, modal close, modal CTA clicks (follow, view-on-platform). Stretch: engagement duration tracking.
- `analytics-umami`: New event names and properties must be emitted for modal interactions using the existing `data-umami-event-*` attribute pattern.
- `conversion-ctas`: New CTA instances inside the modal ("Subscribe on YouTube" / "Follow on Spotify", "View on YouTube" / "Listen on Spotify") must follow the existing `CtaLink` tracking conventions.
- `conversion-ctas`: New CTA instances inside the modal ("Subscribe on YouTube" / "Follow on Spotify", "View on YouTube" / "Listen on Spotify") must follow the existing `CtaLink` tracking conventions.
## Impact
- **Components**: `ContentCard.astro`, `StandardCard.astro` — card click behavior changes from outbound link to modal trigger on video/podcast sources (Instagram cards are unaffected).
- **New component**: A `MediaModal` component (likely client-side JS/Astro island) for the dialog, embed player, and playback control.
- **Pages**: `index.astro`, `videos.astro`, `podcast.astro` — must mount the modal and wire card click handlers.
- **Analytics**: All `data-umami-event="outbound_click"` on video/podcast content cards change to a non-outbound event. New events added for modal interactions.
- **Accessibility**: Modal must follow WCAG 2.2 AA patterns already established in `wcag-responsive-ui` spec — focus trap, `Escape` to close, `aria-modal`, focus return on dismiss.
- **Minimal ingestion/data model enhancements.** Podcast content items may include `audioUrl` (RSS enclosure) to support a first-party audio fallback when a Spotify episode embed URL is not available. Optionally, a small override map (`content/podcast-spotify-map.json`) can supply Spotify episode URLs for embedding.
- **No breaking changes.** Detail pages (`/videos/[id]`, `/podcast/[id]`) remain as-is for SEO/sharing. The modal is an additive UX layer on listing surfaces only.

View File

@@ -0,0 +1,51 @@
## MODIFIED Requirements
### Requirement: Custom event tracking
When Umami is enabled, the site MUST support custom event emission for:
- `cta_click`
- `outbound_click`
- `media_preview`
- `media_preview_close`
- 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.
All tracked clickable items MUST emit events with a unique, consistent set of data elements as defined by the site tracking taxonomy, including at minimum `target_id` and `placement`.
The site MUST instrument tracked clickables using Umami's supported Track Events data-attribute method:
- `data-umami-event="<event-name>"`
- optional event data using `data-umami-event-*`
For interactions that are triggered programmatically (e.g., modal close events where the close method must be recorded), the site MAY use Umami's JavaScript API (`umami.track()`) instead of data attributes when data attributes cannot express the required properties.
For content-related links (clickables representing a specific piece of content), the site MUST also provide the following Umami event data attributes:
- `data-umami-event-title`
- `data-umami-event-type`
#### Scenario: Emit outbound click event
- **WHEN** a user clicks a non-CTA outbound link from the homepage
- **THEN** the system emits an `outbound_click` event with a property identifying the destination domain
#### Scenario: Emit general click event for any clickable
- **WHEN** a user clicks an instrumented navigation link
- **THEN** the system emits a click interaction event with `target_id` and `placement`
#### Scenario: Content click includes title and type
- **WHEN** a user clicks an instrumented content link (video, podcast episode, blog post/page)
- **THEN** the emitted Umami event includes `title` and `type` properties via `data-umami-event-*` attributes
#### Scenario: Uninstrumented clicks do not break the page
- **WHEN** a user clicks an element with no tracking metadata
- **THEN** the system does not throw and navigation/interaction proceeds normally
#### Scenario: Media preview event emitted on card click
- **WHEN** a user clicks a video or podcast content card that opens a media modal
- **THEN** the system emits a `media_preview` event with `target_id`, `placement`, `title`, `type`, and `source`
#### Scenario: Media preview close event emitted
- **WHEN** a user closes the media modal
- **THEN** the system emits a `media_preview_close` event with the `target_id` of the content that was previewed and the `close_method` used
#### Scenario: Modal CTA click emitted
- **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`

View File

@@ -0,0 +1,45 @@
## MODIFIED Requirements
### Requirement: Standard card information architecture
All content cards rendered by the site MUST use a standardized layout so cards across different surfaces look consistent.
The standard card layout MUST be:
- featured image displayed prominently at the top (when available)
- title
- summary/excerpt text, trimmed to a fixed maximum length
- footer row showing:
- publish date on the left
- views when available (if omitted, the footer MUST still render cleanly)
- the content source label (e.g., `youtube`, `podcast`, `blog`)
If a field is not available (for example, views for some sources), the card MUST still render cleanly with that field omitted.
For content cards with source `youtube` or `podcast`, the card MUST render as a clickable element that opens a media modal dialog instead of navigating to an external URL. The card MUST NOT render as an outbound `<a>` link for these sources.
For content cards with other sources (e.g., `blog`, `instagram`), the card MUST continue to render as a navigational link (the existing behavior).
The card element for modal-trigger cards MUST carry the content item's data (id, source, url, title, summary, publishedAt, thumbnailUrl, views) as `data-*` attributes so the modal script can access them.
#### Scenario: Card renders with all fields
- **WHEN** a content item has an image, title, summary, publish date, views, and source
- **THEN** the card renders those fields in the standard card layout order
#### Scenario: Card renders without views
- **WHEN** a content item has no views data
- **THEN** the card renders the footer bar with date + source and omits views without breaking the layout
#### Scenario: Card renders without featured image
- **WHEN** a content item has no featured image
- **THEN** the card renders a placeholder media area and still renders the remaining fields
#### Scenario: YouTube video card opens modal
- **WHEN** a user clicks a content card with source `youtube`
- **THEN** a media modal dialog opens with the video's embedded player and metadata instead of navigating to YouTube
#### Scenario: Podcast card opens modal
- **WHEN** a user clicks a content card with source `podcast`
- **THEN** a media modal dialog opens with the episode's embedded player (or metadata link) instead of navigating to the podcast platform
#### Scenario: Blog card still navigates
- **WHEN** a user clicks a content card with source `blog`
- **THEN** the card navigates to the blog post as an internal link (existing behavior, unaffected)

View File

@@ -0,0 +1,48 @@
## ADDED Requirements
### Requirement: Modal CTAs for YouTube and Spotify
The media modal MUST render two CTA actions:
- "Subscribe on YouTube" / "Follow on Spotify" — links to the channel/podcast profile page
- "View on YouTube" / "Listen on Spotify" — links to the specific content item URL
The CTA label and destination MUST be determined by the content source:
- For `youtube` source: "Subscribe on YouTube" links to the YouTube channel URL, "View on YouTube" links to the video URL
- For `podcast` source: "Follow on Spotify" links to the podcast profile URL, "Listen on Spotify" links to the episode URL
Each CTA MUST be rendered using the existing `CtaLink` component conventions (or equivalent markup) with UTM parameters appended.
#### Scenario: YouTube video modal shows YouTube CTAs
- **WHEN** the media modal is displaying a YouTube video
- **THEN** the modal renders "Subscribe on YouTube" (linking to the channel) and "View on YouTube" (linking to the video URL) as CTA actions
#### Scenario: Podcast episode modal shows Spotify CTAs
- **WHEN** the media modal is displaying a podcast episode
- **THEN** the modal renders "Follow on Spotify" (linking to the podcast profile) and "Listen on Spotify" (linking to the episode URL) as CTA actions
### Requirement: Modal CTA tracking
Each CTA rendered inside the media modal MUST emit a `cta_click` event conforming to the existing CTA tracking requirements.
The modal CTAs MUST use:
- `placement=media_modal`
- `target_id` following the `modal.cta.{action}.{platform}` namespace
- `platform` set to `youtube` or `spotify` (mapped from content source)
#### Scenario: Modal CTA emits cta_click event
- **WHEN** a user clicks the "Subscribe on YouTube" CTA inside the media modal
- **THEN** the system emits a `cta_click` event with `target_id=modal.cta.subscribe.youtube`, `placement=media_modal`, `platform=youtube`, and `target_url` set to the YouTube channel URL
#### Scenario: Modal CTA emits cta_click event (secondary)
- **WHEN** a user clicks the "View on YouTube" CTA inside the media modal
- **THEN** the system emits a `cta_click` event with `target_id=modal.cta.view.youtube`, `placement=media_modal`, `platform=youtube`, and `target_url` set to the video URL
#### Scenario: Modal CTA emits cta_click event (podcast)
- **WHEN** a user clicks the "Follow on Spotify" CTA inside the media modal
- **THEN** the system emits a `cta_click` event with `target_id=modal.cta.follow.spotify`, `placement=media_modal`, `platform=spotify`, and `target_url` set to the podcast profile URL
#### Scenario: Modal CTA emits cta_click event (podcast secondary)
- **WHEN** a user clicks the "Listen on Spotify" CTA inside the media modal
- **THEN** the system emits a `cta_click` event with `target_id=modal.cta.listen.spotify`, `placement=media_modal`, `platform=spotify`, and `target_url` set to the episode URL
#### Scenario: Modal CTA includes UTM parameters
- **WHEN** a modal CTA is rendered
- **THEN** the CTA link URL includes UTM parameters for attribution (utm_source, utm_medium, utm_campaign, utm_content)

View File

@@ -0,0 +1,102 @@
## ADDED Requirements
### Requirement: Media preview event for in-page content views
The tracking taxonomy MUST define a `media_preview` event for content card clicks that open an in-page media modal instead of navigating outbound.
The `media_preview` event MUST include the following properties:
- `target_id` (stable unique identifier for the card, following the existing `card.{placement}.{source}.{id}` format)
- `placement` (the listing surface where the card appears, e.g., `home.newest`, `videos.list`, `podcast.list`)
- `title` (human-readable content title, truncated to max 160 characters)
- `type` (`video` or `podcast_episode`)
- `source` (`youtube` or `podcast`)
#### Scenario: Video card click emits media_preview
- **WHEN** a user clicks a YouTube video card on the videos listing page
- **THEN** the system emits a `media_preview` event with `target_id=card.videos.list.youtube.{id}`, `placement=videos.list`, `type=video`, and `source=youtube`
#### Scenario: Podcast card click emits media_preview on homepage
- **WHEN** a user clicks a podcast card in the homepage podcast section
- **THEN** the system emits a `media_preview` event with `target_id=card.home.podcast.podcast.{id}`, `placement=home.podcast`, `type=podcast_episode`, and `source=podcast`
### Requirement: Media preview close event
The tracking taxonomy MUST define a `media_preview_close` event emitted when the media modal is dismissed.
The `media_preview_close` event MUST include:
- `target_id` (the same identifier as the `media_preview` event that opened the modal)
- `close_method` (one of: `button`, `escape`, `backdrop`)
#### Scenario: User closes modal via Escape key
- **WHEN** the user presses Escape to close the media modal that was opened from a video card
- **THEN** the system emits a `media_preview_close` event with the original `target_id` and `close_method=escape`
#### Scenario: User closes modal via close button
- **WHEN** the user clicks the close button on the media modal
- **THEN** the system emits a `media_preview_close` event with the original `target_id` and `close_method=button`
#### Scenario: User closes modal via backdrop click
- **WHEN** the user clicks the backdrop outside the modal content
- **THEN** the system emits a `media_preview_close` event with the original `target_id` and `close_method=backdrop`
### Requirement: Modal CTA namespace
The tracking taxonomy MUST define a `media_modal` placement value for CTA interactions within the media modal.
Modal CTAs MUST use `target_id` values in the namespace `modal.cta.{action}.{platform}`.
The `action` value MUST be one of:
- `subscribe` (YouTube channel)
- `view` (YouTube video)
- `follow` (Spotify podcast profile)
- `listen` (Spotify episode)
#### Scenario: User clicks "View on YouTube" in modal
- **WHEN** the user clicks the "View on YouTube" CTA inside the media modal
- **THEN** the system emits a `cta_click` event with `target_id=modal.cta.view.youtube`, `placement=media_modal`, and `platform=youtube`
#### Scenario: User clicks "Subscribe on YouTube" in modal
- **WHEN** the user clicks the "Subscribe on YouTube" CTA inside the media modal
- **THEN** the system emits a `cta_click` event with `target_id=modal.cta.subscribe.youtube`, `placement=media_modal`, and `platform=youtube`
#### Scenario: User clicks "Follow on Spotify" in modal
- **WHEN** the user clicks the "Follow on Spotify" CTA inside the media modal
- **THEN** the system emits a `cta_click` event with `target_id=modal.cta.follow.spotify`, `placement=media_modal`, and `platform=spotify`
#### Scenario: User clicks "Listen on Spotify" in modal
- **WHEN** the user clicks the "Listen on Spotify" CTA inside the media modal
- **THEN** the system emits a `cta_click` event with `target_id=modal.cta.listen.spotify`, `placement=media_modal`, and `platform=spotify`
## MODIFIED Requirements
### Requirement: Minimum required properties
Every tracked click event MUST include, at minimum:
- `target_id`
- `placement`
For links, the event MUST also include:
- `target_url` (or a stable target identifier that can be mapped to a URL)
For content-related links (clickables representing a specific piece of content), the event MUST also include:
- `title` (human-readable content title)
- `type` (content type identifier)
The `type` value MUST be one of:
- `video`
- `podcast_episode`
- `blog_post`
- `blog_page`
For non-link clickables that trigger in-page actions (e.g., modal openers), the event MUST also include:
- `title` (human-readable content title)
- `type` (content type identifier)
- `source` (content source identifier)
#### Scenario: Tracking a content card click
- **WHEN** a user clicks a content card link
- **THEN** the emitted event includes `target_id`, `placement`, and `target_url`
#### Scenario: Tracking a content link includes title and type
- **WHEN** a user clicks a content-related link that represents a specific content item
- **THEN** the emitted event includes `target_id`, `placement`, `target_url`, `title`, and `type`
#### Scenario: Tracking a modal-trigger card includes title, type, and source
- **WHEN** a user clicks a content card that opens a media modal instead of navigating
- **THEN** the emitted event includes `target_id`, `placement`, `title`, `type`, and `source` (no `target_url` since the user stays on-page)

View File

@@ -0,0 +1,88 @@
## ADDED Requirements
### Requirement: Media modal dialog
The site MUST provide a modal dialog that displays embedded media (YouTube video or podcast episode) when a user clicks a video or podcast content card on a listing surface (homepage, `/videos`, `/podcast`).
The modal MUST render the following elements in order:
- A header row with the content title on the left and a close button on the right
- An embedded media player (YouTube iframe for videos; Spotify embed for podcast episodes when the URL is a Spotify URL; otherwise an in-modal audio player when an `audioUrl` is available)
- The full description/summary text (not truncated)
- The publish date and view count (when available)
- A "Subscribe on YouTube" / "Follow on Spotify" CTA and a "View on YouTube" / "Listen on Spotify" CTA
#### Scenario: User clicks a YouTube video card
- **WHEN** a user clicks a video content card on any listing surface
- **THEN** a modal dialog opens displaying a YouTube iframe embed, the video title, full description, date, view count (if available), and CTAs for "Subscribe on YouTube" and "View on YouTube"
#### Scenario: User clicks a podcast episode card with a Spotify URL
- **WHEN** a user clicks a podcast content card whose URL is a Spotify URL
- **THEN** a modal dialog opens displaying a Spotify episode embed, the episode title, full description, date, and CTAs for "Follow on Spotify" and "Listen on Spotify"
#### Scenario: User clicks a podcast episode card with a non-Spotify URL
- **WHEN** a user clicks a podcast content card whose URL is not a Spotify URL
- **THEN** the modal dialog opens displaying the episode metadata (title, description, date) and either:
- an in-modal audio player when an `audioUrl` is available
- otherwise, a "Listen on Spotify" outbound link
#### Scenario: Modal renders with missing optional fields
- **WHEN** a content item has no view count or no summary
- **THEN** the modal MUST still render cleanly with those fields omitted
### Requirement: Playback stops on modal close
The modal MUST stop all media playback when it is dismissed, regardless of the dismissal method.
The modal MUST support three dismissal methods:
- Close button click
- Pressing the `Escape` key
- Clicking the backdrop outside the modal content
After dismissal, no audio or video from the embedded player MUST continue playing.
#### Scenario: User closes modal via close button
- **WHEN** the modal is open with a playing YouTube video and the user clicks the close button
- **THEN** the modal closes and the video playback stops immediately
#### Scenario: User presses Escape while modal is open
- **WHEN** the modal is open with a playing Spotify episode and the user presses the `Escape` key
- **THEN** the modal closes and the audio playback stops immediately
#### Scenario: User clicks the backdrop
- **WHEN** the modal is open and the user clicks outside the modal content area (the backdrop)
- **THEN** the modal closes and any active media playback stops immediately
### Requirement: Modal accessibility
The modal MUST conform to WCAG 2.2 AA dialog patterns:
- The modal MUST use the native `<dialog>` element opened via `showModal()`
- The modal MUST trap keyboard focus within the dialog while open
- The modal MUST set `aria-modal="true"` and have an accessible label (via `aria-labelledby` referencing the title element)
- Closing the modal MUST return focus to the element that triggered it (the card that was clicked)
- The close button MUST have an accessible label (e.g., `aria-label="Close"`)
#### Scenario: Focus is trapped within the modal
- **WHEN** the modal is open and the user presses `Tab`
- **THEN** focus cycles through the focusable elements within the modal and does not move to elements behind the modal
#### Scenario: Focus returns to trigger on close
- **WHEN** the user closes the modal
- **THEN** focus returns to the card element that originally opened the modal
### Requirement: Responsive modal layout
The modal MUST be responsive across viewports:
- On desktop viewports, the modal MUST be centered with a max-width that leaves visible backdrop on both sides
- On mobile viewports (at or below the site's mobile breakpoint), the modal MUST expand to near-full viewport width with reduced padding
- Embedded media MUST scale proportionally (16:9 aspect ratio for YouTube video, fixed height for Spotify embed)
#### Scenario: Modal on desktop viewport
- **WHEN** the modal is opened on a desktop viewport
- **THEN** the modal is centered horizontally with backdrop visible and the video embed maintains a 16:9 aspect ratio
#### Scenario: Modal on mobile viewport
- **WHEN** the modal is opened on a mobile viewport
- **THEN** the modal expands to near-full viewport width and the video embed scales to fit
### Requirement: Embed loading state
The modal MUST display a loading placeholder while the embedded media iframe is loading.
#### Scenario: Iframe loading
- **WHEN** the modal opens and the iframe has not yet loaded
- **THEN** a placeholder (matching the site's card-placeholder style) is visible in the embed area until the iframe finishes loading

View File

@@ -0,0 +1,52 @@
## 1. Card component changes
- [x] 1.1 Add a `mode` prop (or equivalent) to `StandardCard.astro` to support rendering as a `<button>` (modal trigger) instead of an `<a>` link. When `mode="modal"`, the root element MUST be a `<button>` (or `role="button" tabindex="0"`) with the same visual styling as the current card.
- [x] 1.2 Add `data-*` attributes to modal-trigger cards in `ContentCard.astro` — encode `id`, `source`, `url`, `title`, `summary`, `publishedAt`, `thumbnailUrl`, and `metrics.views` so the modal script can read them on click.
- [x] 1.3 Update `ContentCard.astro` to use `mode="modal"` for `youtube` and `podcast` sources, and keep `mode="link"` (current behavior) for other sources.
- [x] 1.4 Change Umami event from `outbound_click` to `media_preview` on modal-trigger cards. Update `data-umami-event-*` attributes to include `target_id`, `placement`, `title`, `type`, and `source` (drop `domain`, `target_url` since it's no longer outbound).
## 2. Media modal component
- [x] 2.1 Create `MediaModal.astro` component containing a native `<dialog>` element with the modal layout: header (title + close button), embed area, description, date/views row, CTA row.
- [x] 2.2 Add CSS for the modal in `global.css`: backdrop styling, modal container, responsive layout (centered on desktop, near-full-width on mobile), embed aspect-ratio (16:9 for video), loading placeholder.
- [x] 2.3 Write the modal open/populate script: listen for clicks on modal-trigger cards, read `data-*` attributes, populate the modal fields (title, description, date, views), construct the embed URL, set the iframe `src`, and call `dialog.showModal()`.
- [x] 2.4 Implement embed URL construction: extract YouTube video ID from `item.url``https://www.youtube.com/embed/{id}?rel=0&modestbranding=1`. For podcast, embed Spotify when a Spotify episode ID is available (`https://open.spotify.com/embed/episode/{id}`). For non-Spotify podcast URLs, render an in-modal audio player when an `audioUrl` is available, otherwise show a "Listen on Spotify" link.
- [x] 2.5 Implement playback stop on close: on dialog `close` event, set iframe `src` to `about:blank` (or remove the iframe) to guarantee playback stops.
- [x] 2.6 Implement all three close methods: close button click, `Escape` key (native `<dialog>` handles this), and backdrop click (detect clicks on `<dialog>` element outside the inner content container).
## 3. Modal accessibility
- [x] 3.1 Set `aria-modal="true"` and `aria-labelledby` (referencing the title element) on the `<dialog>`.
- [x] 3.2 Add `aria-label="Close"` to the close button.
- [x] 3.3 Implement focus return: store a reference to the clicked card before opening, restore focus to it on close.
- [x] 3.4 Verify focus trapping works with the native `<dialog>` (Tab cycles through modal focusables only).
- [x] 3.5 Verify `prefers-reduced-motion` suppresses modal open/close animations (covered by the existing global CSS rule).
## 4. Modal CTAs
- [x] 4.1 Render "Subscribe on YouTube" / "Follow on Spotify" CTA inside the modal, linking to the channel/podcast profile URL (from `LINKS.youtubeChannel` / `LINKS.podcast`). Apply UTM parameters.
- [x] 4.2 Render "View on YouTube" / "Listen on Spotify" CTA inside the modal, linking to the specific content item URL. Apply UTM parameters.
- [x] 4.3 Add `data-umami-event="cta_click"` with `target_id=modal.cta.{action}.{platform}`, `placement=media_modal`, `platform`, and `target_url` on each modal CTA. Actions: `subscribe`/`view` for YouTube; `follow`/`listen` for Spotify.
## 5. Umami analytics updates
- [x] 5.1 Emit `media_preview_close` event on modal close with `target_id` (from the card that opened it) and `close_method` (`button`, `escape`, or `backdrop`). Use `umami.track()` JS API since `close_method` is determined at runtime.
- [x] 5.2 Verify `media_preview` event fires correctly from card `data-umami-event` attributes on listing surfaces (homepage newest, homepage high-performing, homepage podcast, videos list, podcast list).
- [x] 5.3 Update existing Umami attribute tests (`site/tests/umami-attributes.test.ts`) to expect `media_preview` instead of `outbound_click` for video/podcast cards.
- [x] 5.4 Add new test cases for modal CTA Umami attributes (`cta_click`, `target_id`, `placement=media_modal`).
## 6. Page integration
- [x] 6.1 Add the `MediaModal` component to `index.astro` (homepage).
- [x] 6.2 Add the `MediaModal` component to `videos.astro`.
- [x] 6.3 Add the `MediaModal` component to `podcast.astro`.
## 7. Verification
- [x] 7.1 Run `npm run build` in `site/` and verify no build errors.
- [x] 7.2 Run `npm test` in `site/` and verify all tests pass (including updated Umami attribute tests).
Note: `site/tests/blog-nav.test.ts` is failing in the repo for reasons unrelated to this change.
- [x] 7.3 Manual smoke test: click a video card → modal opens with YouTube embed → close → playback stops. Repeat with Escape and backdrop click.
- [x] 7.4 Manual smoke test: click a podcast card → modal opens with Spotify embed (or fallback link) → close → playback stops.
- [x] 7.5 Verify modal accessibility: keyboard-only navigation, focus trap, focus return, screen reader announces dialog.
- [x] 7.6 Verify responsive behavior: modal layout on desktop and mobile viewports.

View File

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

View File

@@ -17,6 +17,6 @@
## 4. Verification
- [ ] 4.1 Verify service worker registers in production build and does not register in dev
- [ ] 4.2 Verify repeat navigation and asset loads hit cache (Chrome DevTools Application tab)
- [ ] 4.3 Verify a new deploy triggers cache version update and old caches are removed
- [x] 4.1 Verify service worker registers in production build and does not register in dev
- [x] 4.2 Verify repeat navigation and asset loads hit cache (Chrome DevTools Application tab)
- [x] 4.3 Verify a new deploy triggers cache version update and old caches are removed

View File

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

View File

@@ -0,0 +1,106 @@
## Context
The site currently uses a single dark theme defined via CSS variables in `site/public/styles/global.css` (e.g., `--bg0`, `--bg1`, `--fg`, `--muted`, `--stroke`, `--accent`). There is no existing theme selection mechanism (no `data-theme` attribute, no persisted preference).
The site shell uses a sticky `.site-header` and a per-page `.subnav` row (when present). A theme switcher notch must be fixed-positioned such that it does not overlap the header and leaves enough space for the subnav region.
## Goals / Non-Goals
**Goals:**
- Provide three themes: `dark`, `light`, `high-contrast`.
- Allow switching themes via a floating notch on the right side of the screen positioned below the primary nav bar and not overlapping the subnav area.
- Make switching feel premium:
- hover animation on the notch
- smooth theme transitions (without an abrupt flash)
- Ensure accessibility:
- keyboard operable
- visible focus
- respects `prefers-reduced-motion`
- high contrast theme is meaningfully higher contrast, not just a color swap
- Persist the user's selection across page loads.
**Non-Goals:**
- Rebuild the entire visual system or rewrite all CSS to a design-token framework.
- PWA theming (manifest/theme-color) beyond what is required to implement UI themes.
- Adding user accounts or server-side persistence of theme preference.
## Decisions
### 1. Theme selection mechanism: `data-theme` on `<html>`
Use `document.documentElement.dataset.theme = "dark" | "light" | "high-contrast"`.
Rationale:
- Works cleanly with CSS variables.
- Scopes theme styles across the entire page without specificity fights.
Alternatives considered:
- Adding theme classes to `body` (works, but html-scoped variables are simpler for form control theming).
### 2. Token strategy: override existing CSS variables per theme
Keep the existing variable names and provide theme-specific overrides:
- `:root` remains the default (dark)
- `html[data-theme="light"] { ... }`
- `html[data-theme="high-contrast"] { ... }`
Rationale:
- Minimizes churn in existing CSS.
- Enables incremental migration of any remaining hard-coded colors to tokens.
### 3. Default theme resolution order
On first load, resolve the active theme in this order:
1) stored user preference (`localStorage.theme`)
2) forced colors / high-contrast OS mode (if detected) -> `high-contrast`
3) system color scheme -> `light` if `prefers-color-scheme: light`, else `dark`
Rationale:
- User choice wins.
- If the user is in a forced/high-contrast environment, defaulting to high-contrast aligns with accessibility intent.
### 4. Prevent flash of wrong theme with a tiny head script
Insert a small inline script in the document `<head>` that sets `data-theme` before first paint.
Rationale:
- Avoids "flash" where the page renders in dark before switching to light/high-contrast.
Trade-off:
- Inline scripts can constrain future CSP hardening; keep script small and self-contained.
### 5. Smooth transitions without animating on every page load
Use a transient attribute/class (e.g., `data-theme-transition="on"`) only during user-initiated theme changes.
Implementation shape:
- When switching: set `data-theme-transition="on"`, update `data-theme`, then remove after ~250ms.
- CSS applies transitions for color/background/border/shadow only when the attribute is present.
Rationale:
- Avoids "everything animates" feeling during initial load.
- Avoids subtle jank on navigation.
### 6. Notch UI: fixed-position, expands on hover/focus
Implement the switcher as a fixed-position control at the right edge:
- Default collapsed: a small vertical tab.
- On `:hover` and `:focus-within`: expands into a small panel exposing the three theme options.
Accessibility decisions:
- Use a real `<button>` to open/close (for touch) OR a `<fieldset role="radiogroup">` with three radio-like buttons.
- Ensure it is reachable via keyboard and has clear `aria-label`s.
Placement decisions:
- Use a CSS variable `--theme-notch-top` to position it.
- A small inline script computes this based on `.site-header` height and, if a `.subnav` exists near the top, positions below it.
### 7. High Contrast theme semantics
The high-contrast theme will be a dedicated palette (not only increased brightness) with:
- strong background/foreground contrast
- high visibility focus ring
- more assertive stroke borders
Additionally, handle OS forced-colors mode:
- In `@media (forced-colors: active)`, prefer system colors and avoid gradients that reduce clarity.
## Risks / Trade-offs
- **[Notch overlaps content]** -> compute top offset from header/subnav; provide safe-area padding; add responsive rules for small viewports.
- **[Theme transitions reduce readability]** -> scope transitions to a short window and limit properties; disable via `prefers-reduced-motion`.
- **[High contrast breaks brand feel]** -> keep layout/typography unchanged and only adjust palette and borders.
- **[CSP constraints]** -> keep head script minimal and consider moving to an external script if CSP hardening becomes a priority.

View File

@@ -0,0 +1,27 @@
## Why
Add modern theming controls (dark/light/high-contrast) to improve accessibility and give the site a polished, customizable "WOW" experience.
## What Changes
- Add three user-selectable themes: **Dark**, **Light**, and **High Contrast**.
- Add a floating theme switcher "notch" on the right edge of the screen:
- positioned just below the primary nav bar
- leaves enough vertical space for secondary navigation
- hover state includes a tasteful animation
- Theme switching uses a smooth transition (not an abrupt flash).
## Capabilities
### New Capabilities
- `site-theming`: Theme tokens and a theme application mechanism that can switch between Dark/Light/High Contrast across the site.
- `theme-switcher-notch`: A floating, accessible UI control (right-side notch) that lets the user switch themes.
### Modified Capabilities
- `wcag-responsive-ui`: Extend accessibility baseline to include theme switching requirements (keyboard, focus, reduced motion) and ensure High Contrast theme is supported.
## Impact
- Affected UI/CSS: global design tokens (CSS variables), background layers, card/CTA styling, focus styling.
- Affected layout: a new floating notch component that must not overlap navigation across breakpoints.
- Affected UX/accessibility: keyboard navigation and motion preferences during theme transitions.

View File

@@ -0,0 +1,55 @@
## ADDED Requirements
### Requirement: Site themes
The site MUST support three themes:
- `dark`
- `light`
- `high-contrast`
Themes MUST be applied by setting a `data-theme` attribute on the root document element (`<html>`).
#### Scenario: Dark theme active
- **WHEN** `data-theme="dark"` is set on `<html>`
- **THEN** the site's background, text, and component styling reflect the dark palette
#### Scenario: Light theme active
- **WHEN** `data-theme="light"` is set on `<html>`
- **THEN** the site's background, text, and component styling reflect the light palette
#### Scenario: High contrast theme active
- **WHEN** `data-theme="high-contrast"` is set on `<html>`
- **THEN** the site uses a high-contrast palette with a clearly visible focus ring and high-contrast borders
### Requirement: Theme persistence
The site MUST persist the user's theme selection so it is retained across page loads and navigations.
Persistence MUST be stored locally in the browser (e.g., localStorage).
#### Scenario: Theme persists across reload
- **WHEN** the user selects `light` theme and reloads the page
- **THEN** the `light` theme remains active
### Requirement: Default theme selection
If the user has not explicitly selected a theme, the site MUST choose a default theme using environment signals.
Default selection order:
1) If forced colors / high-contrast mode is active, default to `high-contrast`
2) Else if the system prefers light color scheme, default to `light`
3) Else default to `dark`
#### Scenario: No stored preference uses system settings
- **WHEN** the user has no stored theme preference
- **THEN** the site selects a default theme based on forced-colors and prefers-color-scheme
### Requirement: Theme switching transition
Theme changes initiated by the user MUST transition smoothly.
The transition MUST be disabled or substantially reduced when `prefers-reduced-motion: reduce` is set.
#### Scenario: Smooth transition on switch
- **WHEN** the user switches from `dark` to `light` theme
- **THEN** theme-affecting properties transition smoothly instead of abruptly switching
#### Scenario: Reduced motion disables theme animation
- **WHEN** `prefers-reduced-motion: reduce` is set and the user switches theme
- **THEN** the theme change occurs without noticeable animation

View File

@@ -0,0 +1,42 @@
## ADDED Requirements
### Requirement: Floating theme switcher notch
The site MUST provide a floating theme switcher control anchored to the right side of the viewport.
The control MUST be positioned below the primary navigation bar and MUST leave sufficient vertical space for secondary navigation.
#### Scenario: Notch positioned below header
- **WHEN** the page loads
- **THEN** the theme switcher notch is visible on the right side and does not overlap the sticky header or sub-navigation
### Requirement: Notch interaction and animation
The notch MUST provide a hover affordance (a small, tasteful animation) that indicates it is interactive.
The hover animation MUST be disabled or substantially reduced under `prefers-reduced-motion: reduce`.
#### Scenario: Hover animation present
- **WHEN** a pointer user hovers the notch
- **THEN** the notch animates in a way that suggests it can be expanded or interacted with
#### Scenario: Reduced motion disables hover animation
- **WHEN** `prefers-reduced-motion: reduce` is set
- **THEN** hovering the notch does not trigger a noticeable animation
### Requirement: Theme selection UI
The notch MUST expose the three theme options (`dark`, `light`, `high-contrast`) and allow the user to select one.
The control MUST be keyboard accessible:
- it MUST be reachable via `Tab`
- it MUST have a visible focus indicator
- selection MUST be possible using keyboard input
#### Scenario: Keyboard selects theme
- **WHEN** a keyboard user focuses the notch and selects `high-contrast`
- **THEN** the site updates to the `high-contrast` theme and the selection is persisted
### Requirement: Accessibility labels
The notch and theme options MUST have accessible labels.
#### Scenario: Screen reader announces theme switcher
- **WHEN** a screen reader user focuses the theme switcher control
- **THEN** it announces an appropriate label (e.g., "Theme" or "Theme switcher") and the currently selected theme

View File

@@ -0,0 +1,30 @@
## ADDED Requirements
### Requirement: Theme switching accessibility
Theme switching controls MUST be accessible and usable with keyboard and assistive technology.
The theme switcher control MUST:
- be reachable via keyboard navigation
- provide a visible focus indication
- expose an accessible name/label
- allow selecting any supported theme without requiring a pointer
#### Scenario: Theme switcher is keyboard reachable
- **WHEN** a keyboard user tabs through the page
- **THEN** the theme switcher notch receives focus and shows a visible focus indicator
#### Scenario: Theme switcher is labeled
- **WHEN** a screen reader user focuses the theme switcher
- **THEN** it announces a meaningful label and the current theme state
### Requirement: High contrast theme meets WCAG intent
The `high-contrast` theme MUST provide materially higher contrast than the default theme.
The theme MUST keep text readable and interactive affordances obvious, including:
- strong foreground/background contrast
- clearly visible focus ring
- strong borders on interactive elements
#### Scenario: High contrast theme improves readability
- **WHEN** the user enables `high-contrast` theme
- **THEN** primary text and secondary UI labels remain clearly readable and interactive elements are visually distinct

View File

@@ -0,0 +1,26 @@
## 1. Theme Tokens And Application
- [x] 1.1 Add `data-theme` overrides in `site/public/styles/global.css` for `light` and `high-contrast` (keep `:root` as default dark)
- [x] 1.2 Add `color-scheme` rules per theme so native form controls match (dark/light)
- [x] 1.3 Add theme initialization script in `site/src/layouts/BaseLayout.astro` to set theme before first paint (stored preference → forced colors/high contrast → prefers-color-scheme)
- [x] 1.4 Persist theme selection to localStorage and update `data-theme` on change
- [x] 1.5 Implement scoped smooth transitions for user-initiated theme changes (no global transition on initial load)
## 2. Theme Switcher Notch UI
- [x] 2.1 Add markup for a fixed-position right-side notch in `site/src/layouts/BaseLayout.astro`
- [x] 2.2 Implement notch positioning below `.site-header` and below `.subnav` when present (compute top offset; handle resize)
- [x] 2.3 Add notch hover animation (expand/slide) and ensure it feels intentional
- [x] 2.4 Add keyboard and screen reader support (label, focus styles, keyboard selection)
- [x] 2.5 Ensure notch does not overlap critical content on mobile (responsive rules; safe-area)
## 3. High Contrast Theme Verification
- [x] 3.1 Ensure high-contrast theme has strong fg/bg contrast, obvious focus ring, and strong strokes on interactive elements
- [x] 3.2 Add `@media (forced-colors: active)` adjustments to avoid illegible gradients and ensure system colors are respected
## 4. Verification
- [x] 4.1 Run `npm run build` and verify output HTML includes the head theme-init script
- [x] 4.2 Manual smoke test: switch themes on `/`, `/videos`, `/podcast`, `/blog` and verify persistence across reload
- [x] 4.3 Manual a11y checks: keyboard-only interaction, focus visibility, prefers-reduced-motion behavior

View File

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

View File

@@ -0,0 +1,82 @@
## Context
The primary header navigation is currently laid out as a simple left brand + right nav flex row. This makes the header feel left-heavy and de-emphasizes the brand. The site already uses a cohesive visual system (tokenized colors, glow accents, motion preferences) and has existing accessibility constraints (focus-visible, reduced motion).
Relevant implementation notes:
- Header markup is in `site/src/layouts/BaseLayout.astro`.
- Styling is in `site/src/styles/global.css`.
- Public feature flags are expressed via `PUBLIC_*` env vars and commonly use an opt-out string comparison (`!== "false"`) similar to `PUBLIC_ENABLE_SW`.
This change is purely UI-level: layout and hover presentation. No routing, data, or backend behavior changes are required.
## Goals / Non-Goals
**Goals:**
- Add a left-side logo in the header using the same asset as the favicon, sized appropriately and aligned with the header rhythm.
- Center the site brand label ("SanthoshJ") in the primary header layout without breaking the mobile hamburger behavior.
- Introduce a novel animated hover-line treatment that appears only on hover:
- primary header title links (brand + Videos, Podcast, Blog)
- section/module titles for Videos, Podcast, Blog (titles only)
- Gate the hover-line treatment behind a public env flag so it can be turned off to preserve the current behavior.
- Preserve accessibility:
- no layout shift due to hover effects
- keyboard focus remains visible and not confused with hover-only affordances
- reduced motion mode substantially reduces/disables the animation
**Non-Goals:**
- Redesigning the overall header/nav visual language beyond layout + hover line.
- Changing navigation destinations, labels, or analytics tracking attributes.
- Introducing new font families or major typography changes.
- Adding new build dependencies.
## Decisions
### 1) Header layout strategy: CSS Grid for centering
Decision: implement the header container as a 3-column grid (`left / center / right`) and place:
- left: logo link
- center: brand text link
- right: nav links (desktop) and hamburger toggle (mobile)
Rationale: grid makes true center alignment robust even when left/right columns have different widths, and avoids brittle absolute positioning.
Alternative: flex with spacer elements. Rejected due to center drift as content changes.
### 2) Logo asset and sizing
Decision: reuse `site/public/favicon.png` as the logo source initially, styled to a fixed square size with pixel-snapped rendering.
Rationale: avoids introducing new assets. If needed later, replace with a dedicated logo asset (same path/size contract).
### 3) Hover-line implementation: pseudo-element with no layout impact
Decision: implement the hover line using `::after` (or `background-size`) so it does not affect layout metrics.
- visible only on `:hover`
- reduced motion: animation disabled/reduced
- focus-visible: keep existing focus ring; optionally show a static line for focus (not animated)
Rationale: avoids CLS and keeps the effect decorative.
### 4) Feature flag: public opt-out env var
Decision: add `PUBLIC_ENABLE_NAV_HOVER_LINE` (string) and treat `"false"` as disabled; all other values (including undefined) keep current behavior.
Rationale: matches existing public env patterns and ensures default behavior remains unchanged.
### 5) Scope of "titles"
Decision: apply the hover-line to primary header title links (center brand + Videos/Podcast/Blog nav) and to explicit title elements for the three primary content surfaces (Videos, Podcast, Blog) where they are presented as headings; avoid applying to card titles and other headings.
Rationale: keeps the effect intentional and avoids visual noise.
## Risks / Trade-offs
- [Mobile header regressions] Grid changes could break the hamburger layout → Mitigation: keep mobile nav toggle and panel behavior unchanged; add targeted CSS at the existing breakpoint.
- [Accessibility confusion] Hover-only affordance might be mistaken for focus indicator → Mitigation: do not remove focus-visible styles; keep hover line hover-only and optionally static on focus.
- [Theme contrast] Line color may be low-contrast in some themes → Mitigation: use existing accent tokens and test across dark/light/high-contrast.
- [Over-application] Accidentally applying effect to card titles → Mitigation: apply only to selectors scoped to nav links + specific page/module title classes.

View File

@@ -0,0 +1,33 @@
## Why
The site header can feel visually left-weighted. A tighter, more intentional navbar layout and hover treatment will improve polish and brand presentation.
## What Changes
- Move the site branding text "SanthoshJ" to the center of the navbar (desktop and mobile-safe).
- Add a left-side logo using the existing favicon asset, sized appropriately for the header.
- Add a novel/creative animated hover line treatment that appears only on hover:
- primary header title links: brand + Videos, Podcast, Blog
- section/module titles for Videos, Podcast, Blog (titles only)
- Add a public env flag to enable/disable the hover line effect:
- default behavior remains unchanged unless explicitly enabled/disabled
- when disabled, revert to current hover behavior
- Ensure all the above changes are WCAG compliant.
## Capabilities
### New Capabilities
- `navbar-branding`: Add a left logo and centered brand layout in the primary header.
- `nav-hover-line`: Provide an animated hover-line treatment for primary header title links and key section titles, gated by a public env flag.
### Modified Capabilities
- (none)
## Impact
- `site/src/layouts/BaseLayout.astro` (header markup: logo placement, centered brand, nav link structure)
- `site/src/styles/global.css` (navbar layout + hover line animation styles)
- `site/public/favicon.*` (reuse as navbar logo asset)
- `site/src/env.d.ts` + `site/src/lib/config.ts` (new `PUBLIC_*` env flag wiring)

View File

@@ -0,0 +1,47 @@
## ADDED Requirements
### Requirement: Hover line appears only on hover for primary header titles
The site MUST render a decorative animated line that appears only on hover for the primary header title links:
- SanthoshJ (center brand link)
- Videos
- Podcast
- Blog
The hover line MUST NOT cause layout shift.
#### Scenario: Hover line is hidden by default
- **WHEN** the header renders
- **THEN** the hover line is not visible on primary header title links until hover
#### Scenario: Hover line appears on hover
- **WHEN** a pointer user hovers a primary header title link
- **THEN** an animated line appears as a hover affordance for that title
### Requirement: Hover line applies to section/module titles for key surfaces
The site MUST apply the same hover-line treatment to the titles for the Videos, Podcast, and Blog surfaces, but only on titles (not on card titles).
#### Scenario: Titles use hover line
- **WHEN** a pointer user hovers the Videos/Podcast/Blog surface title element
- **THEN** the hover line appears only for that title
#### Scenario: Card titles are unaffected
- **WHEN** a pointer user hovers a content card title
- **THEN** no hover line effect is applied (existing behavior remains)
### Requirement: Hover line is gated by a public env flag
The hover-line effect MUST be controllable via a public environment variable `PUBLIC_ENABLE_NAV_HOVER_LINE`.
If `PUBLIC_ENABLE_NAV_HOVER_LINE` is set to the string `"false"`, the hover-line effect MUST be disabled and the current behavior MUST continue.
#### Scenario: Flag disables hover line
- **WHEN** `PUBLIC_ENABLE_NAV_HOVER_LINE` is set to `"false"`
- **THEN** hovering primary header title links and surface titles does not render the hover line effect
### Requirement: Reduced motion disables or substantially reduces animation
If `prefers-reduced-motion: reduce` is set, the hover-line animation MUST be disabled or substantially reduced.
#### Scenario: Reduced motion disables noticeable hover animation
- **WHEN** `prefers-reduced-motion: reduce` is set and a user hovers a nav title
- **THEN** the hover line does not animate noticeably

View File

@@ -0,0 +1,25 @@
## ADDED Requirements
### Requirement: Primary header shows logo and centered brand
The site MUST render a primary header that includes:
- a left-side logo that links to `/`
- a centered brand label "SanthoshJ" that links to `/`
- the primary navigation links (Videos, Podcast, Blog)
The logo MUST use the same visual asset as the site favicon.
#### Scenario: Desktop header layout
- **WHEN** a user loads the home page on a desktop viewport
- **THEN** the header shows the logo on the left, the brand label centered, and the nav links on the right
#### Scenario: Mobile header layout
- **WHEN** a user loads the home page on a mobile viewport
- **THEN** the header still shows the logo on the left and the brand label centered without overlapping the nav toggle
### Requirement: Logo size is visually aligned
The header logo MUST be sized to match the header rhythm and MUST not cause layout shift.
#### Scenario: Logo has fixed dimensions
- **WHEN** the header renders
- **THEN** the logo element has explicit width and height and does not change size on load

View File

@@ -0,0 +1,20 @@
## 1. Header Branding Layout
- [x] 1.1 Update `site/src/layouts/BaseLayout.astro` header markup to add a left logo link (favicon asset) and move "SanthoshJ" brand link to the center
- [x] 1.2 Update `site/src/styles/global.css` header layout to center brand text robustly (grid-based layout) while preserving mobile hamburger behavior
- [x] 1.3 Ensure logo has correct sizing and explicit dimensions (no layout shift)
## 2. Hover Line Effect (Gated)
- [x] 2.1 Add `PUBLIC_ENABLE_NAV_HOVER_LINE` to `site/src/env.d.ts`
- [x] 2.2 Add config wiring in `site/src/lib/config.ts` (or direct `import.meta.env` usage) with opt-out semantics (`"false"` disables)
- [x] 2.3 Implement hover-line CSS for primary header title links (brand + Videos/Podcast/Blog), hover-only, no layout shift
- [x] 2.4 Implement hover-line CSS for the Videos/Podcast/Blog surface title elements only (not card titles)
- [x] 2.5 Add reduced-motion handling so the effect is disabled/substantially reduced under `prefers-reduced-motion: reduce`
- [x] 2.6 Ensure focus-visible styles remain clear and are not replaced by hover-only affordances
## 3. Verification
- [x] 3.1 Verify layout on desktop + mobile widths (no overlap with nav toggle)
- [x] 3.2 Verify hover-line is enabled by default and fully disabled when `PUBLIC_ENABLE_NAV_HOVER_LINE="false"`
- [x] 3.3 Run `npm -C site run build` and ensure no new typecheck regressions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
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

@@ -17,6 +17,9 @@ When Umami is disabled or not configured, the site MUST still function and MUST
When Umami is enabled, the site MUST support custom event emission for:
- `cta_click`
- `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.
@@ -27,6 +30,8 @@ The site MUST instrument tracked clickables using Umami's supported Track Events
- `data-umami-event="<event-name>"`
- optional event data using `data-umami-event-*`
For interactions that are triggered programmatically (e.g., modal close events where the close method must be recorded), the site MAY use Umami's JavaScript API (`umami.track()`) instead of data attributes when data attributes cannot express the required properties.
For content-related links (clickables representing a specific piece of content), the site MUST also provide the following Umami event data attributes:
- `data-umami-event-title`
- `data-umami-event-type`
@@ -47,6 +52,38 @@ For content-related links (clickables representing a specific piece of content),
- **WHEN** a user clicks an element with no tracking metadata
- **THEN** the system does not throw and navigation/interaction proceeds normally
#### Scenario: Media preview event emitted on card click
- **WHEN** a user clicks a video or podcast content card that opens a media modal
- **THEN** the system emits a `media_preview` event with `target_id`, `placement`, `title`, `type`, and `source`
#### Scenario: Media preview close event emitted
- **WHEN** a user closes the media modal
- **THEN** the system emits a `media_preview_close` event with the `target_id` of the content that was previewed and the `close_method` used
#### Scenario: Modal CTA click emitted
- **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.

View File

@@ -8,7 +8,7 @@ Define a standardized card layout so content cards across surfaces look consiste
All content cards rendered by the site MUST use a standardized layout so cards across different surfaces look consistent.
The standard card layout MUST be:
- featured image displayed prominently at the top (when available)
- featured image displayed prominently at the top (when available), with a shimmer placeholder visible while the image loads
- title
- summary/excerpt text, trimmed to a fixed maximum length
- footer row showing:
@@ -18,6 +18,12 @@ The standard card layout MUST be:
If a field is not available (for example, views for some sources), the card MUST still render cleanly with that field omitted.
For content cards with source `youtube` or `podcast`, the card MUST render as a clickable element that opens a media modal dialog instead of navigating to an external URL. The card MUST NOT render as an outbound `<a>` link for these sources.
For content cards with other sources (e.g., `blog`, `instagram`), the card MUST continue to render as a navigational link (the existing behavior).
The card element for modal-trigger cards MUST carry the content item's data (id, source, url, title, summary, publishedAt, thumbnailUrl, views) as `data-*` attributes so the modal script can access them.
#### Scenario: Card renders with all fields
- **WHEN** a content item has an image, title, summary, publish date, views, and source
- **THEN** the card renders those fields in the standard card layout order
@@ -30,3 +36,22 @@ If a field is not available (for example, views for some sources), the card MUST
- **WHEN** a content item has no featured image
- **THEN** the card renders a placeholder media area and still renders the remaining fields
#### Scenario: Card image shows shimmer while loading
- **WHEN** a content item has an image URL and the image has not yet loaded
- **THEN** the card media area displays an animated shimmer placeholder until the image loads and fades in
#### Scenario: Card image load failure shows static placeholder
- **WHEN** a content item has an image URL but the image fails to load
- **THEN** the card media area displays a static placeholder (no broken image icon) and the card remains visually intact
#### Scenario: YouTube video card opens modal
- **WHEN** a user clicks a content card with source `youtube`
- **THEN** a media modal dialog opens with the video's embedded player and metadata instead of navigating to YouTube
#### Scenario: Podcast card opens modal
- **WHEN** a user clicks a content card with source `podcast`
- **THEN** a media modal dialog opens with the episode's embedded player (or metadata link) instead of navigating to the podcast platform
#### Scenario: Blog card still navigates
- **WHEN** a user clicks a content card with source `blog`
- **THEN** the card navigates to the blog post as an internal link (existing behavior, unaffected)

View File

@@ -38,3 +38,50 @@ In addition, CTA clicks MUST conform to the site click tracking taxonomy and MUS
#### Scenario: Two CTAs to the same destination
- **WHEN** two CTAs link to the same destination but appear in different placements
- **THEN** their emitted events contain different `target_id` values
### Requirement: Modal CTAs for YouTube and Spotify
The media modal MUST render two CTA actions:
- "Subscribe on YouTube" / "Follow on Spotify" — links to the channel/podcast profile page
- "View on YouTube" / "Listen on Spotify" — links to the specific content item URL
The CTA label and destination MUST be determined by the content source:
- For `youtube` source: "Subscribe on YouTube" links to the YouTube channel URL, "View on YouTube" links to the video URL
- For `podcast` source: "Follow on Spotify" links to the podcast profile URL, "Listen on Spotify" links to the episode URL
Each CTA MUST be rendered using the existing `CtaLink` component conventions (or equivalent markup) with UTM parameters appended.
#### Scenario: YouTube video modal shows YouTube CTAs
- **WHEN** the media modal is displaying a YouTube video
- **THEN** the modal renders "Subscribe on YouTube" (linking to the channel) and "View on YouTube" (linking to the video URL) as CTA actions
#### Scenario: Podcast episode modal shows Spotify CTAs
- **WHEN** the media modal is displaying a podcast episode
- **THEN** the modal renders "Follow on Spotify" (linking to the podcast profile) and "Listen on Spotify" (linking to the episode URL) as CTA actions
### Requirement: Modal CTA tracking
Each CTA rendered inside the media modal MUST emit a `cta_click` event conforming to the existing CTA tracking requirements.
The modal CTAs MUST use:
- `placement=media_modal`
- `target_id` following the `modal.cta.{action}.{platform}` namespace
- `platform` set to `youtube` or `spotify` (mapped from content source)
#### Scenario: Modal CTA emits cta_click event
- **WHEN** a user clicks the "Subscribe on YouTube" CTA inside the media modal
- **THEN** the system emits a `cta_click` event with `target_id=modal.cta.subscribe.youtube`, `placement=media_modal`, `platform=youtube`, and `target_url` set to the YouTube channel URL
#### Scenario: Modal CTA emits cta_click event (secondary)
- **WHEN** a user clicks the "View on YouTube" CTA inside the media modal
- **THEN** the system emits a `cta_click` event with `target_id=modal.cta.view.youtube`, `placement=media_modal`, `platform=youtube`, and `target_url` set to the video URL
#### Scenario: Modal CTA emits cta_click event (podcast)
- **WHEN** a user clicks the "Follow on Spotify" CTA inside the media modal
- **THEN** the system emits a `cta_click` event with `target_id=modal.cta.follow.spotify`, `placement=media_modal`, `platform=spotify`, and `target_url` set to the podcast profile URL
#### Scenario: Modal CTA emits cta_click event (podcast secondary)
- **WHEN** a user clicks the "Listen on Spotify" CTA inside the media modal
- **THEN** the system emits a `cta_click` event with `target_id=modal.cta.listen.spotify`, `placement=media_modal`, `platform=spotify`, and `target_url` set to the episode URL
#### Scenario: Modal CTA includes UTM parameters
- **WHEN** a modal CTA is rendered
- **THEN** the CTA link URL includes UTM parameters for attribution (utm_source, utm_medium, utm_campaign, utm_content)

View File

@@ -0,0 +1,54 @@
## Purpose
Define a consistent lazy-loading experience for images by showing a shimmer placeholder while images download, fading in images on load, and degrading gracefully on failures.
## Requirements
### Requirement: Shimmer placeholder while images load
Every site image that uses `loading="lazy"` MUST display an animated shimmer placeholder in its container while the image is downloading.
The shimmer MUST be a translucent gradient sweep animation that matches the site's dark theme.
The shimmer MUST be visible from the moment the page renders until the image finishes loading.
#### Scenario: Image loads successfully on slow connection
- **WHEN** a page renders with a lazy-loaded image and the image takes time to download
- **THEN** the image container displays an animated shimmer placeholder until the image finishes loading
#### Scenario: Image loads from browser cache
- **WHEN** a page renders with a lazy-loaded image that is already in the browser cache
- **THEN** the image displays immediately with no visible shimmer flicker
### Requirement: Fade-in transition on image load
When a lazy-loaded image finishes downloading, it MUST fade in smoothly over the shimmer placeholder using a CSS opacity transition.
The fade-in duration MUST be short enough to feel responsive (no longer than 300ms).
#### Scenario: Image completes loading
- **WHEN** a lazy-loaded image finishes downloading
- **THEN** the image fades in over approximately 200-300ms, replacing the shimmer placeholder
### Requirement: Graceful degradation on image load failure
If a lazy-loaded image fails to load (network error, 404, etc.), the shimmer animation MUST stop and the placeholder MUST remain visible as a static block.
The page MUST NOT display a broken image icon.
#### Scenario: Image fails to load
- **WHEN** a lazy-loaded image triggers an error event (e.g., 404 or network failure)
- **THEN** the shimmer animation stops and the container displays a static placeholder background instead of a broken image icon
### Requirement: Reduced motion support for shimmer
The shimmer animation MUST be suppressed when the user has `prefers-reduced-motion: reduce` enabled.
When motion is reduced, the placeholder MUST still be visible as a static block (no animation), maintaining the loading indicator without motion.
#### Scenario: User has reduced motion enabled
- **WHEN** a user with `prefers-reduced-motion: reduce` views a page with lazy-loaded images
- **THEN** the placeholder is visible as a static block without any sweeping animation
### Requirement: No layout shift from shimmer
The shimmer placeholder MUST NOT cause any cumulative layout shift (CLS). The placeholder MUST occupy the exact same dimensions as the image it replaces.
#### Scenario: Placeholder matches image dimensions
- **WHEN** a page renders with a shimmer placeholder for a card thumbnail
- **THEN** the placeholder occupies the same width and height as the image area (e.g., 100% width x 180px height for card thumbnails) with no layout shift when the image loads

View File

@@ -28,6 +28,70 @@ The taxonomy MUST define namespaces for repeated UI surfaces. For the blog surfa
- **WHEN** two links point to the same destination but appear in different placements
- **THEN** their `target_id` values are different so their clicks can be measured independently
### Requirement: Media preview event for in-page content views
The tracking taxonomy MUST define a `media_preview` event for content card clicks that open an in-page media modal instead of navigating outbound.
The `media_preview` event MUST include the following properties:
- `target_id` (stable unique identifier for the card, following the existing `card.{placement}.{source}.{id}` format)
- `placement` (the listing surface where the card appears, e.g., `home.newest`, `videos.list`, `podcast.list`)
- `title` (human-readable content title, truncated to max 160 characters)
- `type` (`video` or `podcast_episode`)
- `source` (`youtube` or `podcast`)
#### Scenario: Video card click emits media_preview
- **WHEN** a user clicks a YouTube video card on the videos listing page
- **THEN** the system emits a `media_preview` event with `target_id=card.videos.list.youtube.{id}`, `placement=videos.list`, `type=video`, and `source=youtube`
#### Scenario: Podcast card click emits media_preview on homepage
- **WHEN** a user clicks a podcast card in the homepage podcast section
- **THEN** the system emits a `media_preview` event with `target_id=card.home.podcast.podcast.{id}`, `placement=home.podcast`, `type=podcast_episode`, and `source=podcast`
### Requirement: Media preview close event
The tracking taxonomy MUST define a `media_preview_close` event emitted when the media modal is dismissed.
The `media_preview_close` event MUST include:
- `target_id` (the same identifier as the `media_preview` event that opened the modal)
- `close_method` (one of: `button`, `escape`, `backdrop`)
#### Scenario: User closes modal via Escape key
- **WHEN** the user presses Escape to close the media modal that was opened from a video card
- **THEN** the system emits a `media_preview_close` event with the original `target_id` and `close_method=escape`
#### Scenario: User closes modal via close button
- **WHEN** the user clicks the close button on the media modal
- **THEN** the system emits a `media_preview_close` event with the original `target_id` and `close_method=button`
#### Scenario: User closes modal via backdrop click
- **WHEN** the user clicks the backdrop outside the modal content
- **THEN** the system emits a `media_preview_close` event with the original `target_id` and `close_method=backdrop`
### Requirement: Modal CTA namespace
The tracking taxonomy MUST define a `media_modal` placement value for CTA interactions within the media modal.
Modal CTAs MUST use `target_id` values in the namespace `modal.cta.{action}.{platform}`.
The `action` value MUST be one of:
- `subscribe` (YouTube channel)
- `view` (YouTube video)
- `follow` (Spotify podcast profile)
- `listen` (Spotify episode)
#### Scenario: User clicks "View on YouTube" in modal
- **WHEN** the user clicks the "View on YouTube" CTA inside the media modal
- **THEN** the system emits a `cta_click` event with `target_id=modal.cta.view.youtube`, `placement=media_modal`, and `platform=youtube`
#### Scenario: User clicks "Subscribe on YouTube" in modal
- **WHEN** the user clicks the "Subscribe on YouTube" CTA inside the media modal
- **THEN** the system emits a `cta_click` event with `target_id=modal.cta.subscribe.youtube`, `placement=media_modal`, and `platform=youtube`
#### Scenario: User clicks "Follow on Spotify" in modal
- **WHEN** the user clicks the "Follow on Spotify" CTA inside the media modal
- **THEN** the system emits a `cta_click` event with `target_id=modal.cta.follow.spotify`, `placement=media_modal`, and `platform=spotify`
#### Scenario: User clicks "Listen on Spotify" in modal
- **WHEN** the user clicks the "Listen on Spotify" CTA inside the media modal
- **THEN** the system emits a `cta_click` event with `target_id=modal.cta.listen.spotify`, `placement=media_modal`, and `platform=spotify`
### Requirement: Minimum required properties
Every tracked click event MUST include, at minimum:
- `target_id`
@@ -46,6 +110,11 @@ The `type` value MUST be one of:
- `blog_post`
- `blog_page`
For non-link clickables that trigger in-page actions (e.g., modal openers), the event MUST also include:
- `title` (human-readable content title)
- `type` (content type identifier)
- `source` (content source identifier)
#### Scenario: Tracking a content card click
- **WHEN** a user clicks a content card link
- **THEN** the emitted event includes `target_id`, `placement`, and `target_url`
@@ -54,9 +123,35 @@ The `type` value MUST be one of:
- **WHEN** a user clicks a content-related link that represents a specific content item
- **THEN** the emitted event includes `target_id`, `placement`, `target_url`, `title`, and `type`
#### Scenario: Tracking a modal-trigger card includes title, type, and source
- **WHEN** a user clicks a content card that opens a media modal instead of navigating
- **THEN** the emitted event includes `target_id`, `placement`, `title`, `type`, and `source` (no `target_url` since the user stays on-page)
### Requirement: No PII in event properties
The taxonomy MUST prohibit including personally identifiable information (PII) in event names or event properties.
#### 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`

View File

@@ -0,0 +1,50 @@
## Purpose
Define a deterministic Lighthouse quality gate that produces repeatable scores and blocks regressions across required categories.
## Requirements
### Requirement: Deterministic Lighthouse configuration
The project MUST define a deterministic Lighthouse run configuration that specifies:
- a fixed list of URLs to audit
- mobile and desktop runs
- theme variants: `light`, `dark`, and `high-contrast`
- a clean run environment (no browser extensions)
- throttling / device emulation settings
The configuration MUST be checked into the repository.
#### Scenario: Lighthouse config is version-controlled
- **WHEN** a developer checks the repository
- **THEN** a Lighthouse configuration file/script exists and is referenced by a documented command
### Requirement: Lighthouse gate enforces perfect scores
The project MUST provide a command that runs Lighthouse for the configured URLs and variants and fails if any category score is below 100:
- Performance
- Accessibility
- Best Practices
- SEO
The command MUST output the reports as build artifacts (HTML and/or JSON) to a deterministic output directory.
#### Scenario: Gate fails on a regression
- **WHEN** Lighthouse runs and any audited variant scores 99 in any category
- **THEN** the command exits non-zero and reports which URL/variant failed
#### Scenario: Gate produces artifacts
- **WHEN** Lighthouse runs successfully
- **THEN** the reports are written to the configured output directory for later inspection
### Requirement: Repeatable scoring rule
The Lighthouse gate MUST define a repeatable scoring rule to reduce run-to-run noise.
At minimum, it MUST document one of the following:
- single-run with fixed throttling and clean environment, or
- multiple runs per variant with a deterministic selection rule (e.g., median)
#### Scenario: Run-to-run method is documented
- **WHEN** a developer runs the gate locally or in CI
- **THEN** the method for selecting/reporting scores is explicitly documented

View File

@@ -0,0 +1,109 @@
## Purpose
Define the media modal dialog behavior for in-page video and podcast previews.
## Requirements
### Requirement: Media modal dialog
The site MUST provide a modal dialog that displays embedded media (YouTube video or podcast episode) when a user clicks a video or podcast content card on a listing surface (homepage, `/videos`, `/podcast`).
The modal MUST render the following elements in order:
- A header row with the content title on the left and a close button on the right
- An embedded media player (YouTube iframe for videos; Spotify embed for podcast episodes when the URL is a Spotify URL; otherwise an in-modal audio player when an `audioUrl` is available)
- The full description/summary text (not truncated)
- The publish date and view count (when available)
- A "Subscribe on YouTube" / "Follow on Spotify" CTA and a "View on YouTube" / "Listen on Spotify" CTA
All modal CTAs that represent navigation MUST be implemented as crawlable anchors:
- Each CTA MUST be an `<a>` element with a non-empty `href` attribute.
- The UI MUST NOT render placeholder `<a>` elements without `href` in the initial HTML.
- If CTA destinations are not known until a user selects an item, the CTA UI MUST be rendered as non-anchor elements until the destinations are known.
#### Scenario: User clicks a YouTube video card
- **WHEN** a user clicks a video content card on any listing surface
- **THEN** a modal dialog opens displaying a YouTube iframe embed, the video title, full description, date, view count (if available), and CTAs for "Subscribe on YouTube" and "View on YouTube"
#### Scenario: User clicks a podcast episode card with a Spotify URL
- **WHEN** a user clicks a podcast content card whose URL is a Spotify URL
- **THEN** a modal dialog opens displaying a Spotify episode embed, the episode title, full description, date, and CTAs for "Follow on Spotify" and "Listen on Spotify"
#### Scenario: User clicks a podcast episode card with a non-Spotify URL
- **WHEN** a user clicks a podcast content card whose URL is not a Spotify URL
- **THEN** the modal dialog opens displaying the episode metadata (title, description, date) and either:
- an in-modal audio player when an `audioUrl` is available
- otherwise, a "Listen on Spotify" outbound link
#### Scenario: Modal renders with missing optional fields
- **WHEN** a content item has no view count or no summary
- **THEN** the modal MUST still render cleanly with those fields omitted
#### Scenario: Modal CTAs are crawlable anchors
- **WHEN** the modal is present in the DOM (before any user interaction)
- **THEN** the document contains no `<a>` elements in the modal that are missing `href`
### Requirement: Embed fallback is a link only when a destination is available
If an embed fallback is presented as a link to an external page, it MUST be an anchor with a valid `href`. If no destination is available, the fallback MUST be hidden or rendered as non-link text.
#### Scenario: Embed fallback does not render a non-crawlable anchor
- **WHEN** the modal is rendered before any item selection
- **THEN** the embed fallback is not rendered as an anchor without `href`
### Requirement: Playback stops on modal close
The modal MUST stop all media playback when it is dismissed, regardless of the dismissal method.
The modal MUST support three dismissal methods:
- Close button click
- Pressing the `Escape` key
- Clicking the backdrop outside the modal content
After dismissal, no audio or video from the embedded player MUST continue playing.
#### Scenario: User closes modal via close button
- **WHEN** the modal is open with a playing YouTube video and the user clicks the close button
- **THEN** the modal closes and the video playback stops immediately
#### Scenario: User presses Escape while modal is open
- **WHEN** the modal is open with a playing Spotify episode and the user presses the `Escape` key
- **THEN** the modal closes and the audio playback stops immediately
#### Scenario: User clicks the backdrop
- **WHEN** the modal is open and the user clicks outside the modal content area (the backdrop)
- **THEN** the modal closes and any active media playback stops immediately
### Requirement: Modal accessibility
The modal MUST conform to WCAG 2.2 AA dialog patterns:
- The modal MUST use the native `<dialog>` element opened via `showModal()`
- The modal MUST trap keyboard focus within the dialog while open
- The modal MUST set `aria-modal="true"` and have an accessible label (via `aria-labelledby` referencing the title element)
- Closing the modal MUST return focus to the element that triggered it (the card that was clicked)
- The close button MUST have an accessible label (e.g., `aria-label="Close"`)
#### Scenario: Focus is trapped within the modal
- **WHEN** the modal is open and the user presses `Tab`
- **THEN** focus cycles through the focusable elements within the modal and does not move to elements behind the modal
#### Scenario: Focus returns to trigger on close
- **WHEN** the user closes the modal
- **THEN** focus returns to the card element that originally opened the modal
### Requirement: Responsive modal layout
The modal MUST be responsive across viewports:
- On desktop viewports, the modal MUST be centered with a max-width that leaves visible backdrop on both sides
- On mobile viewports (at or below the site's mobile breakpoint), the modal MUST expand to near-full viewport width with reduced padding
- Embedded media MUST scale proportionally (16:9 aspect ratio for YouTube video, fixed height for Spotify embed)
#### Scenario: Modal on desktop viewport
- **WHEN** the modal is opened on a desktop viewport
- **THEN** the modal is centered horizontally with backdrop visible and the video embed maintains a 16:9 aspect ratio
#### Scenario: Modal on mobile viewport
- **WHEN** the modal is opened on a mobile viewport
- **THEN** the modal expands to near-full viewport width and the video embed scales to fit
### Requirement: Embed loading state
The modal MUST display a loading placeholder while the embedded media iframe is loading.
#### Scenario: Iframe loading
- **WHEN** the modal opens and the iframe has not yet loaded
- **THEN** a placeholder (matching the site's card-placeholder style) is visible in the embed area until the iframe finishes loading

View File

@@ -0,0 +1,51 @@
# nav-hover-line Specification
## Purpose
TBD - created by archiving change final-touches. Update Purpose after archive.
## Requirements
### Requirement: Hover line appears only on hover for primary header titles
The site MUST render a decorative animated line that appears only on hover for the primary header title links:
- SanthoshJ (center brand link)
- Videos
- Podcast
- Blog
The hover line MUST NOT cause layout shift.
#### Scenario: Hover line is hidden by default
- **WHEN** the header renders
- **THEN** the hover line is not visible on primary header title links until hover
#### Scenario: Hover line appears on hover
- **WHEN** a pointer user hovers a primary header title link
- **THEN** an animated line appears as a hover affordance for that title
### Requirement: Hover line applies to section/module titles for key surfaces
The site MUST apply the same hover-line treatment to the titles for the Videos, Podcast, and Blog surfaces, but only on titles (not on card titles).
#### Scenario: Titles use hover line
- **WHEN** a pointer user hovers the Videos/Podcast/Blog surface title element
- **THEN** the hover line appears only for that title
#### Scenario: Card titles are unaffected
- **WHEN** a pointer user hovers a content card title
- **THEN** no hover line effect is applied (existing behavior remains)
### Requirement: Hover line is gated by a public env flag
The hover-line effect MUST be controllable via a public environment variable `PUBLIC_ENABLE_NAV_HOVER_LINE`.
If `PUBLIC_ENABLE_NAV_HOVER_LINE` is set to the string `"false"`, the hover-line effect MUST be disabled and the current behavior MUST continue.
#### Scenario: Flag disables hover line
- **WHEN** `PUBLIC_ENABLE_NAV_HOVER_LINE` is set to `"false"`
- **THEN** hovering primary header title links and surface titles does not render the hover line effect
### Requirement: Reduced motion disables or substantially reduces animation
If `prefers-reduced-motion: reduce` is set, the hover-line animation MUST be disabled or substantially reduced.
#### Scenario: Reduced motion disables noticeable hover animation
- **WHEN** `prefers-reduced-motion: reduce` is set and a user hovers a nav title
- **THEN** the hover line does not animate noticeably

View File

@@ -0,0 +1,29 @@
# navbar-branding Specification
## Purpose
TBD - created by archiving change final-touches. Update Purpose after archive.
## Requirements
### Requirement: Primary header shows logo and centered brand
The site MUST render a primary header that includes:
- a left-side logo that links to `/`
- a centered brand label "SanthoshJ" that links to `/`
- the primary navigation links (Videos, Podcast, Blog)
The logo MUST use the same visual asset as the site favicon.
#### Scenario: Desktop header layout
- **WHEN** a user loads the home page on a desktop viewport
- **THEN** the header shows the logo on the left, the brand label centered, and the nav links on the right
#### Scenario: Mobile header layout
- **WHEN** a user loads the home page on a mobile viewport
- **THEN** the header still shows the logo on the left and the brand label centered without overlapping the nav toggle
### Requirement: Logo size is visually aligned
The header logo MUST be sized to match the header rhythm and MUST not cause layout shift.
#### Scenario: Logo has fixed dimensions
- **WHEN** the header renders
- **THEN** the logo element has explicit width and height and does not change size on load

View File

@@ -0,0 +1,33 @@
## Purpose
Define responsive image delivery requirements that reduce layout shift and improve deterministic performance outcomes for Lighthouse.
## Requirements
### Requirement: Card thumbnails have explicit dimensions
All card thumbnail images MUST include explicit `width` and `height` attributes matching the rendered aspect ratio.
#### Scenario: Thumbnail dimensions are present
- **WHEN** a crawler or browser loads a listing surface with content cards
- **THEN** each card thumbnail `<img>` includes `width` and `height` attributes
### Requirement: Above-the-fold imagery is optimized
The site MUST ensure above-the-fold images are optimized for performance:
- avoid unnecessarily large image payloads for the requested viewport
- prefer modern image formats when first-party controlled
- avoid layout shift caused by late image dimension discovery
#### Scenario: Above-the-fold images do not cause layout shift
- **WHEN** a user loads the home page on a mobile viewport
- **THEN** the hero/top content area does not shift vertically due to image loading
### Requirement: Third-party image variability is controlled for the Lighthouse gate
If a gated page depends on third-party images (e.g., external thumbnails), the gate MUST either:
- ensure those images do not block reaching 100s by design (e.g., not above-the-fold, lazy-loaded), or
- provide a first-party controlled alternative for gated pages.
#### Scenario: Gated pages avoid third-party image volatility
- **WHEN** Lighthouse runs against the gated URL set
- **THEN** third-party image delivery does not prevent meeting the required scores

View File

@@ -42,7 +42,7 @@ The site MUST provide Open Graph and Twitter card metadata for indexable pages s
### Requirement: Sitemap and robots
The site MUST provide:
- `sitemap.xml` enumerating indexable pages
- `sitemap-index.xml` enumerating indexable pages (and/or referencing sitemap shards)
- `robots.txt` that allows indexing of indexable pages
The sitemap MUST include the blog surface routes:
@@ -51,17 +51,32 @@ The sitemap MUST include the blog surface routes:
- blog page detail routes
- blog category listing routes
#### Scenario: Sitemap is available
- **WHEN** a crawler requests `/sitemap.xml`
- **THEN** the server returns an XML sitemap listing `/`, `/videos`, `/podcast`, `/about`, and `/blog`
`robots.txt` MUST reference the sitemap using an absolute URL for the production domain.
#### Scenario: Sitemap index is available
- **WHEN** a crawler requests `/sitemap-index.xml`
- **THEN** the server returns an XML sitemap index (or 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 references sitemap with absolute URL
- **WHEN** a crawler requests `/robots.txt`
- **THEN** the response contains a `Sitemap:` line with an absolute URL to `/sitemap-index.xml`
### Requirement: Structured data
The site MUST support structured data (JSON-LD) for Video and Podcast content when detail pages exist, and MUST ensure the JSON-LD is valid JSON.
#### Scenario: Video structured data present
- **WHEN** a video detail page exists and is requested
- **THEN** the HTML includes JSON-LD describing the video using a recognized schema type
### Requirement: Organization and website structured data
The home page SHOULD include JSON-LD structured data for the site and its owner/organization.
If present, the JSON-LD MUST be valid JSON and MUST use a recognized schema type.
#### Scenario: Home page includes valid JSON-LD
- **WHEN** a crawler requests `/`
- **THEN** the HTML contains a JSON-LD script tag that parses as valid JSON

View File

@@ -0,0 +1,64 @@
## Purpose
Improve repeat-visit performance and reduce network usage by using a service worker to pre-cache critical shell assets and apply safe runtime caching strategies.
## Requirements
### Requirement: Service Worker registration
The site SHALL register a Service Worker on supported browsers when running in production (HTTPS), scoped to the site root so it can control all site pages.
#### Scenario: Production registration
- **WHEN** a user loads any page in a production environment
- **THEN** the site registers a service worker at `/sw.js` with scope `/`
#### Scenario: Development does not register
- **WHEN** a user loads any page in a local development environment
- **THEN** the site does not register a service worker
### Requirement: Pre-cache critical site shell assets
The Service Worker SHALL pre-cache a set of critical static assets required to render the site shell quickly on repeat visits.
#### Scenario: Pre-cache on install
- **WHEN** the service worker is installed
- **THEN** it caches the configured site shell assets in a versioned cache
### Requirement: Runtime caching for media assets
The Service Worker SHALL use runtime caching for media assets (for example images) to reduce repeat network fetches, while ensuring content can refresh.
#### Scenario: Cache-first for images
- **WHEN** a user requests an image resource
- **THEN** the service worker serves the cached image when available, otherwise fetches from the network and stores the response in the media cache
#### Scenario: Enforce cache size bounds
- **WHEN** the number of cached media items exceeds the configured maximum
- **THEN** the service worker evicts older entries to stay within the bound
### Requirement: Navigation requests avoid indefinite staleness
The Service Worker MUST NOT serve stale HTML indefinitely for navigation requests.
#### Scenario: Network-first navigation
- **WHEN** a user navigates to a page route (a document navigation request)
- **THEN** the service worker attempts to fetch from the network first and falls back to a cached response if the network is unavailable
### Requirement: Safe updates and cache cleanup
The Service Worker SHALL use versioned caches and remove old caches during activation to ensure updated assets are used after a new deploy.
#### Scenario: Activate new version and clean old caches
- **WHEN** a new service worker version activates
- **THEN** it deletes caches from older versions and begins using the current versioned caches
### Requirement: Critical assets do not remain stale after deploy
The service worker and server caching strategy MUST ensure critical shell assets (including the global stylesheet and service worker script) do not remain stale across deploys.
The implementation MUST include at least one cache-busting mechanism for critical assets, such as:
- content-hashed asset filenames, or
- an asset version query suffix that changes per deploy
#### Scenario: New deploy updates critical CSS
- **WHEN** a new deploy is released and a returning user loads the site
- **THEN** the user receives the updated global stylesheet without requiring a manual hard refresh
#### Scenario: Service worker updates predictably
- **WHEN** a new deploy is released
- **THEN** the browser can retrieve the updated service worker script and activate it without being pinned by long-lived caches

View File

@@ -0,0 +1,100 @@
## Purpose
Define site-wide theme support (dark, light, high-contrast) using CSS tokens and an application mechanism that can switch across the entire UI.
## Requirements
### Requirement: Site themes
The site MUST support three themes:
- `dark`
- `light`
- `high-contrast`
Themes MUST be applied by setting a `data-theme` attribute on the root document element (`<html>`).
#### Scenario: Dark theme active
- **WHEN** `data-theme="dark"` is set on `<html>`
- **THEN** the site's background, text, and component styling reflect the dark palette
#### Scenario: Light theme active
- **WHEN** `data-theme="light"` is set on `<html>`
- **THEN** the site's background, text, and component styling reflect the light palette
#### Scenario: High contrast theme active
- **WHEN** `data-theme="high-contrast"` is set on `<html>`
- **THEN** the site uses a high-contrast palette with a clearly visible focus ring and high-contrast borders
### Requirement: Theme tokens meet contrast intent
For each supported theme (`dark`, `light`, `high-contrast`), the theme token pairs used for primary text and primary surfaces MUST meet WCAG 2.2 AA contrast intent.
At minimum:
- primary body text on the primary background MUST be high-contrast
- link text on the primary background MUST be distinguishable and meet contrast intent
- secondary labels on the primary background MUST remain readable
#### Scenario: Dark theme text is readable
- **WHEN** `data-theme="dark"` is active
- **THEN** primary text remains readable against primary surfaces without low-contrast combinations
#### Scenario: Dark theme links are readable
- **WHEN** `data-theme="dark"` is active
- **THEN** links in common surfaces are readable against their background
### Requirement: Theme persistence
The site MUST persist the user's theme selection so it is retained across page loads and navigations.
Persistence MUST be stored locally in the browser (e.g., localStorage).
#### Scenario: Theme persists across reload
- **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.
Default selection order:
1) If forced colors / high-contrast mode is active, default to `high-contrast`
2) Else if the system prefers light color scheme, default to `light`
3) Else default to `dark`
#### Scenario: No stored preference uses system settings
- **WHEN** the user has no stored theme preference
- **THEN** the site selects a default theme based on forced-colors and prefers-color-scheme
### Requirement: Theme switching transition
Theme changes initiated by the user MUST transition smoothly.
The transition MUST be disabled or substantially reduced when `prefers-reduced-motion: reduce` is set.
#### Scenario: Smooth transition on switch
- **WHEN** the user switches from `dark` to `light` theme
- **THEN** theme-affecting properties transition smoothly instead of abruptly switching
#### Scenario: Reduced motion disables theme animation
- **WHEN** `prefers-reduced-motion: reduce` is set and the user switches theme
- **THEN** the theme change occurs without noticeable animation

View File

@@ -0,0 +1,46 @@
## Purpose
Define the requirements for a floating, accessible theme switcher notch anchored to the right side of the viewport.
## Requirements
### Requirement: Floating theme switcher notch
The site MUST provide a floating theme switcher control anchored to the right side of the viewport.
The control MUST be positioned below the primary navigation bar and MUST leave sufficient vertical space for secondary navigation.
#### Scenario: Notch positioned below header
- **WHEN** the page loads
- **THEN** the theme switcher notch is visible on the right side and does not overlap the sticky header or sub-navigation
### Requirement: Notch interaction and animation
The notch MUST provide a hover affordance (a small, tasteful animation) that indicates it is interactive.
The hover animation MUST be disabled or substantially reduced under `prefers-reduced-motion: reduce`.
#### Scenario: Hover animation present
- **WHEN** a pointer user hovers the notch
- **THEN** the notch animates in a way that suggests it can be expanded or interacted with
#### Scenario: Reduced motion disables hover animation
- **WHEN** `prefers-reduced-motion: reduce` is set
- **THEN** hovering the notch does not trigger a noticeable animation
### Requirement: Theme selection UI
The notch MUST expose the three theme options (`dark`, `light`, `high-contrast`) and allow the user to select one.
The control MUST be keyboard accessible:
- it MUST be reachable via `Tab`
- it MUST have a visible focus indicator
- selection MUST be possible using keyboard input
#### Scenario: Keyboard selects theme
- **WHEN** a keyboard user focuses the notch and selects `high-contrast`
- **THEN** the site updates to the `high-contrast` theme and the selection is persisted
### Requirement: Accessibility labels
The notch and theme options MUST have accessible labels.
#### Scenario: Screen reader announces theme switcher
- **WHEN** a screen reader user focuses the theme switcher control
- **THEN** it announces an appropriate label (e.g., "Theme" or "Theme switcher") and the currently selected theme

View File

@@ -69,3 +69,51 @@ The site MUST ensure text remains readable:
- **WHEN** a user navigates between pages
- **THEN** typography (font family and basic scale) remains consistent
### Requirement: Theme switching accessibility
Theme switching controls MUST be accessible and usable with keyboard and assistive technology.
The theme switcher control MUST:
- be reachable via keyboard navigation
- provide a visible focus indication
- expose an accessible name/label
- allow selecting any supported theme without requiring a pointer
#### Scenario: Theme switcher is keyboard reachable
- **WHEN** a keyboard user tabs through the page
- **THEN** the theme switcher notch receives focus and shows a visible focus indicator
#### Scenario: Theme switcher is labeled
- **WHEN** a screen reader user focuses the theme switcher
- **THEN** it announces a meaningful label and the current theme state
### Requirement: High contrast theme meets WCAG intent
The `high-contrast` theme MUST provide materially higher contrast than the default theme.
The theme MUST keep text readable and interactive affordances obvious, including:
- strong foreground/background contrast
- clearly visible focus ring
- strong borders on interactive elements
#### Scenario: High contrast theme improves readability
- **WHEN** the user enables `high-contrast` theme
- **THEN** primary text and secondary UI labels remain clearly readable and interactive elements are visually distinct
### Requirement: Interactive elements use correct semantics
Interactive elements MUST use correct semantic elements:
- Navigation MUST use anchors (`<a>`) with valid `href`.
- Actions that do not navigate MUST use `<button>`.
The site MUST NOT render anchor elements that lack an `href` attribute.
#### Scenario: Modal triggers are buttons
- **WHEN** a content card opens the media modal
- **THEN** the card is a `<button>` element and is keyboard operable
#### Scenario: Navigation CTAs are anchors
- **WHEN** a user sees a CTA that navigates to another page
- **THEN** the CTA is an `<a>` element with a valid `href`
#### Scenario: No non-crawlable anchors exist
- **WHEN** a crawler inspects the rendered HTML
- **THEN** there are no `<a>` elements without `href`

76
review-notes.md Normal file
View File

@@ -0,0 +1,76 @@
# Performance Review — santhoshj.com
Date: 2026-04-11
## Lighthouse Scores
| Category | Desktop | Mobile |
|----------|---------|--------|
| Performance | 100 | 95 |
| Accessibility | 100 | 100 |
| Best Practices | 100 | 100 |
| SEO | 100 | 100 |
## Core Web Vitals
| Metric | Desktop | Mobile | Rating |
|--------|---------|--------|--------|
| FCP | 0.4s | 2.3s | Desktop: Great / Mobile: Needs improvement |
| LCP | 0.6s | 2.4s | Desktop: Great / Mobile: Good |
| TBT | 0ms | 0ms | Great |
| CLS | 0.001 | 0.001 | Great |
| SI | 0.5s | 2.4s | Desktop: Great / Mobile: Great |
| TTI | 0.6s | 2.4s | Desktop: Great / Mobile: Great |
LCP element: H1 text node (no image resource).
---
## Actionable Items (Priority Order)
### 1. Fix forced reflow — 48ms waste
- `setOpen()` at `index.html:502` causes 47ms forced sync layout
- `updateNotchTop()` at `index.html:623` also triggers reflow
- Root cause: reads geometric properties (e.g. `offsetWidth`) after DOM writes
- Fix: batch all layout reads before writes, or wrap writes in `requestAnimationFrame`
### 2. Add cache headers to JS bundles
- Astro-hashed JS assets served with `cache-control` TTL=0
- These files have content hashes in filenames — safe to cache long-term
- Fix: set `cache-control: public, max-age=31536000, immutable` on `/_astro/*` and other hashed assets
- Location: nginx config or CDN cache policy
### 3. Remove `cache-control: no-store` from HTML
- Current: HTML doc uses `no-store` → back/forward cache disabled
- Prevents instant back navigation (bfcache)
- Fix: replace `no-store` with `cache-control: max-age=0, must-revalidate`
- Also affects JS network requests receiving `no-store` resources
### 4. Tree-shake unused CSS — 30KB waste
- Two CSS chunks (16KB + 14KB) are entirely unused on the homepage
- Likely page-specific styles loaded eagerly
- Fix: ensure Astro code-splitting for CSS per page, or remove unused selectors
- Check `/_astro/_slug_.*.css` — may bundle all page styles into one sheet
### 5. Defer render-blocking JS — 550ms mobile waste
- Main JS bundle and CSS are render-blocking
- Fix: inline critical JS/CSS, defer remaining with `async` or `defer`
- Or: use `<link rel="preload">` for critical resources + `defer` for non-critical
### 6. Reduce unused JavaScript — 38KB / 99KB (38% waste)
- Main Astro JS bundle: 38KB unused out of 99KB
- Fix: audit Astro client-side scripts, remove unused island code, or split into smaller chunks
- Check if `setOpen`, `updateNotchTop`, umami analytics, and other inline scripts can be deferred
### 7. Optimize image delivery — 323KB mobile savings
- YouTube thumbnails served as-is (JPEG, no WebP/AVIF)
- No responsive `srcset` — full-size thumbnails on all viewports
- Fix: use `<picture>` with WebP/AVIF fallback, add `loading="lazy"` for below-fold images, consider `width`/`height` attributes to reduce CLS risk
- Note: these are 3rd-party `ytimg.com` URLs — limited control. Consider proxying or using YouTube's `mqdefault.jpg` (smaller) for mobile

Some files were not shown because too many files have changed in this diff Show More