How to Build a Link Preview Component in React
Tutorial4 min read

How to Build a Link Preview Component in React

Step-by-step tutorial on creating a beautiful link preview component in React. Learn to fetch metadata, handle loading states, and display rich previews.

Katsau

Katsau Team

December 22, 2025

Share:

Link previews are everywhere - Slack, Discord, Twitter, LinkedIn. They transform boring URLs into rich, clickable cards that show the page's title, description, and image. In this tutorial, we'll build one from scratch in React.

What We're Building

By the end of this tutorial, you'll have a reusable LinkPreview component that:

  • Fetches metadata from any URL
  • Shows a loading skeleton while fetching
  • Displays title, description, image, and favicon
  • Handles errors gracefully
  • Looks great on mobile and desktop

The Challenge with Link Previews

You can't just fetch a URL from the browser - CORS policies will block you. You need a backend service to fetch the page, parse the HTML, and extract the metadata.

You have two options:

  1. Build your own backend - Parse HTML, handle edge cases, maintain infrastructure
  2. Use a metadata API - One API call, get all the data you need

We'll use Katsau's metadata API because it handles all the complexity for us.

Setting Up

First, install the dependencies:

npm install @tanstack/react-query

We'll use React Query for data fetching - it handles caching, loading states, and error handling beautifully.

The LinkPreview Component

Here's the complete component:

import { useQuery } from '@tanstack/react-query';

interface LinkPreviewProps {
  url: string;
}

interface Metadata {
  title: string;
  description: string;
  image: string;
  favicon: string;
  siteName: string;
}

async function fetchMetadata(url: string): Promise<Metadata> {
  const response = await fetch(
    `https://api.katsau.com/v1/extract?url=${encodeURIComponent(url)}`,
    {
      headers: {
        'Authorization': `Bearer ${process.env.NEXT_PUBLIC_KATSAU_API_KEY}`
      }
    }
  );

  if (!response.ok) {
    throw new Error('Failed to fetch metadata');
  }

  const data = await response.json();
  return data.data;
}

export function LinkPreview({ url }: LinkPreviewProps) {
  const { data, isLoading, error } = useQuery({
    queryKey: ['linkPreview', url],
    queryFn: () => fetchMetadata(url),
    staleTime: 1000 * 60 * 60, // Cache for 1 hour
  });

  if (isLoading) {
    return <LinkPreviewSkeleton />;
  }

  if (error || !data) {
    return <LinkPreviewFallback url={url} />;
  }

  return (
    <a
      href={url}
      target="_blank"
      rel="noopener noreferrer"
      className="block border rounded-lg overflow-hidden hover:shadow-lg transition-shadow"
    >
      {data.image && (
        <div className="aspect-video relative">
          <img
            src={data.image}
            alt={data.title}
            className="w-full h-full object-cover"
          />
        </div>
      )}
      <div className="p-4">
        <div className="flex items-center gap-2 mb-2">
          {data.favicon && (
            <img src={data.favicon} alt="" className="w-4 h-4" />
          )}
          <span className="text-sm text-gray-500">
            {data.siteName || new URL(url).hostname}
          </span>
        </div>
        <h3 className="font-semibold line-clamp-2">{data.title}</h3>
        {data.description && (
          <p className="text-sm text-gray-600 mt-1 line-clamp-2">
            {data.description}
          </p>
        )}
      </div>
    </a>
  );
}

The Loading Skeleton

A good loading state makes your app feel fast:

function LinkPreviewSkeleton() {
  return (
    <div className="border rounded-lg overflow-hidden animate-pulse">
      <div className="aspect-video bg-gray-200" />
      <div className="p-4">
        <div className="flex items-center gap-2 mb-2">
          <div className="w-4 h-4 bg-gray-200 rounded" />
          <div className="w-24 h-4 bg-gray-200 rounded" />
        </div>
        <div className="w-3/4 h-5 bg-gray-200 rounded mb-2" />
        <div className="w-full h-4 bg-gray-200 rounded" />
      </div>
    </div>
  );
}

Error Fallback

When metadata can't be fetched, show a simple fallback:

function LinkPreviewFallback({ url }: { url: string }) {
  const hostname = new URL(url).hostname;

  return (
    <a
      href={url}
      target="_blank"
      rel="noopener noreferrer"
      className="flex items-center gap-3 p-4 border rounded-lg hover:bg-gray-50"
    >
      <div className="w-10 h-10 bg-gray-100 rounded flex items-center justify-center">
        🔗
      </div>
      <div>
        <p className="font-medium">{hostname}</p>
        <p className="text-sm text-gray-500 truncate max-w-xs">{url}</p>
      </div>
    </a>
  );
}

Using the Component

Now you can use it anywhere in your app:

function ChatMessage({ message }) {
  const urls = extractUrls(message.text);

  return (
    <div>
      <p>{message.text}</p>
      {urls.map(url => (
        <LinkPreview key={url} url={url} />
      ))}
    </div>
  );
}

Performance Tips

1. Cache Aggressively

Metadata doesn't change often. Cache responses for at least an hour:

staleTime: 1000 * 60 * 60, // 1 hour

2. Deduplicate Requests

React Query automatically deduplicates concurrent requests for the same URL.

3. Use Suspense

For even better UX, use React Suspense:

<Suspense fallback={<LinkPreviewSkeleton />}>
  <LinkPreview url={url} />
</Suspense>

4. Prefetch on Hover

Prefetch metadata when users hover over links:

const queryClient = useQueryClient();

function prefetchMetadata(url: string) {
  queryClient.prefetchQuery({
    queryKey: ['linkPreview', url],
    queryFn: () => fetchMetadata(url),
  });
}

Why Use an API?

You might wonder: "Can't I just fetch the page and parse it myself?"

You could, but you'd need to handle:

  • CORS issues (requires a backend)
  • Different meta tag formats (og:, twitter:, standard)
  • Relative URL resolution
  • Character encoding issues
  • Timeout handling
  • Rate limiting
  • Edge cases (SPAs, JavaScript-rendered content)

A dedicated metadata API handles all of this for you, so you can focus on building your product.

Conclusion

Building a link preview component is straightforward with the right tools. Using React Query for data fetching and a metadata API for extraction, you can add professional link previews to your app in under an hour.


Ready to add link previews to your app? Get your free Katsau API key and start building.

Ready to build?

Try Katsau API

Extract metadata, generate link previews, and monitor URLs with our powerful API. Start free with 1,000 requests per month.