Link previews (those little cards with a title, image, and description) make any list of URLs dramatically more scannable. But you can't build them purely client-side: fetching another site's HTML from the browser is blocked by CORS, and even if it weren't, you'd be shipping a full HTML parser to every visitor.
You need something server-side. Here are three ways to do it in a React app, from zero-backend to full DIY.
Option 1: Zero backend — call a metadata API from the client
If your app has no server (static SPA, GitHub Pages, etc.), use a metadata extraction API that supports CORS. I built LinkPeek for exactly this — it returns title, description, image, favicon, site name, OpenGraph and Twitter Card data as JSON.
import { useEffect, useState } from "react";
function LinkCard({ url }) {
const [meta, setMeta] = useState(null);
useEffect(() => {
let alive = true;
fetch(`https://linkpeek.dpears.workers.dev/v1/preview?url=${encodeURIComponent(url)}`)
.then(r => r.json())
.then(data => alive && setMeta(data))
.catch(() => alive && setMeta({ error: true }));
return () => { alive = false; };
}, [url]);
if (!meta) return <div className="link-card skeleton" />;
if (meta.error || !meta.title) return <a href={url}>{url}</a>;
return (
<a href={url} className="link-card" target="_blank" rel="noopener noreferrer">
{meta.image && <img src={meta.image} alt="" loading="lazy" />}
<div>
<strong>{meta.title}</strong>
<p>{meta.description?.slice(0, 140)}</p>
<small>
{meta.favicon && <img src={meta.favicon} width="14" height="14" alt="" />}
{meta.siteName ?? new URL(url).hostname}
</small>
</div>
</a>
);
}
With some card CSS (flex row, rounded corners, muted description text) this renders a Slack-style unfurl. The anonymous tier allows 25 requests a day per IP — enough for development. For production there's a free plan (500/month) and paid tiers via RapidAPI, plus a tiny typed client on npm:
npm i linkpeek-client
import { LinkPeek } from "linkpeek-client";
const lp = new LinkPeek({ rapidApiKey: process.env.RAPIDAPI_KEY });
const meta = await lp.preview("https://github.com");
Option 2: Next.js — fetch server-side, render static
If you're on Next.js, do the extraction at build/request time so the client ships zero fetch logic:
// app/components/LinkCard.jsx (Server Component)
export default async function LinkCard({ url }) {
const res = await fetch(
`https://linkpeek.dpears.workers.dev/v1/preview?url=${encodeURIComponent(url)}`,
{ next: { revalidate: 86400 } } // cache 24h
);
const meta = await res.json();
// ...same JSX as above, no useState/useEffect needed
}
Server components + revalidate give you cached previews with no client waterfall — the card is in the initial HTML, which also means it works in RSS readers and with JS disabled.
Option 3: Full DIY
Rolling your own is a fun weekend project. The core is simple — fetch the page, parse <meta property="og:*"> tags — but production hardening is where the time goes:
- Redirects & canonical URLs (short links, tracking wrappers)
- Bot blocking — many big sites serve challenge pages to datacenter IPs
-
Relative URLs in
og:imageand favicons that need resolving -
Fallback chains —
og:title→twitter:title→<title> -
SSRF protection — if you fetch user-supplied URLs, you MUST block
localhost, RFC-1918 ranges, and internal hostnames, or your preview endpoint is a proxy into your own infrastructure - Caching — you do not want to re-fetch a URL on every render
- Rate limiting — a public preview endpoint will be abused
I wrote up the sharpest edge I hit (distributed rate limiting on eventually-consistent storage) here, and the whole extractor is open source (MIT) if you want to read a working implementation: github.com/daviscodesbugs/linkpeek.
Which to pick?
| Situation | Pick |
|---|---|
| Static site / SPA, no backend | Option 1 (client fetch) |
| Next.js / Remix / server rendering | Option 2 (server fetch + cache) |
| Learning project, or very high volume | Option 3 (DIY) |
Questions welcome — happy to go deeper on any of these in the comments.
Top comments (1)
I've used the Open Graph protocol for link previews, but how do you handle cases where the metadata isn't available? Would love to swap ideas on this, following for more React tips.