Sygnał post_save, transaction.on_commit, adres zapisany przez update().
Strony w Django zwykle rysują karty OG ręcznie w Pillow, piksel po pikselu. Z szablonem w HTML i CSS zmieniasz wygląd w arkuszu stylów, a po stronie Pythona zostaje jedno żądanie po zapisaniu wpisu.
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))
Zapisuj adres przez `QuerySet.update()`, nie `post.save()`. `save()` w obsłudze `post_save` odpala sygnał ponownie — i dostajesz pętlę, która renderuje ten sam obrazek, aż coś przekroczy limit czasu.
Z ?wait=true czekasz na gotowy obrazek w tym samym połączeniu — zwykle około sekundy.
{
"id": "img_01m08s3ez1zcv28h335g9923bw",
"status": "completed",
"url": "https://cdn.imageapi.dev/renders/img_01m08s3ez1zcv28h335g9923bw.png",
"duration_ms": 912
}
Sto obrazków miesięcznie za darmo, potem $9 za dwa tysiące. Ten sam wpis wyrenderowany drugi raz zwraca istniejący adres i nie zużywa limitu — więc przebudowanie strony nic nie kosztuje.