# 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 `` - Modify: `website/episode.html` — replace lines 95-136 with empty footer, add script tag - Modify: `website/clips.html` — replace lines 68-109 with empty footer, add script tag - Modify: `website/stats.html` — replace inline footer with empty footer, add script tag - Modify: `website/privacy.html` — replace inline footer with empty footer, add script tag - Modify: `website/terms.html` — replace inline footer with empty footer, add script tag - Modify: `website/how-it-works.html` — replace inline footer with empty footer, add script tag **Step 1: Update each page** For each of the 7 HTML files: 1. Replace the entire `` block with just `` 2. Add `` near the end of ``, 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(/<!\[CDATA\[|\]\]>/g, "").trim() : ""; const descMatch = item.match(/<description>(.*?)<\/description>/s); description = descMatch ? descMatch[1].replace(/<!\[CDATA\[|\]\]>/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(/</g, "<"); const escDesc = description.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<"); const canonicalUrl = `https://lukeattheroost.com/episode.html?slug=${slug}`; // Replace placeholder meta tags html = html.replace( /<meta property="og:title"[^>]*>/, `<meta property="og:title" content="${escTitle}">` ); html = html.replace( /<meta property="og:description"[^>]*>/, `<meta property="og:description" content="${escDesc}">` ); html = html.replace( /<meta property="og:url"[^>]*>/, `<meta property="og:url" content="${canonicalUrl}">` ); html = html.replace( /<meta name="twitter:title"[^>]*>/, `<meta name="twitter:title" content="${escTitle}">` ); html = html.replace( /<meta name="twitter:description"[^>]*>/, `<meta name="twitter:description" content="${escDesc}">` ); html = html.replace( /<title[^>]*>.*?<\/title>/, `<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