Logo

dev-resources.site

for different kinds of informations.

Creating Open Graph Images in Django for Improved Social Media Sharing

Published at
1/11/2025
Categories
django
playwright
webdev
socialmedia
Author
Aidas Bendoraitis
Creating Open Graph Images in Django for Improved Social Media Sharing

Although social media algorithms usually discourage posting links so that users stay as long as possible on the network, people often still post links below an introductory post as a comment or reply. Normal links to websites on social media look pretty dull unless you add open-graph images representing that link.

In this article, I will show you how you can generate open-graph images for a Django website using web rendering from HTML and CSS. I rely on this technique to generate Open Graph previews for links from DjangoTricks, 1st things 1st, and PyBazaar.

What is an Open Graph?

Facebook created the Open Graph protocol to allow websites to provide rich representation of any web page. Although it has specifics for websites, articles, profiles, music, and video, the common use case is to have a preview image with a title for social feeds. Open Graph previews work with most well-known social networks, including Facebook, Threads, LinkedIn, Mastodon, and Blue Sky. Open Graph tags are HTML meta tags that you put in the HEAD section, e.g.:

<meta property="og:type" content="website" />
<meta property="og:url" content="{{ WEBSITE_URL }}{{ request.path }}" />
<meta property="og:title" content="{{ profile.user.get_full_name }}" />
{% if profile.open_graph_image %}
    <meta property="og:image" content="{{ profile.open_graph_image.url }}?t={{ profile.modified|date:'U' }}" />
{% endif %}
<meta property="og:description" content="{{ profile.summary }}" />
<meta property="og:site_name" content="PyBazaar" />
<meta property="og:locale" content="en_US" />

X (formerly known as Twitter) also has basic support for Open Graph, but it is better to add Twitter-Card-specific meta tags to make the preview more prominent. Here's an example:

<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:url" content="{{ WEBSITE_URL }}{{ request.path }}">
<meta name="twitter:title" content="{{ profile.user.get_full_name }}">
<meta name="twitter:description" content="{{ profile.summary }}">
{% if profile.open_graph_image %}
    <meta name="twitter:image" content="{{ profile.open_graph_image.url }}?t={{ profile.modified|date:'U' }}">
{% endif %}

Open Graph/Twitter Card usage on X

Use both Open Graph and Twitter Card meta tags for cross-platform support.

The dimensions of the preview image

The Open Graph Preview image dimensions will vary across different social networks. Many sources online recommend using 1200 x 630 px, but I have also successfully used smaller dimensions.

If the image is too big, the social network might take longer to process it for the preview, which slows down the link publishing.

On the other hand, if it's too small, its text might be too blurry.

Using the browser testing tools for the screenshots

I have used Playwright and Selenium to generate preview images in a background task. Since Playwright has a built-in mechanism to download browser binaries and is much more advanced in testing capabilities, it is also my first choice for screenshots.

You can install Playwright with:

(venv)$ pip install playwright
(venv)$ playwright install

Ensuring to have the necessary settings

To show the proof of concept, you would need WEBSITE_URL and Open Graph Image dimensions in your settings:

WEBSITE_URL = "https://www.pybazaar.com"

OPEN_GRAPH_IMAGE_WIDTH = 800
OPEN_GRAPH_IMAGE_HEIGHT = 418

Passing the WEBSITE_URL to the templates

It's easiest to pass the WEBSITE_URL and other settings to all the templates by having a custom context processor:

def website_settings(request):
    from django.conf import settings

    return {
        "WEBSITE_URL": settings.WEBSITE_URL,
    }

Set it in the settings as follows:

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [os.path.join(BASE_DIR, "templates")],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
                "pybazaar.apps.core.context_processors.website_settings",
            ],
        },
    },
]

Preparing the models

Each model that should have an Open Graph preview image should get an image field open_graph_image:

import os
from django.db import models
from django.utils import timezone


def upload_images_to(instance, filename):
    now = timezone.now()
    filename_base, filename_ext = os.path.splitext(filename)
    return "profiles/{user_id}/{filename}{ext}".format(
        user_id=instance.user.pk,
        filename=now.strftime("%Y%m%d%H%M%S"),
        ext=filename_ext.lower(),
    )

