Back to Blog

How to Build Link Previews with a Screenshot API

8 min readMarch 17, 2026

When you paste a URL into Slack, Discord, or Twitter, it automatically generates a rich preview with a title, description, and thumbnail image. This feature -- called link unfurling -- is one of the most common use cases for screenshot APIs. This guide shows you how to build your own link preview service from scratch.

What Is a Link Preview?

A link preview is a rich card that shows a visual representation of a URL. It typically includes:

The challenge is that many websites do not have Open Graph images, or their OG images are generic logos that do not represent the actual page content. A screenshot-based approach gives you a real visual of the page, which is more useful and engaging.

Architecture Overview

Here is the flow for a link preview service:

  1. User pastes a URL in your app
  2. Your backend receives the URL
  3. You fetch the page metadata (title, description) with a simple HTTP request
  4. You capture a screenshot thumbnail via a Screenshot API
  5. You cache both the metadata and thumbnail
  6. You return the assembled preview card to the frontend

Step 1: Capture the Screenshot Thumbnail

The core of link previews is the thumbnail image. Using our API, you can capture a screenshot of any URL with a single HTTP request:

Node.js (using fetch)

const API_KEY = 'sk_live_your_api_key';
const API_URL = 'https://screenshotapi-api-production.up.railway.app';

async function capturePreview(url) {
  const params = new URLSearchParams({
    url,
    width: '1200',
    height: '630',    // OG image ratio (1.91:1)
    format: 'jpeg',
    quality: '80',
    wait: '2000',     // Wait for page to fully load
  });

  const response = await fetch(
    `${API_URL}/v1/screenshot?${params}`,
    { headers: { 'Authorization': `Bearer ${API_KEY}` } }
  );

  if (!response.ok) {
    throw new Error(`Screenshot failed: ${response.status}`);
  }

  return response.arrayBuffer();
}

Python (using requests)

import requests

API_KEY = 'sk_live_your_api_key'
API_URL = 'https://screenshotapi-api-production.up.railway.app'

def capture_preview(url):
    response = requests.get(
        f'{API_URL}/v1/screenshot',
        params={
            'url': url,
            'width': 1200,
            'height': 630,
            'format': 'jpeg',
            'quality': 80,
            'wait': 2000,
        },
        headers={'Authorization': f'Bearer {API_KEY}'},
    )
    response.raise_for_status()
    return response.content

Step 2: Fetch Page Metadata

To get the title and description, you can parse the HTML's meta tags. Here is a lightweight approach in Node.js:

async function fetchMetadata(url) {
  const response = await fetch(url, {
    headers: { 'User-Agent': 'LinkPreviewBot/1.0' },
    signal: AbortSignal.timeout(5000),
  });

  const html = await response.text();

  // Extract Open Graph or standard meta tags
  const title = extractMeta(html, 'og:title')
    || extractTag(html, 'title');

  const description = extractMeta(html, 'og:description')
    || extractMeta(html, 'description');

  const domain = new URL(url).hostname;

  return { title, description, domain };
}

function extractMeta(html, property) {
  const match = html.match(
    new RegExp(
      `<meta[^>]*(?:property|name)=["']${property}["'][^>]*content=["']([^"']*)["']`,
      'i'
    )
  );
  return match ? match[1] : null;
}

function extractTag(html, tag) {
  const match = html.match(
    new RegExp(`<${tag}[^>]*>([^<]*)<\/${tag}>`, 'i')
  );
  return match ? match[1].trim() : null;
}

Step 3: Build the Preview API Endpoint

Combine screenshot capture and metadata fetching into a single Express endpoint:

import express from 'express';

const app = express();
const cache = new Map(); // Simple in-memory cache

app.get('/api/preview', async (req, res) => {
  const { url } = req.query;
  if (!url) return res.status(400).json({ error: 'URL required' });

  // Check cache first (1-hour TTL)
  const cached = cache.get(url);
  if (cached && Date.now() - cached.timestamp < 3600000) {
    return res.json(cached.data);
  }

  try {
    // Fetch metadata and screenshot in parallel
    const [metadata, screenshot] = await Promise.all([
      fetchMetadata(url),
      capturePreview(url),
    ]);

    // Convert screenshot to base64 for easy embedding
    const thumbnailBase64 = Buffer.from(screenshot)
      .toString('base64');

    const preview = {
      url,
      title: metadata.title,
      description: metadata.description,
      domain: metadata.domain,
      thumbnail: `data:image/jpeg;base64,${thumbnailBase64}`,
    };

    // Cache the result
    cache.set(url, { data: preview, timestamp: Date.now() });

    res.json(preview);
  } catch (error) {
    res.status(500).json({ error: 'Failed to generate preview' });
  }
});

app.listen(3000);

Step 4: Display the Preview Card

On the frontend, render the preview data as a card. Here is a simple React component:

function LinkPreview({ url, title, description, domain, thumbnail }) {
  return (
    <a href={url} className="link-preview-card">
      <img src={thumbnail} alt={title} />
      <div className="link-preview-content">
        <span className="link-preview-domain">{domain}</span>
        <h3>{title}</h3>
        <p>{description}</p>
      </div>
    </a>
  );
}

Production Considerations

Caching Strategy

In production, replace the in-memory cache with Redis or a CDN. Screenshot thumbnails are static assets that rarely change -- cache them for 24 hours or more. This reduces API calls and speeds up repeat requests.

Image Storage

Instead of returning base64, store screenshots in S3, R2, or a CDN and return a URL. This reduces payload size from megabytes to bytes and allows the browser to cache the image separately.

Async Processing

For real-time chat applications, generate previews asynchronously. When a user pastes a URL, immediately show a loading skeleton. Trigger the preview generation in the background and push the result via WebSocket when ready.

Rate Limiting and Abuse Prevention

Validate URLs before capturing screenshots. Block requests to private/internal IPs (SSRF protection), limit URL length, and rate-limit requests per user. Our API includes built-in SSRF protection, so you do not need to implement it yourself.

Use Cases for Link Preview Screenshots

Why Use a Screenshot API Instead of Open Graph Images?

FeatureOG ImagesScreenshot API
Shows actual page contentNoYes
Works when OG tags are missingNoYes
Always up to dateCachedFresh
Consistent dimensionsVariesControlled
No infrastructure neededYesYes (API)

Getting Started

Ready to build link previews? You can start with our free tier -- 100 screenshots per month, no credit card required.

  1. Create a free account
  2. Get your API key from the dashboard
  3. Try the interactive playground to test screenshots
  4. Copy the code examples above into your project

For production link preview services handling thousands of requests, our Pro plan at $29/month gives you 10,000 screenshots -- enough for most applications. See our pricing page for details.

Related Posts

Build link previews in minutes

100 free screenshots per month. No credit card required.

Get Your Free API Key