The architecture behind those URL cards in Slack, Discord, and every messaging app.
When you paste a URL in Slack, Discord, iMessage, or Twitter, a preview card appears — a title, description, thumbnail, and sometimes a site icon. These link previews are one of the most common uses of screenshot APIs and OG tag parsing. Here's how to build your own.
The typical flow when a user pastes a URL:
og:title, og:description, og:image)import { JSDOM } from 'jsdom';
async function getOGTags(url) {
const response = await fetch(url, {
headers: { 'User-Agent': 'LinkPreviewBot/1.0' },
signal: AbortSignal.timeout(10000),
});
const html = await response.text();
const dom = new JSDOM(html);
const doc = dom.window.document;
const getMeta = (property) =>
doc.querySelector(`meta[property="${property}"]`)?.content ||
doc.querySelector(`meta[name="${property}"]`)?.content;
return {
title: getMeta('og:title') || doc.title || url,
description: getMeta('og:description') || getMeta('description') || '',
image: getMeta('og:image') || null,
siteName: getMeta('og:site_name') || new URL(url).hostname,
favicon: `https://www.google.com/s2/favicons?domain=${new URL(url).hostname}&sz=32`,
};
}
Not every page has OG tags. For pages without an og:image, generate a thumbnail screenshot:
async function getScreenshotThumbnail(url) {
const response = await fetch(
'https://api.nightglass.xyz/api/v1/screenshot',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.NIGHTGLASS_KEY}`,
},
body: JSON.stringify({
url,
width: 1200,
height: 630,
format: 'jpeg',
quality: 75,
}),
}
);
return Buffer.from(await response.arrayBuffer());
}
async function getLinkPreview(url) {
const og = await getOGTags(url);
// If no OG image, generate a screenshot
if (!og.image) {
const screenshot = await getScreenshotThumbnail(url);
const screenshotUrl = await uploadToStorage(screenshot, `previews/${hash(url)}.jpg`);
og.image = screenshotUrl;
}
return og;
}
Link previews should be cached aggressively. Once generated, the preview for a URL rarely changes. Use a two-tier cache:
Key by URL hash. When a user pastes a URL, check cache first. If miss, generate in the background and return a placeholder immediately — don't block the message send on preview generation.
For apps with many users pasting URLs, preview generation should be async. When a URL is pasted, enqueue a job to generate the preview. The message renders immediately with the URL text, and the preview card appears a few seconds later when the job completes.
This prevents slow or broken URLs from blocking the user experience. If a page takes 15 seconds to load, the user doesn't wait — the preview arrives when it's ready.
Your preview service will be called for every URL your users paste. Consider: rate limiting by user (prevent spam), URL validation (block internal/private IPs), timeout handling (don't wait forever for slow pages), and content filtering (don't generate previews for known malicious domains).
nightglass handles the hard part. The screenshot generation, browser management, and rendering complexity is handled by the API. Your link preview service just needs to do OG tag parsing and API calls. See the quickstart guide to get set up.
If generating custom social media OG images from templates, the OG image guide covers the HTML template + screenshot pattern. For full-page captures, see the full-page screenshot guide. Evaluating which API to use? 2026 API comparison.