class Profile(models.Model):
    user = models.OneToOneField(
        "accounts.User", verbose_name="User", on_delete=models.CASCADE
    )
    open_graph_image = models.ImageField(
        "Open-graph image", upload_to=upload_images_to, blank=True
    )

    def generate_open_graph_image(self):
        from .tasks import generate_profile_open_graph_image

        if self.open_graph_image:
            self.open_graph_image.delete()
        generate_profile_open_graph_image(profile_id=self.pk)

Creating a Django view for the Open Graph preview

To define the preview layout, we'll create a custom HTML view:

from django.views.decorators.cache import never_cache
from django.shortcuts import render

@never_cache
def profile_open_graph_preview(request, username):
    profile = get_object_or_404(Profile, user__username=username)
    context = {
        "profile": profile,
    }
    return render(request, "profiles/profile_open_graph_preview.html", context)

The Django view used for the Open Graph preview image can use CSS frameworks, such as TailwindCSS, custom fonts, and Javascript enhancements for layout. This allows you to have consistent typography and styling with the website.

Plugging the view into the URLs

The URL rule for the view is pretty straightforward:

from django.urls import path
from . import views

app_name = "profiles"

urlpatterns = [
    #...
    path(
        "<str:username>/_open-graph-preview/",
        views.profile_open_graph_preview,
        name="profile_open_graph_preview",
    ),
]

Creating a background task to make screenshots

I use Huey for background tasks. The background task that generates screenshots looks like this (Celery task would be analogical):

from huey.contrib.djhuey import db_task


@db_task()
def generate_profile_open_graph_image(profile_id):
    import os
    from playwright.sync_api import sync_playwright
    from PIL import Image
    from django.conf import settings
    from django.core.files.base import ContentFile
    from django.core.files.storage import default_storage
    from django.urls import reverse
    from io import BytesIO
    from .models import Profile

    os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"  # for Playwright
    profile = Profile.objects.get(pk=profile_id)

    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        context = browser.new_context(
            viewport={
                "width": settings.OPEN_GRAPH_IMAGE_WIDTH,  # Changed to target dimensions
                "height": settings.OPEN_GRAPH_IMAGE_HEIGHT,
            },
            ignore_https_errors=True,
        )
        page = context.new_page()
        page.goto(
            settings.WEBSITE_URL
            + reverse(
                "profiles:profile_open_graph_preview",
                kwargs={"username": profile.user.username},
            )
        )
        page.wait_for_load_state("networkidle")
        screenshot_bytes = page.screenshot()
        browser.close()

    # Open and resize if needed
    image = Image.open(BytesIO(screenshot_bytes))
    if image.size != (settings.OPEN_GRAPH_IMAGE_WIDTH, settings.OPEN_GRAPH_IMAGE_HEIGHT):
        image.thumbnail(
            (settings.OPEN_GRAPH_IMAGE_WIDTH, settings.OPEN_GRAPH_IMAGE_HEIGHT),
            resample=Image.LANCZOS
        )

    # Save the image
    final_image_io = BytesIO()
    image.save(final_image_io, format="PNG")
    final_image_io.seek(0)

    rel_file_path = Profile._meta.get_field("open_graph_image").upload_to(
        profile, "profile.png"
    )
    default_storage.save(rel_file_path, ContentFile(final_image_io.getvalue()))
    Profile.objects.filter(pk=profile.pk).update(open_graph_image=rel_file_path)

Triggering the generation of the Open Graph preview image

Call the image generation method after saving an entry and its relations in a change view, administration view, or management commands:

profile.generate_open_graph_image()

The method runs the background task that launches Chromium, opens the Open Graph preview page, takes a screenshot, resizes it if necessary, and then saves the image. All that takes up to 15 seconds.

Final words

If you want the pages you share on social feeds to have a branded look and feel, generating Open Graph images with Playwright could be the right approach. You can use Playwright to make a screenshot of a web page in Chromium. The page can be styled with the same quality as your website. JavaScripts can be applied to highlight code syntax or resize the texts to fit into the given container, too.

Cover photo by Zeeshaan Shabbir

Featured ones: