DEV Community

Davis
Davis

Posted on

3 ways to add link previews to a React app (with and without a backend)

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
import { LinkPeek } from "linkpeek-client";
const lp = new LinkPeek({ rapidApiKey: process.env.RAPIDAPI_KEY });
const meta = await lp.preview("https://github.com");
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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:image and favicons that need resolving
  • Fallback chainsog:titletwitter: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)

Collapse
 
frank_signorini profile image
Frank

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.