reduce bounce rate
Some checks failed
ci / site (push) Has been cancelled
publish-image / publish (push) Has been cancelled

This commit is contained in:
2026-02-10 17:36:34 -05:00
parent ac3de3e142
commit 5d07e57256
15 changed files with 992 additions and 90 deletions

View File

@@ -20,17 +20,10 @@ const { categories, activeCategorySlug } = Astro.props;
>
All
</a>
<a
class={activeCategorySlug === "__pages" ? "active" : ""}
href="/blog/pages"
data-umami-event="click"
data-umami-event-target_id="blog.subnav.pages"
data-umami-event-placement="blog.subnav"
data-umami-event-target_url="/blog/pages"
>
Pages
</a>
{categories.map((c) => (
{categories
.filter((c) => c.slug !== "uncategorized")
.map((c) => (
<a
class={activeCategorySlug === c.slug ? "active" : ""}
href={`/blog/category/${c.slug}`}

View File

@@ -35,26 +35,55 @@ const umamiType =
: undefined;
const umamiTitle = umamiType ? truncate(item.title, 160) : undefined;
// Determine if card should open modal (youtube/podcast) or link normally (other sources)
const isModalTrigger = item.source === "youtube" || item.source === "podcast";
const mode = isModalTrigger ? "modal" : "link";
// Build link attrs based on mode
const linkAttrs = isModalTrigger
? {
// Modal trigger: pass all item data via data-* attributes
"data-item-id": item.id,
"data-item-source": item.source,
"data-item-url": item.url,
"data-item-title": item.title,
"data-item-summary": item.summary || "",
"data-item-published-at": item.publishedAt,
"data-item-thumbnail-url": item.thumbnailUrl || "",
"data-item-audio-url": item.source === "podcast" ? item.audioUrl || "" : "",
"data-item-views": item.metrics?.views?.toString() || "",
// Umami tracking: media_preview instead of outbound_click
"data-umami-event": "media_preview",
"data-umami-event-target_id": targetId,
"data-umami-event-placement": placement,
"data-umami-event-title": umamiTitle,
"data-umami-event-type": umamiType,
"data-umami-event-source": item.source,
}
: {
// Normal link: outbound_click tracking
"data-umami-event": "outbound_click",
"data-umami-event-target_id": targetId,
"data-umami-event-placement": placement,
"data-umami-event-target_url": item.url,
"data-umami-event-title": umamiTitle,
"data-umami-event-type": umamiType,
"data-umami-event-domain": domain || "unknown",
"data-umami-event-source": item.source,
"data-umami-event-ui_placement": "content_card",
};
---
<StandardCard
href={item.url}
href={isModalTrigger ? undefined : item.url}
title={item.title}
summary={item.summary}
imageUrl={item.thumbnailUrl}
dateLabel={dateLabel}
viewsLabel={item.metrics?.views !== undefined ? `${item.metrics.views.toLocaleString()} views` : undefined}
sourceLabel={item.source}
isExternal={true}
linkAttrs={{
"data-umami-event": "outbound_click",
"data-umami-event-target_id": targetId,
"data-umami-event-placement": placement,
"data-umami-event-target_url": item.url,
"data-umami-event-title": umamiTitle,
"data-umami-event-type": umamiType,
"data-umami-event-domain": domain || "unknown",
"data-umami-event-source": item.source,
"data-umami-event-ui_placement": "content_card",
}}
isExternal={!isModalTrigger}
mode={mode}
linkAttrs={linkAttrs}
/>

View File

@@ -0,0 +1,398 @@
---
import { LINKS } from "../lib/links";
---
<dialog id="media-modal" aria-modal="true" aria-labelledby="media-modal-title">
<div class="media-modal-content">
<div class="media-modal-header">
<div class="media-modal-header-left">
<img class="media-modal-thumb" alt="" loading="lazy" />
<div class="media-modal-title-wrap">
<div class="media-modal-kicker"></div>
<h2 id="media-modal-title" class="media-modal-title"></h2>
</div>
</div>
<button type="button" class="media-modal-close" aria-label="Close" data-close-method="button">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="media-modal-embed-container" data-embed-kind="">
<div class="media-modal-embed-placeholder">
<a class="media-modal-embed-fallback" target="_blank" rel="noopener noreferrer"></a>
</div>
<iframe
class="media-modal-embed"
title="Media embed"
width="100%"
frameborder="0"
allowfullscreen
></iframe>
<audio class="media-modal-audio" controls></audio>
</div>
<div class="media-modal-body">
<p class="media-modal-description"></p>
<div class="media-modal-meta">
<span class="media-modal-date"></span>
<span class="media-modal-views"></span>
</div>
</div>
<div class="media-modal-ctas">
<a class="cta primary media-modal-cta-follow" target="_blank" rel="me noopener noreferrer"></a>
<a class="cta media-modal-cta-view" target="_blank" rel="me noopener noreferrer"></a>
</div>
</div>
</dialog>
<script is:inline define:vars={{ youtubeChannelUrl: LINKS.youtubeChannel, podcastUrl: LINKS.podcast }}>
(function() {
const dialog = document.getElementById("media-modal");
const dialogContent = dialog.querySelector(".media-modal-content");
const kickerEl = dialog.querySelector(".media-modal-kicker");
const thumbEl = dialog.querySelector(".media-modal-thumb");
const titleEl = dialog.querySelector(".media-modal-title");
const embedContainer = dialog.querySelector(".media-modal-embed-container");
const embedPlaceholder = dialog.querySelector(".media-modal-embed-placeholder");
const embedFallback = dialog.querySelector(".media-modal-embed-fallback");
const iframe = dialog.querySelector(".media-modal-embed");
const audio = dialog.querySelector(".media-modal-audio");
const descriptionEl = dialog.querySelector(".media-modal-description");
const dateEl = dialog.querySelector(".media-modal-date");
const viewsEl = dialog.querySelector(".media-modal-views");
const followCta = dialog.querySelector(".media-modal-cta-follow");
const viewCta = dialog.querySelector(".media-modal-cta-view");
const closeBtn = dialog.querySelector(".media-modal-close");
let triggerElement = null;
let currentTargetId = null;
// Extract video ID from YouTube URL
function extractYoutubeId(url) {
try {
const u = new URL(url);
// Handle youtube.com/watch?v=ID
if (u.hostname.includes("youtube.com") && u.searchParams.has("v")) {
return u.searchParams.get("v");
}
// Handle youtu.be/ID
if (u.hostname.includes("youtu.be")) {
return u.pathname.slice(1).split("/")[0];
}
} catch (e) {}
return null;
}
// Extract Spotify episode ID from URL
function extractSpotifyEpisodeId(input) {
try {
const u = new URL(String(input));
if (!u.hostname.includes("spotify.com")) return null;
const match = u.pathname.match(/\/episode\/([^/?]+)/);
return match ? match[1] : null;
} catch (e) {}
return null;
}
function extractSpotifyEpisodeIdFromGuid(guid) {
const s = String(guid || "").trim();
if (!s) return null;
// spotify:episode:EPISODE_ID
if (s.startsWith("spotify:episode:")) {
const parts = s.split(":");
return parts.length >= 3 ? parts[2] : null;
}
// URL form
const fromUrl = extractSpotifyEpisodeId(s);
if (fromUrl) return fromUrl;
return null;
}
function getEmbed(source, itemUrl, itemId) {
if (source === "youtube") {
const videoId = extractYoutubeId(itemUrl);
if (!videoId) return null;
return {
kind: "youtube",
src: `https://www.youtube.com/embed/${videoId}?rel=0&modestbranding=1`,
};
}
if (source === "podcast") {
const episodeId =
extractSpotifyEpisodeId(itemUrl) ||
extractSpotifyEpisodeIdFromGuid(itemId) ||
extractSpotifyEpisodeId(itemId);
if (!episodeId) return null;
return {
kind: "spotify",
src: `https://open.spotify.com/embed/episode/${episodeId}`,
height: 232,
};
}
return null;
}
// Add UTM parameters to URL
function withUtm(url, utmParams) {
const u = new URL(url);
for (const [key, value] of Object.entries(utmParams)) {
if (value) u.searchParams.set(key, value);
}
return u.toString();
}
function withYoutubeSubscribePrompt(url) {
try {
const u = new URL(url);
u.searchParams.set("sub_confirmation", "1");
return u.toString();
} catch {
return url;
}
}
// Stop playback by blanking the iframe
function stopPlayback() {
iframe.src = "about:blank";
embedPlaceholder.style.display = "block";
embedContainer.dataset.embedKind = "";
iframe.style.display = "";
embedFallback.style.display = "none";
embedFallback.removeAttribute("href");
audio.pause();
audio.removeAttribute("src");
audio.style.display = "none";
thumbEl.removeAttribute("src");
thumbEl.style.display = "none";
kickerEl.textContent = "";
iframe.removeAttribute("allow");
iframe.removeAttribute("height");
}
// Close modal
function closeModal(method) {
if (!dialog.open) return;
stopPlayback();
dialog.close();
// Emit media_preview_close event if Umami is available
if (typeof window.umami !== "undefined" && currentTargetId) {
window.umami.track("media_preview_close", {
target_id: currentTargetId,
close_method: method || "unknown"
});
}
// Restore focus to trigger element
if (triggerElement) {
triggerElement.focus();
triggerElement = null;
}
currentTargetId = null;
}
// Open and populate modal
function openModal(cardEl) {
// Store trigger for focus return
triggerElement = cardEl;
// Read data attributes
const itemId = cardEl.dataset.itemId;
const source = cardEl.dataset.itemSource;
const url = cardEl.dataset.itemUrl;
const audioUrl = cardEl.dataset.itemAudioUrl;
const title = cardEl.dataset.itemTitle;
const summary = cardEl.dataset.itemSummary;
const publishedAt = cardEl.dataset.itemPublishedAt;
const thumbnailUrl = cardEl.dataset.itemThumbnailUrl;
const views = cardEl.dataset.itemViews;
// Store target ID for close tracking
currentTargetId = cardEl.dataset.umamiEventTargetId;
kickerEl.textContent = source === "podcast" ? "Podcast" : source === "youtube" ? "Video" : "";
if (source === "podcast" && thumbnailUrl) {
thumbEl.src = thumbnailUrl;
thumbEl.style.display = "block";
} else {
thumbEl.removeAttribute("src");
thumbEl.style.display = "none";
}
// Populate title
titleEl.textContent = title || "";
// Populate description
descriptionEl.textContent = summary || "";
// Populate date
if (publishedAt) {
const d = new Date(publishedAt);
const formatted = Number.isFinite(d.valueOf())
? d.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" })
: "";
dateEl.textContent = formatted;
dateEl.style.display = formatted ? "" : "none";
} else {
dateEl.style.display = "none";
}
// Populate views
if (views && views !== "") {
const viewsNum = parseInt(views, 10);
viewsEl.textContent = `${viewsNum.toLocaleString()} views`;
viewsEl.style.display = "";
} else {
viewsEl.style.display = "none";
}
// Construct embed
const embed = getEmbed(source, url, itemId);
if (embed) {
embedContainer.dataset.embedKind = embed.kind;
embedPlaceholder.style.display = "block";
embedFallback.style.display = "none";
embedFallback.removeAttribute("href");
iframe.style.display = "";
if (embed.kind === "spotify") {
iframe.setAttribute(
"allow",
"autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture",
);
if (embed.height) iframe.setAttribute("height", String(embed.height));
} else {
iframe.setAttribute(
"allow",
"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",
);
iframe.removeAttribute("height");
}
iframe.src = embed.src;
iframe.addEventListener("load", function onLoad() {
embedPlaceholder.style.display = "none";
iframe.removeEventListener("load", onLoad);
});
embedContainer.style.display = "block";
} else {
// No embed available
if (source === "podcast") {
embedContainer.style.display = "block";
iframe.src = "about:blank";
iframe.style.display = "none";
if (audioUrl) {
embedContainer.dataset.embedKind = "audio";
embedPlaceholder.style.display = "none";
embedFallback.style.display = "none";
audio.style.display = "block";
audio.src = audioUrl;
} else {
embedContainer.dataset.embedKind = "fallback";
embedPlaceholder.style.display = "flex";
embedFallback.style.display = "inline-flex";
embedFallback.href = url;
embedFallback.textContent = "Listen on Spotify";
audio.style.display = "none";
}
} else {
embedContainer.style.display = "none";
}
}
const platform = source === "youtube" ? "youtube" : "spotify";
const channelUrl = source === "youtube" ? youtubeChannelUrl : podcastUrl;
const followAction = source === "youtube" ? "subscribe" : "follow";
const viewAction = source === "youtube" ? "view" : "listen";
const followLabel = source === "youtube" ? "Subscribe on YouTube" : "Follow on Spotify";
const viewLabel = source === "youtube" ? "View on YouTube" : "Listen on Spotify";
const followBaseUrl = source === "youtube" ? withYoutubeSubscribePrompt(channelUrl) : channelUrl;
const followUrl = withUtm(followBaseUrl, {
utm_source: "website",
utm_medium: "cta",
utm_campaign: "social-acquisition",
utm_content: `${platform}:media_modal`,
});
followCta.href = followUrl;
followCta.textContent = followLabel;
followCta.setAttribute("data-umami-event", "cta_click");
followCta.setAttribute("data-umami-event-target_id", `modal.cta.${followAction}.${platform}`);
followCta.setAttribute("data-umami-event-placement", "media_modal");
followCta.setAttribute("data-umami-event-platform", platform);
followCta.setAttribute("data-umami-event-target_url", channelUrl);
const viewUrl = withUtm(url, {
utm_source: "website",
utm_medium: "cta",
utm_campaign: "social-acquisition",
utm_content: `${platform}:media_modal`,
});
viewCta.href = viewUrl;
viewCta.textContent = viewLabel;
viewCta.setAttribute("data-umami-event", "cta_click");
viewCta.setAttribute("data-umami-event-target_id", `modal.cta.${viewAction}.${platform}`);
viewCta.setAttribute("data-umami-event-placement", "media_modal");
viewCta.setAttribute("data-umami-event-platform", platform);
viewCta.setAttribute("data-umami-event-target_url", url);
// Open the dialog
dialog.showModal();
}
// Listen for clicks on modal-trigger cards
document.addEventListener("click", function(e) {
const card = e.target.closest("button.card[data-item-id]");
if (card) {
e.preventDefault();
openModal(card);
}
});
// Close button
closeBtn.addEventListener("click", function() {
closeModal("button");
});
// Backdrop click
dialog.addEventListener("click", function(e) {
if (e.target === dialog) {
closeModal("backdrop");
}
});
// Escape key (native dialog handles the close, we hook into the close event)
dialog.addEventListener("close", function() {
// If dialog was closed but we didn't call closeModal explicitly (i.e., Escape key)
if (dialog.open === false && currentTargetId) {
stopPlayback();
if (typeof window.umami !== "undefined") {
window.umami.track("media_preview_close", {
target_id: currentTargetId,
close_method: "escape"
});
}
if (triggerElement) {
triggerElement.focus();
triggerElement = null;
}
currentTargetId = null;
}
});
})();
</script>

View File

@@ -1,6 +1,6 @@
---
type Props = {
href: string;
href?: string;
title: string;
summary?: string;
imageUrl?: string;
@@ -9,6 +9,7 @@ type Props = {
sourceLabel: string;
isExternal?: boolean;
linkAttrs?: Record<string, any>;
mode?: "link" | "modal";
};
const {
@@ -21,6 +22,7 @@ const {
sourceLabel,
isExternal,
linkAttrs,
mode = "link",
} = Astro.props;
function truncate(s: string, n: number) {
@@ -33,14 +35,21 @@ function truncate(s: string, n: number) {
const summaryText = truncate(summary || "", 180);
const Element = mode === "modal" ? "button" : "a";
const elementProps = mode === "modal"
? { type: "button", ...linkAttrs }
: {
href,
target: isExternal ? "_blank" : undefined,
rel: isExternal ? "noopener noreferrer" : undefined,
...linkAttrs
};
---
<a
<Element
class="card"
href={href}
target={isExternal ? "_blank" : undefined}
rel={isExternal ? "noopener noreferrer" : undefined}
{...(linkAttrs || {})}
{...elementProps}
>
<div class="card-media">
{imageUrl ? (
@@ -66,4 +75,4 @@ const summaryText = truncate(summary || "", 180);
<span class={`pill pill-${sourceLabel}`}>{sourceLabel}</span>
</div>
</div>
</a>
</Element>

View File

@@ -12,6 +12,7 @@ export type ContentItem = {
summary?: string;
publishedAt: string; // ISO-8601
thumbnailUrl?: string;
audioUrl?: string;
metrics?: ContentMetrics;
};

View File

@@ -26,6 +26,7 @@ export function normalizePodcastFeedItems(items: any[], limit: number): ContentI
const out = (items || []).slice(0, limit).map((it) => {
const url = it.link || "";
const id = (it.guid || it.id || url).toString();
const audioUrl = (it.enclosure?.url || "").toString();
const publishedAt = (it.isoDate || it.pubDate || new Date(0).toISOString()).toString();
const summary = truncate(
(it.contentSnippet ||
@@ -44,6 +45,7 @@ export function normalizePodcastFeedItems(items: any[], limit: number): ContentI
summary: summary || undefined,
publishedAt: new Date(publishedAt).toISOString(),
thumbnailUrl: (it.itunes?.image || undefined) as string | undefined,
audioUrl: audioUrl || undefined,
};
});

View File

@@ -1,5 +1,5 @@
export const LINKS = {
youtubeChannel: "https://www.youtube.com/santhoshj",
instagramProfile: "https://www.instagram.com/santhoshjanan/",
podcast: "https://podcasters.spotify.com/pod/show/irregularmind", // default; override in CTA props if needed
podcast: "https://creators.spotify.com/pod/profile/the-irregular-mind/",
};

View File

@@ -3,6 +3,7 @@ import BaseLayout from "../layouts/BaseLayout.astro";
import CtaLink from "../components/CtaLink.astro";
import ContentCard from "../components/ContentCard.astro";
import InstagramEmbed from "../components/InstagramEmbed.astro";
import MediaModal from "../components/MediaModal.astro";
import { readContentCache } from "../lib/content/cache";
import {
newestItems,
@@ -240,4 +241,6 @@ const newYorkTime = new Date(cache.generatedAt).toLocaleString('en-US', options)
<CtaLink platform="podcast" placement="footer_cta" url={LINKS.podcast} label="Podcast" />
</div>
</section>
<MediaModal />
</BaseLayout>

View File

@@ -1,6 +1,7 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
import ContentCard from "../components/ContentCard.astro";
import MediaModal from "../components/MediaModal.astro";
import { readContentCache } from "../lib/content/cache";
import { podcastEpisodes } from "../lib/content/selectors";
@@ -35,4 +36,6 @@ const episodes = podcastEpisodes(cache).sort(
)
}
</section>
<MediaModal />
</BaseLayout>

View File

@@ -1,6 +1,7 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
import ContentCard from "../components/ContentCard.astro";
import MediaModal from "../components/MediaModal.astro";
import { readContentCache } from "../lib/content/cache";
import { youtubeVideos } from "../lib/content/selectors";
@@ -35,4 +36,6 @@ const videos = youtubeVideos(cache).sort(
)
}
</section>
<MediaModal />
</BaseLayout>