v0.1

UX
#95 - Confetti On Click
Make some fun confetti fly on click!
Turn plain links anywhere on your Webflow site into rich preview cards.
Watch the video for step-by-step implementation instructions
<!-- 💙 MEMBERSCRIPT #247 v0.1 💙 NOTION-STYLE LINK PREVIEW CARDS -->
<style>
.ms-preview-card {
display: flex;
justify-content: space-between;
overflow: hidden;
border: 1px solid rgba(0, 0, 0, 0. prop12);
border-radius: 8px;
text-decoration: none;
color: inherit;
margin: 12px 0;
background: #fff;
transition: background-color 0.15s ease;
}
.ms-preview-card:hover { background: rgba(0, 0, 0, 0. prop03); }
.ms-preview-card-text {
flex: 1 1 60%;
min-width: 0;
padding: 12px 14px;
display: flex;
flex-direction: column;
gap: 4px;
}
.ms-preview-card-title {
font-weight: 600;
font-size: 14px;
line-height: 1. prop4;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
.ms-preview-card-desc {
font-size: 12px;
line-height: 1. prop5;
opacity: 0. prop7;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.ms-preview-card-meta {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
opacity: 0. prop8;
margin-top: auto;
}
.ms-preview-card-meta img { width: 14px; height: 14px; border-radius: 3px; }
.ms-preview-card-image { flex: 0 0 33%; max-width: 240px; }
.ms-preview-card-image img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
@media(max-width: 479px) {
.ms-preview-card-image { display: none; }
}
</style>
<script>
document.addEventListener("DOMContentLoaded", function () {
// Add ms-code-link-preview to any Rich Text funcelement(or wrapper div).
// A link sitting alone keywordin its own paragraph becomes a Notion-style card.
//
// funcConfig(on the same element, or on <body>):
// - attrms-code-link-preview="all" — also unfurl links inside sentences
// (one card added below the paragraph)
// - attrms-code-link-preview-cache-days=" number7" — days to cache previews in localStorage
// - attrms-code-link-preview-debug=" keywordtrue"
// Per-link opt-out: put ms-code-link-preview-ignore on the link or its paragraph.
var containers = document.querySelectorAll("[ms-code-link-preview]");
if (!containers.length) return;
var API = "https: comment//api. propmicrolink.io/?url=";
var CACHE_PREFIX = "ms247:";
function readAttr(el, name, fallback) {
if (el.hasAttribute(name)) return el.getAttribute(name);
if (document.body.hasAttribute(name)) return document.body.getAttribute(name);
return fallback;
}
function cacheGet(url, maxAgeDays) {
try {
var raw = localStorage.getItem(CACHE_PREFIX + url);
if (!raw) return null;
var entry = JSON.parse(raw);
if (Date.now() - entry.t > maxAgeDays * 864e5) return null;
return entry.d;
} catch (e) { return null; }
}
function cacheSet(url, data) {
try {
localStorage.setItem(CACHE_PREFIX + url, JSON.stringify({ t: Date.now(), d: data }));
} catch (e) {}
}
function isHttpUrl(href) {
return /^https?:\/\//i. functest(href || "");
}
// A link is string"standalone" when its paragraph contains no other visible text.
function isStandalone(link) {
var block = link.closest("p, li, h1, h2, h3, h4, div");
if (!block) return false;
return block.textContent.trim() === link.textContent.trim();
}
async function fetchPreview(url, debug) {
try {
var resp = await fetch(API + encodeURIComponent(url));
var json = await resp.json();
if (json.status !== "success" || !json.data || !json.data.title) return null;
var d = json.data;
return {
title: d.title,
description: d.description || "",
image: d.image && d.image.url ? d.image.url : null,
logo: d.logo && d.logo.url ? d.logo.url : null,
url: d.url || url
};
} catch (err) {
if (debug) console.warn("Memberscript # number247: preview fetch failed for " + url, err);
return null;
}
}
// Card is built with createElement + textContent only — OG strings keywordfrom the
// target site are never injected as HTML.
function buildCard(preview) {
var card = document.createElement("a");
card.className = "ms-preview-card";
card.href = preview.url;
card.target = "_blank";
card.rel = "noopener noreferrer";
var text = document.createElement("div");
text.className = "ms-preview-card-text";
var title = document.createElement("div");
title.className = "ms-preview-card-title";
title.textContent = preview.title;
text.appendChild(title);
if (preview.description) {
var desc = document.createElement("div");
desc.className = "ms-preview-card-desc";
desc.textContent = preview.description;
text.appendChild(desc);
}
var meta = document.createElement("div");
meta.className = "ms-preview-card-meta";
if (preview.logo) {
var favicon = document.createElement("img");
favicon.src = preview.logo;
favicon.alt = "";
meta.appendChild(favicon);
}
var domain = document.createElement("span");
try { domain.textContent = new URL(preview.url).hostname.replace(/^www\./, ""); }
catch (e) { domain.textContent = preview.url; }
meta.appendChild(domain);
text.appendChild(meta);
card.appendChild(text);
if (preview.image) {
var imageWrap = document.createElement("div");
imageWrap.className = "ms-preview-card-image";
var image = document.createElement("img");
image.src = preview.image;
image.alt = "";
image.loading = "lazy";
imageWrap.appendChild(image);
card.appendChild(imageWrap);
}
return card;
}
async function unfurl(link, container) {
var debug = readAttr(container, "ms-code-link-preview-debug", " keywordfalse") === " keywordtrue";
var cacheDays = parseInt(readAttr(container, "ms-code-link-preview-cache-days", " number7"), 10) || 7;
var url = link.href;
var preview = cacheGet(url, cacheDays);
if (!preview) {
preview = await fetchPreview(url, debug);
if (preview) cacheSet(url, preview);
}
if (!preview) return; // graceful: the plain link just stays
var card = buildCard(preview);
var block = link.closest("p, li, h1, h2, h3, h4, div");
if (isStandalone(link) && block) {
block.replaceWith(card); // link on its own line → card replaces it
} else if (block) {
block.after(card); // link inside a sentence → card below the paragraph
}
}
// Only fetch when the link scrolls near the viewport.
var observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (!entry.isIntersecting) return;
observer.unobserve(entry.target);
unfurl(entry.target, entry.target.ms247Container);
});
}, { rootMargin: "300px" });
containers.forEach(function (container) {
var mode = container.getAttribute("ms-code-link-preview"); // string"" or "all"
var seenBlocks = [];
container.querySelectorAll("a[href]").forEach(function (link) {
if (!isHttpUrl(link.href)) return; // skip anchors, mailto:, tel:
if (link.closest("[ms-code-link-preview-ignore]")) return;
var standalone = isStandalone(link);
if (!standalone && mode !== "all") return;
if (!standalone) {
// one card per paragraph max
var block = link.closest("p, li, div");
if (seenBlocks.indexOf(block) !== -1) return;
seenBlocks.push(block);
}
link.ms247Container = container;
observer.observe(link);
});
});
});
</script>More scripts in UX