Growth features: share buttons, NEW badge, sticky CTA, newsletter cross-promote

- Share buttons on episode and clip cards (Web Share API + clipboard fallback)
- NEW badge on latest episode card
- Sticky call-in CTA bar (appears after hero scrolls out)
- Daily AI Briefing newsletter cross-promote in footer
- Bump cache versions to v=5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-16 01:23:43 -06:00
parent d39cb3f3d4
commit 39297d4aa5
12 changed files with 314 additions and 11 deletions
+47 -2
View File
@@ -51,6 +51,7 @@ function truncate(html, maxLen) {
// SVG icons
const playSVG = '<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>';
const pauseSVG = '<svg viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>';
const shareSVG = '<svg viewBox="0 0 24 24"><path d="M16 5l-1.42 1.42-1.59-1.59V16h-2V4.83L9.42 6.42 8 5l4-4 4 4zm4 5v11a2 2 0 01-2 2H6a2 2 0 01-2-2V10a2 2 0 012-2h3v2H6v11h12V10h-3V8h3a2 2 0 012 2z"/></svg>';
// Fetch with timeout
function fetchWithTimeout(url, ms = 8000) {
@@ -59,6 +60,26 @@ function fetchWithTimeout(url, ms = 8000) {
return fetch(url, { signal: controller.signal }).finally(() => clearTimeout(timeout));
}
async function shareContent(title, url, btn) {
if (navigator.share) {
try {
await navigator.share({ title, url });
return;
} catch (e) {
if (e.name === 'AbortError') return;
}
}
try {
await navigator.clipboard.writeText(url);
const orig = btn.innerHTML;
btn.innerHTML = 'Copied!';
btn.classList.add('share-copied');
setTimeout(() => { btn.innerHTML = orig; btn.classList.remove('share-copied'); }, 2000);
} catch (e) {
prompt('Copy this link:', url);
}
}
// Fetch and parse RSS feed
async function fetchEpisodes() {
let xml;
@@ -130,19 +151,30 @@ function createEpisodeCard(ep) {
<div class="episode-title">${escapeAttr(ep.title)}</div>
<div class="episode-desc">${truncate(ep.description, 150)}</div>
${epSlug ? `<a href="/episode.html?slug=${encodeURIComponent(epSlug)}" class="episode-transcript-link">Read Transcript</a>` : ''}
<button class="episode-share-btn" aria-label="Share episode">${shareSVG}</button>
</div>
`;
const btn = card.querySelector('.episode-play-btn');
btn.addEventListener('click', () => playEpisode(ep.audioUrl, ep.title, card, btn));
const shareBtn = card.querySelector('.episode-share-btn');
const shareUrl = epSlug
? `${window.location.origin}/episode.html?slug=${encodeURIComponent(epSlug)}`
: window.location.origin;
shareBtn.addEventListener('click', () => shareContent(ep.title, shareUrl, shareBtn));
return card;
}
function showMoreEpisodes() {
const batch = allEpisodes.slice(displayedCount, displayedCount + EPISODES_PER_PAGE);
batch.forEach((ep) => {
episodesList.appendChild(createEpisodeCard(ep));
batch.forEach((ep, i) => {
const card = createEpisodeCard(ep);
if (displayedCount === 0 && i === 0) {
card.querySelector('.episode-meta').insertAdjacentHTML('afterbegin', '<span class="episode-new-badge">NEW</span> ');
}
episodesList.appendChild(card);
});
displayedCount += batch.length;
@@ -185,6 +217,7 @@ function playEpisode(url, title, card, btn) {
playerTitle.textContent = title;
stickyPlayer.classList.add('active');
if (stickyCta) stickyCta.classList.add('player-active');
}
// Episode card icon sync (sticky player icons handled by player.js)
@@ -295,6 +328,18 @@ function checkOnAir() {
.catch(() => {});
}
// Sticky CTA — show after scrolling past hero
const stickyCta = document.getElementById('sticky-cta');
const heroSection = document.querySelector('.hero');
if (stickyCta && heroSection) {
const ctaObserver = new IntersectionObserver(([entry]) => {
const show = !entry.isIntersecting;
stickyCta.classList.toggle('visible', show);
stickyCta.setAttribute('aria-hidden', String(!show));
}, { threshold: 0 });
ctaObserver.observe(heroSection);
}
// Init
fetchEpisodes();
initTestimonials();
+29
View File
@@ -1,6 +1,7 @@
const CLIPS_JSON_URL = '/data/clips.json';
const clipPlaySVG = '<svg viewBox="0 0 24 24" fill="#fff"><path d="M8 5v14l11-7z"/></svg>';
const clipShareSVG = '<svg viewBox="0 0 24 24" fill="#fff"><path d="M16 5l-1.42 1.42-1.59-1.59V16h-2V4.83L9.42 6.42 8 5l4-4 4 4zm4 5v11a2 2 0 01-2 2H6a2 2 0 01-2-2V10a2 2 0 012-2h3v2H6v11h12V10h-3V8h3a2 2 0 012 2z"/></svg>';
function escapeHTML(str) {
const el = document.createElement('span');
@@ -8,6 +9,26 @@ function escapeHTML(str) {
return el.innerHTML;
}
async function shareClipContent(title, url, btn) {
if (navigator.share) {
try {
await navigator.share({ title, url });
return;
} catch (e) {
if (e.name === 'AbortError') return;
}
}
try {
await navigator.clipboard.writeText(url);
const orig = btn.innerHTML;
btn.innerHTML = 'Copied!';
btn.classList.add('share-copied');
setTimeout(() => { btn.innerHTML = orig; btn.classList.remove('share-copied'); }, 2000);
} catch (e) {
prompt('Copy this link:', url);
}
}
function renderClipCard(clip, featured) {
const card = document.createElement('div');
card.className = 'clip-card' + (featured ? ' clip-card-featured' : '');
@@ -31,6 +52,7 @@ function renderClipCard(clip, featured) {
<h3 class="clip-card-title">${title}</h3>
<p class="clip-card-desc">${desc}</p>
${hasVideo ? `<button class="clip-play-btn" aria-label="Play clip">${clipPlaySVG}</button>` : ''}
${hasVideo ? `<button class="clip-share-btn" aria-label="Share clip">${clipShareSVG}</button>` : ''}
</div>
</div>
`;
@@ -41,6 +63,13 @@ function renderClipCard(clip, featured) {
const inner = card.querySelector('.clip-card-inner');
inner.innerHTML = `<iframe src="https://www.youtube-nocookie.com/embed/${youtubeId}?autoplay=1&rel=0" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>`;
});
const shareBtn = card.querySelector('.clip-share-btn');
if (shareBtn) {
shareBtn.addEventListener('click', (e) => {
e.stopPropagation();
shareClipContent(clip.title || '', `https://youtube.com/watch?v=${youtubeId}`, shareBtn);
});
}
}
return card;
+32
View File
@@ -41,10 +41,42 @@ function initFooter() {
<a href="https://youtube.com/lukemacneil" target="_blank" rel="noopener">YouTube</a>
</div>
</div>
<div class="footer-newsletter">
<p class="footer-newsletter-text">Also from Luke: <strong class="footer-newsletter-name">The Daily AI Briefing</strong> — curated insights on AI infrastructure, automation, and engineering.</p>
<form class="footer-newsletter-form" id="footer-newsletter-form">
<input type="email" class="footer-newsletter-input" placeholder="your@email.com" required aria-label="Email address">
<button type="submit" class="footer-newsletter-btn">Subscribe</button>
</form>
<p class="footer-newsletter-success" id="footer-newsletter-success" hidden>You're subscribed to the Daily AI Briefing.</p>
</div>
<p class="footer-contact"><a href="https://ko-fi.com/lukemacneil" target="_blank" rel="noopener">Support the Show</a></p>
<p class="footer-contact">Sales &amp; Collaboration: <a href="mailto:luke@lukeattheroost.com">luke@lukeattheroost.com</a></p>
<p>&copy; 2026 Luke at the Roost &middot; <a href="/privacy">Privacy Policy</a> &middot; <a href="/terms">Terms of Service</a> &middot; <a href="https://monitoring.macneilmediagroup.com/status/lukeattheroost" target="_blank" rel="noopener">System Status</a></p>
`;
const form = document.getElementById('footer-newsletter-form');
const success = document.getElementById('footer-newsletter-success');
if (form) {
form.addEventListener('submit', async (e) => {
e.preventDefault();
const btn = form.querySelector('.footer-newsletter-btn');
const email = form.querySelector('.footer-newsletter-input').value;
btn.disabled = true;
btn.textContent = 'Subscribing...';
try {
await fetch('https://mmg-form-handler.luke-3b5.workers.dev/api/lead-magnet', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, source: 'lukeattheroost-footer' })
});
form.hidden = true;
success.hidden = false;
} catch {
btn.disabled = false;
btn.textContent = 'Subscribe';
}
});
}
}
initFooter();