From 9071ed36d372dbcf32135bf135d184025dedf324 Mon Sep 17 00:00:00 2001
From: tcpsyn
Date: Fri, 14 Aug 2026 04:23:53 -0500
Subject: [PATCH] Track design and implementation plans
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Seven plan documents covering the website JS infrastructure, show theme, caller
quality overhaul, cost dashboard, the Alpine relocation, and caller variety plus
Devon's search — the record of why the current architecture looks the way it
does. Plus the press release draft.
Co-Authored-By: Claude Opus 5
---
.../2026-03-16-website-js-infrastructure.md | 719 ++++++++++++++++
docs/plans/2026-03-17-show-theme.md | 335 ++++++++
.../2026-03-30-caller-quality-overhaul.md | 300 +++++++
.../plans/2026-04-02-cost-dashboard-design.md | 107 +++
...026-04-02-cost-dashboard-implementation.md | 810 ++++++++++++++++++
.../2026-05-31-alpine-relocation-design.md | 78 ++
...-caller-variety-and-devon-search-design.md | 56 ++
press_release.md | 55 ++
8 files changed, 2460 insertions(+)
create mode 100644 docs/plans/2026-03-16-website-js-infrastructure.md
create mode 100644 docs/plans/2026-03-17-show-theme.md
create mode 100644 docs/plans/2026-03-30-caller-quality-overhaul.md
create mode 100644 docs/plans/2026-04-02-cost-dashboard-design.md
create mode 100644 docs/plans/2026-04-02-cost-dashboard-implementation.md
create mode 100644 docs/plans/2026-05-31-alpine-relocation-design.md
create mode 100644 docs/plans/2026-06-02-caller-variety-and-devon-search-design.md
create mode 100644 press_release.md
diff --git a/docs/plans/2026-03-16-website-js-infrastructure.md b/docs/plans/2026-03-16-website-js-infrastructure.md
new file mode 100644
index 0000000..48397fe
--- /dev/null
+++ b/docs/plans/2026-03-16-website-js-infrastructure.md
@@ -0,0 +1,719 @@
+# Website JS Infrastructure, SEO, Shared Components & Content Fixes
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** Fix JS duplication, add worker-level social meta injection, standardize analytics proxying, improve security and UX, and clean up content/SEO issues across lukeattheroost.com.
+
+**Architecture:** Extract shared footer into `js/footer.js`, extract shared audio player into `js/player.js`, enhance `_worker.js` to intercept social crawler requests and inject episode-specific meta tags, switch all subpages to proxied Plausible analytics, add episode pagination, fix XSS surfaces, and clean up sitemap/clips data.
+
+**Tech Stack:** Vanilla JS, Cloudflare Pages Worker (ES module), static HTML, XML sitemap
+
+---
+
+### Task 1: Create shared footer component (`js/footer.js`)
+
+**Files:**
+- Create: `website/js/footer.js`
+
+**Step 1: Write footer.js**
+
+The footer HTML is duplicated across 7 pages (index.html:265-306, episode.html:95-136, clips.html:68-109, stats.html, privacy.html, terms.html, how-it-works.html). Extract the footer from `index.html` as the canonical version.
+
+```js
+function initFooter() {
+ const footer = document.querySelector('.footer');
+ if (!footer) return;
+
+ footer.innerHTML = `
+
+
+
+
+
+
+ © 2026 Luke at the Roost · Privacy Policy · Terms of Service · System Status
+ `;
+}
+
+initFooter();
+```
+
+**Step 2: Commit**
+
+```bash
+git add website/js/footer.js
+git commit -m "Add shared footer component (js/footer.js)"
+```
+
+---
+
+### Task 2: Replace inline footers with shared component
+
+**Files:**
+- Modify: `website/index.html` — replace lines 265-306 (inline footer content) with empty ``, add `` before closing `
`, before any page-specific scripts
+
+Note: index.html's footer has slightly different nav links (no "Home" link since it IS home). The shared footer includes "Home" which is fine — clicking Home on the homepage just reloads it.
+
+**Step 2: Verify no footer content remains inline**
+
+Search for `footer-icons-label` in all HTML files — should only appear in `js/footer.js`.
+
+**Step 3: Commit**
+
+```bash
+git add website/index.html website/episode.html website/clips.html website/stats.html website/privacy.html website/terms.html website/how-it-works.html
+git commit -m "Replace inline footers with shared footer.js component"
+```
+
+---
+
+### Task 3: Extract shared audio player module (`js/player.js`)
+
+**Files:**
+- Create: `website/js/player.js`
+
+The audio player code is duplicated: `app.js:1-11,14-23,143-226` and `episode.html:159-346` (inline `` then ``
+- Create: `website/js/episode.js` — episode-specific logic extracted from episode.html inline script (load episode from RSS, populate header, transcript loading, play button)
+- Modify: `website/index.html` — add `` before `app.js`
+
+**Step 1: Refactor app.js**
+
+Remove from app.js:
+- Lines 1-11 (element lookups — now in player.js)
+- Lines 14-23 (formatTime — now in player.js)
+- Lines 173-226 (audio event listeners, updatePlayIcons, playerPlayBtn click, playerProgress click — now in player.js)
+
+Keep the `currentEpisodeCard` variable and the card-specific icon toggling in `updatePlayIcons`. Since player.js handles the sticky player icons, app.js only needs to handle the episode card icons. Add a listener:
+
+```js
+audio.addEventListener('play', () => {
+ if (currentEpisodeCard) {
+ const btn = currentEpisodeCard.querySelector('.episode-play-btn');
+ if (btn) { btn.innerHTML = pauseSVG; btn.classList.add('playing'); }
+ }
+});
+audio.addEventListener('pause', () => {
+ if (currentEpisodeCard) {
+ const btn = currentEpisodeCard.querySelector('.episode-play-btn');
+ if (btn) { btn.innerHTML = playSVG; btn.classList.remove('playing'); }
+ }
+});
+audio.addEventListener('ended', () => {
+ if (currentEpisodeCard) {
+ const btn = currentEpisodeCard.querySelector('.episode-play-btn');
+ if (btn) { btn.innerHTML = playSVG; btn.classList.remove('playing'); }
+ }
+});
+```
+
+**Step 2: Create episode.js**
+
+Extract episode-specific logic from episode.html inline script. Use the global `audio`, `playerTitle`, `stickyPlayer` from player.js. Include: `formatDate`, `parseDuration`, `stripHtml`, slug parsing, `loadEpisode()`.
+
+**Step 3: Update HTML script tags**
+
+In `index.html`, change script loading order:
+```html
+
+
+
+
+
+```
+
+In `episode.html`, replace inline `
+
+
+```
+
+**Step 4: Commit**
+
+```bash
+git add website/js/app.js website/js/player.js website/js/episode.js website/index.html website/episode.html
+git commit -m "Deduplicate audio player code into shared player.js module"
+```
+
+---
+
+### Task 5: Fix Plausible analytics — switch all subpages to proxied version
+
+**Files:**
+- Modify: `website/episode.html` line 51-52
+- Modify: `website/clips.html` line 43-44
+- Modify: `website/stats.html` line 43-44
+- Modify: `website/privacy.html` line 37-38
+- Modify: `website/terms.html` line 37-38
+- Modify: `website/how-it-works.html` line 66-67
+
+**Step 1: In each file, replace the direct Plausible script tag**
+
+Replace:
+```html
+
+
+```
+
+With:
+```html
+
+
+```
+
+**Step 2: Verify**
+
+Grep for `plausible.macneilmediagroup.com` in HTML files — should return 0 matches (only `_worker.js` should have it).
+
+**Step 3: Commit**
+
+```bash
+git add website/episode.html website/clips.html website/stats.html website/privacy.html website/terms.html website/how-it-works.html
+git commit -m "Switch all subpages to proxied Plausible analytics"
+```
+
+---
+
+### Task 6: Worker — social crawler meta tag injection for episode pages
+
+**Files:**
+- Modify: `website/_worker.js`
+
+**Step 1: Add social crawler detection and meta injection**
+
+Before the `return env.ASSETS.fetch(request)` line (line 90), add a handler for `/episode.html` requests from social crawlers:
+
+```js
+// Social crawler meta injection for episode pages
+if (url.pathname === "/episode.html" && url.searchParams.get("slug")) {
+ const ua = (request.headers.get("User-Agent") || "").toLowerCase();
+ const isCrawler = /facebookexternalhit|twitterbot|linkedinbot|slackbot|discordbot|telegrambot|whatsapp|pinterest|redditbot/i.test(ua);
+
+ if (isCrawler) {
+ const slug = url.searchParams.get("slug");
+
+ // Fetch RSS to find episode info
+ try {
+ const feedResp = await fetch("https://podcast.macneilmediagroup.com/@LukeAtTheRoost/feed.xml", {
+ signal: AbortSignal.timeout(5000),
+ });
+ if (feedResp.ok) {
+ const feedXml = await feedResp.text();
+
+ // Simple string-based extraction (no DOM parser in Workers)
+ const items = feedXml.split("- ");
+ let title = "";
+ let description = "";
+
+ for (let i = 1; i < items.length; i++) {
+ const item = items[i];
+ const linkMatch = item.match(/(.*?)<\/link>/);
+ if (linkMatch) {
+ const itemSlug = linkMatch[1].split("/episodes/").pop()?.replace(/\/$/, "");
+ if (itemSlug === slug) {
+ const titleMatch = item.match(/(.*?)<\/title>/);
+ title = titleMatch ? titleMatch[1].replace(//g, "").trim() : "";
+ const descMatch = item.match(/(.*?)<\/description>/s);
+ description = descMatch
+ ? descMatch[1].replace(//g, "").replace(/<[^>]+>/g, "").trim().slice(0, 200)
+ : "";
+ break;
+ }
+ }
+ }
+
+ if (title) {
+ // Fetch the actual HTML page
+ const pageResp = await env.ASSETS.fetch(request);
+ let html = await pageResp.text();
+
+ const escTitle = title.replace(/&/g, "&").replace(/"/g, """).replace(/]*>/,
+ ``
+ );
+ html = html.replace(
+ /]*>/,
+ ``
+ );
+ html = html.replace(
+ /]*>/,
+ ``
+ );
+ html = html.replace(
+ /]*>/,
+ ``
+ );
+ html = html.replace(
+ /]*>/,
+ ``
+ );
+ html = html.replace(
+ /]*>.*?<\/title>/,
+ `${escTitle} — Luke at the Roost`
+ );
+
+ return new Response(html, {
+ status: 200,
+ headers: { "Content-Type": "text/html;charset=UTF-8" },
+ });
+ }
+ }
+ } catch (e) {
+ // Fall through to static page
+ }
+ }
+}
+```
+
+**Step 2: Commit**
+
+```bash
+git add website/_worker.js
+git commit -m "Add social crawler meta tag injection for episode pages"
+```
+
+---
+
+### Task 7: Security — sanitize innerHTML XSS surfaces
+
+**Files:**
+- Modify: `website/js/episode.js` (created in Task 4)
+- Modify: `website/js/app.js`
+
+**Step 1: Fix episode description XSS in episode.js**
+
+In the `loadEpisode` function, change line that sets description:
+```js
+// BEFORE (XSS):
+document.getElementById('ep-desc').innerHTML = episode.description || '';
+
+// AFTER (safe):
+document.getElementById('ep-desc').textContent = stripHtml(episode.description || '');
+```
+
+**Step 2: Fix title escaping in app.js episode card rendering**
+
+In `renderEpisodes()`, the title goes into a `data-title` attribute with basic `.replace(/"/g, '"')`. Use the `escapeHTML` pattern from clips.js. Add a helper at top of app.js:
+
+```js
+function escapeAttr(str) {
+ return str.replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>');
+}
+```
+
+Then change line 125:
+```js
+// BEFORE:
+data-title="${ep.title.replace(/"/g, '"')}"
+
+// AFTER:
+data-title="${escapeAttr(ep.title)}"
+```
+
+Also escape the title in the aria-label and visible title:
+```js
+