Django

OG images in
Django

post_save signal, transaction.on_commit, URL written back with update().

Where it plugs in

Django sites usually end up drawing OG cards by hand with Pillow, pixel offset by pixel offset. With a template in HTML and CSS you change the design in a stylesheet, and the Python side shrinks to one request after the post is saved.

blog/signals.py

import requests
from django.conf import settings
from django.db import transaction
from django.db.models.signals import post_save
from django.dispatch import receiver

from .models import Post

API_URL = "https://imageapi.dev/v1/images?wait=true"


def generate_og_image(post_id: int) -> None:
    post = Post.objects.select_related("author").get(pk=post_id)
    response = requests.post(
        API_URL,
        headers={"Authorization": f"Bearer {settings.IMAGE_API_KEY}"},
        json={
            "template": "blog-og",
            "modifications": {
                "title": post.title,
                "author": post.author.get_full_name(),
                "date": post.published_at.date().isoformat(),
                "site": "yourblog.com",
            },
        },
        timeout=20,
    )
    response.raise_for_status()
    # .update() instead of .save(): it does not fire post_save again.
    Post.objects.filter(pk=post_id).update(og_image_url=response.json()["url"])


@receiver(post_save, sender=Post)
def on_post_saved(sender, instance, update_fields=None, **kwargs):
    if not instance.is_published:
        return
    # Run after the transaction commits; with Celery, call .delay() here instead.
    transaction.on_commit(lambda: generate_og_image(instance.pk))
The trap

Write the URL back with `QuerySet.update()`, not `post.save()`. A `save()` inside a `post_save` handler fires the signal again — and you get a loop that renders the same image until something times out.

What comes back

With ?wait=true the finished image arrives on the same connection — usually in about a second.

{
    "id": "img_01m08s3ez1zcv28h335g9923bw",
    "status": "completed",
    "url": "https://cdn.imageapi.dev/renders/img_01m08s3ez1zcv28h335g9923bw.png",
    "duration_ms": 912
}

What it costs

A hundred images a month free, then $9 for two thousand. The same post rendered twice returns the existing URL and uses no quota — so rebuilding your site costs nothing.

Other frameworks