Tutorial

Build a Link Preview Service

|10 min read

Generate rich link previews for any URL — title, description, and image — using SnapRender's /extract and /screenshot endpoints. Full Python and Node.js code with caching.

How link previews work

When you paste a URL into Slack, Discord, or Twitter, the platform fetches the page and reads its Open Graph meta tags (og:title, og:description, og:image) to build the preview card. We will build the same thing as a reusable API service.

1

Extract meta tags

Use /extract to pull og:title, og:description, og:image, and favicon from the rendered page.

2

Screenshot fallback

If no og:image exists, capture a screenshot of the page with /screenshot as the preview image.

3

Cache and serve

Store results in a cache. Serve previews from a simple API endpoint with sub-100ms response times.

Step 1: Extract meta tags (Python)

Use SnapRender's /extract endpoint to pull Open Graph tags from any URL. The key trick: use extract_attributes to get the content attribute from meta tags instead of the element's text content.

preview.py
#E8A0BF">import requests

API_KEY = #A8D4A0">"sr_live_YOUR_KEY"

#E8A0BF">def get_link_preview(url):
    # Step 1: Extract meta tags #E8A0BF">with /extract
    resp = requests.#87CEEB">post(
        #A8D4A0">"https://api.snaprender.dev/v1/extract",
        headers={#A8D4A0">"x-api-key": API_KEY},
        json={
            #A8D4A0">"url": url,
            #A8D4A0">"selectors": {
                #A8D4A0">"title": #A8D4A0">"title",
                #A8D4A0">"og_title": #A8D4A0">'meta[property=#A8D4A0">"og:title"]',
                #A8D4A0">"og_desc": #A8D4A0">'meta[property=#A8D4A0">"og:description"]',
                #A8D4A0">"og_image": #A8D4A0">'meta[property=#A8D4A0">"og:image"]',
                #A8D4A0">"description": #A8D4A0">'meta[name=#A8D4A0">"description"]',
                #A8D4A0">"favicon": #A8D4A0">'link[rel=#A8D4A0">"icon"]',
            },
            #A8D4A0">"extract_attributes": {
                #A8D4A0">"og_title": #A8D4A0">"content",
                #A8D4A0">"og_desc": #A8D4A0">"content",
                #A8D4A0">"og_image": #A8D4A0">"content",
                #A8D4A0">"description": #A8D4A0">"content",
                #A8D4A0">"favicon": #A8D4A0">"href",
            }
        }
    )
    data = resp.#87CEEB">json().#E8A0BF">get(#A8D4A0">"data", {})

    #E8A0BF">return {
        #A8D4A0">"title": data.#E8A0BF">get(#A8D4A0">"og_title") #E8A0BF">or data.#E8A0BF">get(#A8D4A0">"title") #E8A0BF">or url,
        #A8D4A0">"description": data.#E8A0BF">get(#A8D4A0">"og_desc") #E8A0BF">or data.#E8A0BF">get(#A8D4A0">"description") #E8A0BF">or #A8D4A0">"",
        #A8D4A0">"image": data.#E8A0BF">get(#A8D4A0">"og_image") #E8A0BF">or #E8A0BF">None,
        #A8D4A0">"favicon": data.#E8A0BF">get(#A8D4A0">"favicon") #E8A0BF">or #E8A0BF">None,
        #A8D4A0">"url": url,
    }

