How to Build Link Previews from Website Metadata

Rich link previews make any app feel more polished. When a user shares a URL, showing the page title, description, and preview image turns a bare link into an engaging preview card.

In this guide, we'll build a link preview system from scratch — and show how to use a metadata API to skip the hard parts.

What Makes a Good Link Preview

A great link preview has four elements:

  1. Preview image — The hero image that represents the page (usually from og:image)
  2. Title — The page title, ideally the Open Graph title
  3. Description — A concise summary, from meta description or OG description
  4. Domain & favicon — The source domain with its favicon for brand recognition

Step 1: Fetching Metadata with Raw HTML

The traditional approach: fetch the HTML and parse meta tags.

async function getLinkPreview(url) { const response = await fetch(url); const html = await response.text(); // Extract title const titleMatch = html.match(/<title>(.*?)<\/title>/); // Extract OG image const ogImageMatch = html.match( /<meta\s+property="og:image"\s+content="([^"]*)"/ ); // Extract description const descMatch = html.match( /<meta\s+name="description"\s+content="([^"]*)"/ ); return { title: titleMatch?.[1] || null, image: ogImageMatch?.[1] || null, description: descMatch?.[1] || null, }; }

Problems with this approach:

Step 2: Adding JavaScript Rendering

For modern SPAs, you need a headless browser:

import puppeteer from 'puppeteer'; async function getLinkPreview(url) { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url, { waitUntil: 'networkidle2' }); const metadata = await page.evaluate(() => { const getMeta = (property) => { const el = document.querySelector( `meta[property="og:${property}"], meta[name="twitter:${property}"]` ); return el?.getAttribute('content') || null; }; return { title: getMeta('title') || document.title, description: getMeta('description'), image: getMeta('image'), favicon: document.querySelector('link[rel="icon"]')?.href || '/favicon.ico', }; }); await browser.close(); return metadata; }

This works better, but now you're managing headless browser infrastructure — high RAM usage, slow cold starts, and complex scaling.

Step 3: Using a Metadata API

A dedicated API eliminates the infrastructure complexity:

async function getLinkPreview(url) { const response = await fetch('https://api.brandohue.com/api/v1/extract', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.BRANDOHUE_API_KEY}`, }, body: JSON.stringify({ url }), }); const data = await response.json(); return { title: data.ogTitle || data.title, description: data.ogDescription || data.description, image: data.ogImage, favicon: data.faviconUrl, domain: new URL(url).hostname, }; }

One API call gives you:

Step 4: Rendering the Preview Card

Now let's build the UI component:

function LinkPreview({ url }) { const [preview, setPreview] = useState(null); useEffect(() => { getLinkPreview(url).then(setPreview); }, [url]); if (!preview) return <LinkSkeleton />; return ( <div className="link-preview-card"> {preview.image && ( <img src={preview.image} alt="" className="preview-image" /> )} <div className="preview-content"> <div className="preview-domain"> <img src={preview.favicon} alt="" className="favicon" /> {preview.domain} </div> <h3 className="preview-title">{preview.title}</h3> <p className="preview-description">{preview.description}</p> </div> </div> ); }

Edge Cases to Handle

A production link preview system needs to handle:

Brandohue handles all of these automatically — including built-in caching that returns cached data in under 100ms.

Platform-Specific Integrations

Slack Unfurls

app.event('link_shared', async ({ event, client }) => { for (const link of event.links) { const preview = await getLinkPreview(link.url); await client.chat.unfurl({ channel: event.channel, ts: event.message_ts, unfurls: { [link.url]: { title: preview.title, text: preview.description, image_url: preview.image, }, }, }); } });

Discord Embeds

const preview = await getLinkPreview(url); const embed = { title: preview.title, description: preview.description, image: { url: preview.image }, footer: { text: preview.domain, icon_url: preview.favicon }, };

Conclusion

Building link previews from scratch requires parsing HTML, handling JS rendering, managing browser infrastructure, and dealing with edge cases. A metadata API like Brandohue gives you all the preview data you need in one call — with built-in JavaScript rendering, caching, and error handling.

Start building better link previews with 1000 free credits.