You might have seen this "link preview" thing on my website - I implemented it using PHP. But I thought I'd share another approach that's even easier to set up.
This guide will show you how to implement a popover link preview system on your blog in minutes, without a complex backend or heavy plugins, by leveraging Cloudflare Workers (free tier) and a small piece of JavaScript.
The popover preview system has two parts:
- A Cloudflare Worker that fetches and parses web pages
- A frontend script that handles hover events and shows popover previews
The Back-end (Cloudflare Worker)
export default { async fetch(request) { const { searchParams } = new URL(request.url); const targetUrl = searchParams.get("url"); const origin = request.headers.get("Origin"); const allowedOrigin = "https://yourdomain.com"; // Good enough to prevent casual hotlinking. // Build CORS headers only if Origin matches const corsHeaders = { "Content-Type": "application/json", "Access-Control-Allow-Methods": "GET, OPTIONS", "Access-Control-Allow-Headers": "Content-Type", ...(origin === allowedOrigin ? { "Access-Control-Allow-Origin": allowedOrigin } : {}), }; // Handle preflight OPTIONS requests if (request.method === "OPTIONS") { return new Response(null, { headers: corsHeaders }); } if (!targetUrl) { return new Response(JSON.stringify({ error: "No URL provided" }), { status: 400, headers: corsHeaders, }); } // Validate URL let url; try { url = new URL(targetUrl); } catch (e) { return new Response(JSON.stringify({ error: "Invalid URL" }), { status: 400, headers: corsHeaders, }); } let html; try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 10000); const resp = await fetch(url.toString(), { redirect: "follow", signal: controller.signal, headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.9", "Connection": "keep-alive", }, }); clearTimeout(timeout); html = await resp.text(); } catch (e) { return new Response( JSON.stringify({ error: "Failed to fetch the URL (timeout or blocked)" }), { status: 500, headers: corsHeaders } ); } const getMeta = (pattern) => { const match = html.match(pattern); return match ? match[1] : null; }; let title = getMeta(/<title>(.*?)<\/title>/i) || getMeta(/<meta[^>]+property=["']og:title["'][^>]+content=["'](.*?)["']/i) || getMeta(/<meta[^>]+name=["']twitter:title["'][^>]+content=["'](.*?)["']/i); let description = getMeta(/<meta[^>]+name=["']description["'][^>]+content=["'](.*?)["']/i) || getMeta(/<meta[^>]+property=["']og:description["'][^>]+content=["'](.*?)["']/i) || getMeta(/<meta[^>]+name=["']twitter:description["'][^>]+content=["'](.*?)["']/i); let ogImage = getMeta(/<meta[^>]+property=["']og:image["'][^>]+content=["'](.*?)["']/i) || getMeta(/<meta[^>]+name=["']twitter:image["'][^>]+content=["'](.*?)["']/i) || getMeta(/<meta[^>]+name=["']thumbnail["'][^>]+content=["'](.*?)["']/i); if (ogImage && !/^https?:\/\//i.test(ogImage)) { ogImage = `${url.protocol}//${url.host}${ogImage}`; } if (!title) { const h1 = html.match(/<h1[^>]*>(.*?)<\/h1>/i); title = h1 ? h1[1].replace(/<[^>]+>/g, ") : null; } if (!description) { const p = html.match(/<p[^>]*>(.*?)<\/p>/i); description = p ? p[1].replace(/<[^>]+>/g, ") : null; } if (!title && !description && !ogImage) { return new Response(JSON.stringify({ error: "No previewable content found" }), { status: 204, headers: corsHeaders, }); } return new Response( JSON.stringify({ url: targetUrl, title, description, ogImage }), { headers: corsHeaders } ); }, };
The Front-end JS & Popover HTML
<script> async function showPreview(e) { const url = e.target.href; const previewBox = document.getElementById("preview-box"); try { const res = await fetch( "https://your-worker.your-subdomain.workers.dev/?url=" + encodeURIComponent(url) ); if (!res.ok) return; const data = await res.json(); previewBox.innerHTML = ` <div style="display:flex; flex-direction:column; gap:4px;"> <strong>${data.title || "}</strong> <em>${data.description || "}</em> ${data.ogImage ? `<img src="${data.ogImage}" style="width:100%; height:auto;">` : "} </div> `; previewBox.style.display = "block"; const boxWidth = previewBox.offsetWidth; const boxHeight = previewBox.offsetHeight; const pageWidth = window.innerWidth; const pageHeight = window.innerHeight; // Determine horizontal position let left = e.pageX + 10; if (left + boxWidth > pageWidth) { left = e.pageX - boxWidth - 10; if (left < 0) left = 10; } // Determine vertical position let top = e.pageY + 10; if (top + boxHeight > pageHeight) { top = pageHeight - boxHeight - 10; if (top < 0) top = 10; } previewBox.style.left = left + "px"; previewBox.style.top = top + "px"; } catch (err) { console.error(err); } } function hidePreview() { document.getElementById("preview-box").style.display = "none"; } // Only select links inside specific container(s) const containerSelector = "main"; // main works better for bearblog.dev users document.querySelectorAll(`${containerSelector} a`).forEach(link => { link.addEventListener("mouseover", showPreview); link.addEventListener("mouseout", hidePreview); }); </script>
<div id="preview-box" style=" position:absolute; display:none; padding:8px; background:#fff; border:1px solid #ccc; z-index:999; width:300px; box-sizing:border-box; word-wrap:break-word; overflow-wrap:break-word; "></div>
How To Set It Up
Step 1: Create a Cloudflare Worker
- Sign up for Cloudflare (if you don't have an account) at cloudflare.com
- Go to Workers & Pages in your Cloudflare dashboard
- Click "Create a Worker"
- Delete the default code and paste the worker code from
worker.js
- Inside the code, you’ll see:
const allowedOrigin = "https://yourdomain.com";
- Update
https://yourdomain.com
to your own domain.
- Inside the code, you’ll see:
- Click "Save and Deploy"
- Copy your worker URL (it will look like
https://your-worker.your-subdomain.workers.dev
)
Step 2: Add the Frontend Script + HTML snippet
- Open your website's header or footer template
- Add the JavaScript code and HTML snippet from
client.html
to your page - Update the worker URL in the JavaScript:
const res = await fetch( "https://your-worker.your-subdomain.workers.dev/?url=" + encodeURIComponent(url) );
- Add the preview box HTML to your page:
<!-- customize this --> <div id="preview-box" style=" position:absolute; display:none; padding:8px; background:#fff; border:1px solid #ccc; z-index:999; width:300px; box-sizing:border-box; word-wrap:break-word; overflow-wrap:break-word; "></div>
Step 3: Test It
- Save your changes and refresh your page
- Hover over any link - you should see a popover preview appear
- The front-end script is designed to be easily customizable. So make it your own.
Have fun.
Note: The frontend JS for this guide has only been tested for compatibility with bearblog.dev. Users on platforms like WordPress, Ghost, or static sites may need to adjust the containerSelector variable in the script.