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:
- Build your own backend - Parse HTML, handle edge cases, maintain infrastructure
- 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.
Try Katsau API
Extract metadata, generate link previews, and monitor URLs with our powerful API. Start free with 1,000 requests per month.