Generating link preview images with Flask

Jakub Svehla—Oct 28, 2020

In this post, I will show you how to generate link preview images for your blog posts so that they look good when you share them on social media.

Below, you can see how it's gonna look like:

Twitter Card

We will create a simple Flask endpoint that will accept a URL and return a generated image based on the website's metadata.

So let's get to work.

Prepare a Jinja2 template for an HTML version of your card

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{ title }}</title>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
    <style>
      @page {
        size: 1200px 628px;
        margin: 0;
        padding: 0;
      }

      html {
        font-size: 137.5%;
        -webkit-font-smoothing: antialiased;
      }

      body {
        padding: 3rem;
        font-family: 'Inter', sans-serif;
        color: #111111;
        background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='18' viewBox='0 0 100 18'%3E%3Cpath fill='%231c64f2' fill-opacity='0.05' d='M61.82 18c3.47-1.45 6.86-3.78 11.3-7.34C78 6.76 80.34 5.1 83.87 3.42 88.56 1.16 93.75 0 100 0v6.16C98.76 6.05 97.43 6 96 6c-9.59 0-14.23 2.23-23.13 9.34-1.28 1.03-2.39 1.9-3.4 2.66h-7.65zm-23.64 0H22.52c-1-.76-2.1-1.63-3.4-2.66C11.57 9.3 7.08 6.78 0 6.16V0c6.25 0 11.44 1.16 16.14 3.42 3.53 1.7 5.87 3.35 10.73 7.24 4.45 3.56 7.84 5.9 11.31 7.34zM61.82 0h7.66a39.57 39.57 0 0 1-7.34 4.58C57.44 6.84 52.25 8 46 8S34.56 6.84 29.86 4.58A39.57 39.57 0 0 1 22.52 0h15.66C41.65 1.44 45.21 2 50 2c4.8 0 8.35-.56 11.82-2z'%3E%3C/path%3E%3C/svg%3E");
      }

      h1 {
        font-size: 3.052rem;
        margin-top: 0;
      }

      p {
        font-size: 1.563rem;
        line-height: 1.5;
      }
    </style>
  </head>

  <body>
    <h1>{{ title }}</h1>
    <p>{{ excerpt }}</p>
  </body>
</html>

There's nothing special here. It's a regular HTML web page. Just be sure to use the @page CSS at-rule to set the width and height of the card. The @page rule is usually used to set styles for a document when printed but here we use it because the library for exporting the web page to an image uses it as well (I guess it “prints” it to an image).

Create a Flask endpoint that will generate the preview image

import io
import requests
from lxml import etree

@app.route('/preview.png')
def preview():
    page_html = requests.get(request.args['url']).text

    parser = etree.HTMLParser()
    tree = etree.parse(io.StringIO(page_html), parser)
    head = tree.xpath('/html/head')[0]
    title = head.xpath('meta[@property="og:title"]/@content')[0]
    description = head.xpath('meta[@property="og:description"]/@content')[0]

    preview_html = render_template('card.html', title=title, excerpt=description)
    preview_img = weasyprint.HTML(string=preview_html).write_png(resolution=2 * 96)

    return Response(preview_img, mimetype='image/png')

Let's get through the code line by line:

  1. Fetch the website.

    page_html = requests.get(request.args['url']).text
    
  2. Parse the HTML response.

    parser = etree.HTMLParser()
    tree = etree.parse(io.StringIO(page_html), parser)
    
  3. Get the title and description from its metadata.

    head = tree.xpath('/html/head')[0]
    title = head.xpath('meta[@property="og:title"]/@content')[0]
    description = head.xpath('meta[@property="og:description"]/@content')[0]
    
  4. Render the HTML version of the preview (the Jinja template we prepared above).

    preview_html = render_template('card.html', title=title, excerpt=description)
    
  5. Use weasyprint to convert the HTML preview to PNG.

    preview_img = weasyprint.HTML(string=preview_html).write_png(resolution=2 * 96)
    
  6. And finally, return the generated image.

    return Response(preview_img, mimetype='image/png')
    

And that's it! Now you can use the endpoint in your website metadata so that it looks good when shared on social media:

<meta property="og:image" content="https://YOUR_DOMAIN/preview.png?url=YOUR_URL"/>

If you have any comments, feedback or questions, let me know on Twitter or via email.


Thanks for reading

If you liked the post, subscribe to my newsletter to get new posts in your mailbox! 📬

Also, feel free to buy me a coffee. ☕️