# Example
preview = get_link_preview(#A8D4A0">"https://github.com")
#E8A0BF">print(preview)

Step 2: Screenshot fallback

Many pages lack an og:image. Generate a real screenshot as a fallback preview image. Use 1200x630 — the standard Open Graph image dimension.

preview.py (continued)
#E8A0BF">def get_preview_with_screenshot(url):
    # Get meta tags
    preview = get_link_preview(url)

    # If no og:image, generate a screenshot #E8A0BF">as fallback
    #E8A0BF">if #E8A0BF">not preview[#A8D4A0">"image"]:
        resp = requests.#87CEEB">post(
            #A8D4A0">"https://api.snaprender.dev/v1/screenshot",
            headers={#A8D4A0">"x-api-key": API_KEY},
            json={
                #A8D4A0">"url": url,
                #A8D4A0">"width": 1200,
                #A8D4A0">"height": 630,
                #A8D4A0">"format": #A8D4A0">"jpeg",
                #A8D4A0">"quality": 80,
            }
        )
        #E8A0BF">if resp.#87CEEB">status_code == 200:
            # Save screenshot #E8A0BF">and use #E8A0BF">as preview image
            filename = url.split(#A8D4A0">"//")[1].replace(#A8D4A0">"/", #A8D4A0">"_") + #A8D4A0">".jpg"
            #E8A0BF">with #E8A0BF">open(#A8D4A0">"previews/" + filename, #A8D4A0">"wb") #E8A0BF">as f:
                f.#87CEEB">write(resp.#87CEEB">content)
            preview[#A8D4A0">"image"] = #A8D4A0">"/previews/" + filename

    #E8A0BF">return preview

Step 3: Serve as an API (Python)

Wrap the preview logic in a simple Flask endpoint with in-memory caching.

server.py
#E8A0BF">from flask #E8A0BF">import Flask, jsonify, request

#E8A0BF">app = Flask(__name__)

# In-memory cache (use Redis #E8A0BF">in production)
cache = {}

@#E8A0BF">app.#E8A0BF">route(#A8D4A0">"/api/preview")
#E8A0BF">def preview():
    url = request.args.#E8A0BF">get(#A8D4A0">"url")
    #E8A0BF">if #E8A0BF">not url:
        #E8A0BF">return jsonify({#A8D4A0">"error": #A8D4A0">"url parameter required"}), 400

    # Check cache
    #E8A0BF">if url #E8A0BF">in cache:
        #E8A0BF">return jsonify(cache[url])

    # Generate preview
    data = get_preview_with_screenshot(url)
    cache[url] = data
    #E8A0BF">return jsonify(data)

# GET /api/preview?url=https://example.com
# Returns: { title, description, image, favicon, url }

Node.js version

The same service in Node.js with Express. Same logic, same SnapRender endpoints.

server.js
#E8A0BF">const express = #E8A0BF">require(#A8D4A0">'express');
#E8A0BF">const #E8A0BF">app = express();
#E8A0BF">const cache = new Map();

#E8A0BF">async #E8A0BF">function getPreview(url) {
  // Extract meta tags
  #E8A0BF">const resp = #E8A0BF">await fetch(#A8D4A0">'https://api.snaprender.dev/v1/extract', {
    method: #A8D4A0">'POST',
    headers: {
      #A8D4A0">'x-api-key': #A8D4A0">'sr_live_YOUR_KEY',
      #A8D4A0">'Content-Type': #A8D4A0">'application/json',
    },
    body: JSON.#87CEEB">stringify({
      url,
      selectors: {
        title: #A8D4A0">'title',
        og_title: #A8D4A0">'meta[property=#A8D4A0">"og:title"]',
        og_desc: #A8D4A0">'meta[property=#A8D4A0">"og:description"]',
        og_image: #A8D4A0">'meta[property=#A8D4A0">"og:image"]',
      },
      extract_attributes: {
        og_title: #A8D4A0">'content',
        og_desc: #A8D4A0">'content',
        og_image: #A8D4A0">'content',
      },
    }),
  });
  #E8A0BF">const { data } = #E8A0BF">await resp.#87CEEB">json();

  #E8A0BF">return {
    title: data.og_title || data.title || url,
    description: data.og_desc || #A8D4A0">'',
    image: data.og_image || #E8A0BF">null,
    url,
  };
}

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

  #E8A0BF">if (cache.has(url)) #E8A0BF">return res.#87CEEB">json(cache.#E8A0BF">get(url));

  #E8A0BF">const preview = #E8A0BF">await getPreview(url);
  cache.set(url, preview);
  res.#87CEEB">json(preview);
});

#E8A0BF">app.listen(3000);

Build your link preview service

Get your API key in 30 seconds. 100 free requests/month to start building. No credit card required.

Get Your API Key

Frequently asked questions

A link preview is the rich card that appears when you paste a URL into Slack, Discord, Twitter, or iMessage. It typically shows the page title, description, and an image (og:image). These are generated from Open Graph and Twitter Card meta tags in the page's HTML.

Many modern sites render meta tags with JavaScript (React/Next.js apps, SPAs). A simple HTTP request only gets the raw HTML before JavaScript runs, which may not contain the og:title or og:image tags. SnapRender renders the full page first, ensuring you get the actual meta tags.

Use SnapRender's /screenshot endpoint to capture a screenshot of the page. This gives you a real visual of the page content, which is more accurate and up-to-date than relying on the site's og:image (which may be a generic logo or missing entirely).

Yes, and you should. Link preview data rarely changes, so cache the result for 24-48 hours. This saves API requests and speeds up your service. Store the title, description, image URL, and timestamp in your database.