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

javascript dev.to

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.

Source: dev.to

arrow_back Back to Tutorials