Django Guide
Published March 29, 2026 · 9 min read

Add Dynamic OG Images to Django Apps with OGPeek API

Django has a batteries-included philosophy—but Open Graph images are not one of those batteries. Most Django apps either ship with no og:image at all, or a single static image hardcoded in the base template. This guide shows you how to generate unique, branded social preview images for every page in your Django app using template tags, context processors, model methods, and the OGPeek API. No Pillow. No headless Chrome. No image files to manage. Just a URL.

OG image preview for this article generated by OGPeek

Why Django Apps Need Dynamic OG Images

When someone shares a link to your Django app on Twitter, LinkedIn, Slack, or Discord, the platform fetches your page's HTML and looks for <meta property="og:image">. If it finds one, it renders a rich preview card with your image. If it does not, you get a plain text link that nobody clicks.

The data is clear: posts with image previews get 2–3x higher click-through rates than plain links. For a blog, that means more readers. For a SaaS product, that means more signups. For an e-commerce site, that means more conversions. Every shared link without a proper OG image is a missed opportunity.

The traditional approach in Python is to generate images server-side using Pillow or headless Chrome. Both work, but both come with costs:

A URL-based API sidesteps all of this. You construct a URL with your title and branding parameters, embed it in a meta tag, and the API returns a 1200×630 PNG. The image is generated on the API server, cached at the CDN, and your Django app does zero image processing.

How OGPeek Works

OGPeek is a URL-based API that generates Open Graph images from query parameters. You do not need an API key for the free tier (50 images per day). A complete request URL looks like this:

https://todd-agent-prod.web.app/api/v1/og
  ?title=My+Django+Blog+Post
  &subtitle=Python+%C2%B7+Django+%C2%B7+5+min+read
  &template=gradient
  &theme=midnight
  &brandColor=%23F59E0B

The parameters control every aspect of the generated image:

Because it is a standard GET request that returns a PNG, any social media crawler can fetch it directly. No JavaScript rendering, no authentication, no cookies. The URL is the image.

Quick Start: Add og:image to Your Django Base Template

The fastest way to get dynamic OG images working is to add a context variable to your base template. In your base.html, add the meta tag inside <head>:

<!-- templates/base.html -->
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{{ page_title|default:"My Django App" }}</title>
  <meta name="description" content="{{ page_description|default:'' }}">

  <!-- Open Graph -->
  <meta property="og:title" content="{{ page_title|default:'My Django App' }}">
  <meta property="og:description" content="{{ page_description|default:'' }}">
  <meta property="og:type" content="{{ og_type|default:'website' }}">
  <meta property="og:url" content="{{ request.build_absolute_uri }}">
  <meta property="og:image" content="{{ og_image_url }}">
  <meta property="og:image:width" content="1200">
  <meta property="og:image:height" content="630">

  <!-- Twitter Card -->
  <meta name="twitter:card" content="summary_large_image">
  <meta name="twitter:title" content="{{ page_title|default:'My Django App' }}">
  <meta name="twitter:description" content="{{ page_description|default:'' }}">
  <meta name="twitter:image" content="{{ og_image_url }}">
</head>

Then in any view, build the URL and pass it to the template context:

# views.py
from django.shortcuts import render
from urllib.parse import urlencode

OGPEEK_BASE = "https://todd-agent-prod.web.app/api/v1/og"

def build_og_url(title, subtitle=None, brand_color="#F59E0B"):
    params = {
        "title": title,
        "template": "gradient",
        "theme": "midnight",
        "brandColor": brand_color,
    }
    if subtitle:
        params["subtitle"] = subtitle
    return f"{OGPEEK_BASE}?{urlencode(params)}"


def blog_detail(request, slug):
    post = get_object_or_404(Post, slug=slug, status="published")
    return render(request, "blog/detail.html", {
        "post": post,
        "page_title": post.title,
        "page_description": post.excerpt,
        "og_type": "article",
        "og_image_url": build_og_url(post.title, post.category.name),
    })

Every view that passes og_image_url to the context gets a unique social preview image. Views that do not pass it will render an empty og:image tag—which is why we will add a fallback using a context processor next.

Custom Template Tag: {% ogpeek_image %}

Django's template tag system lets you encapsulate the URL-building logic so templates never deal with raw URL construction. Create a custom template tag library:

# myapp/templatetags/ogpeek_tags.py
from django import template
from urllib.parse import urlencode

register = template.Library()

OGPEEK_BASE = (
    "https://todd-agent-prod.web.app/api"
    "/ogpeekApi/api/v1/og"
)

@register.simple_tag
def ogpeek_image(title, subtitle="", template_name="gradient",
                 theme="midnight", brand_color="#F59E0B"):
    """
    Build an OGPeek image URL from the given parameters.

    Usage:
      {% load ogpeek_tags %}
      {% ogpeek_image "My Page Title" "Subtitle text" as og_url %}
      <meta property="og:image" content="{{ og_url }}">
    """
    params = {
        "title": title[:60],
        "template": template_name,
        "theme": theme,
        "brandColor": brand_color,
    }
    if subtitle:
        params["subtitle"] = subtitle[:80]
    return f"{OGPEEK_BASE}?{urlencode(params)}"

Do not forget the __init__.py file in the templatetags/ directory. Then load and use the tag in any template:

<!-- templates/blog/detail.html -->
{% extends "base.html" %}
{% load ogpeek_tags %}

{% block extra_head %}
  {% ogpeek_image post.title post.category.name as og_url %}
  <meta property="og:image" content="{{ og_url }}">
  <meta property="og:image:width" content="1200">
  <meta property="og:image:height" content="630">
  <meta name="twitter:image" content="{{ og_url }}">
{% endblock %}

{% block content %}
  <h1>{{ post.title }}</h1>
  <p>{{ post.body }}</p>
{% endblock %}

Tip: Using as og_url stores the result in a variable so you can reuse it for both og:image and twitter:image without calling the tag twice. This is a standard Django template tag pattern.

Context Processor: Auto-Inject OG Images Everywhere

A context processor runs on every request and injects variables into every template context automatically. This is the cleanest way to ensure every page in your Django app has a fallback OG image, even if the view forgets to pass one:

# myapp/context_processors.py
from urllib.parse import urlencode

OGPEEK_BASE = (
    "https://todd-agent-prod.web.app/api"
    "/ogpeekApi/api/v1/og"
)
SITE_NAME = "My Django App"
BRAND_COLOR = "#F59E0B"


def ogpeek_context(request):
    """
    Inject a default og_image_url into every template context.
    Views can override this by passing their own og_image_url.
    """
    # Extract a title from the URL path as a fallback
    path = request.path.strip("/")
    if path:
        # /blog/my-post/ → "Blog — My Post"
        segments = path.split("/")
        title = " — ".join(
            seg.replace("-", " ").title() for seg in segments[:2]
        )
    else:
        title = SITE_NAME

    params = {
        "title": title[:60],
        "template": "gradient",
        "theme": "midnight",
        "brandColor": BRAND_COLOR,
        "subtitle": SITE_NAME,
    }

    return {
        "default_og_image_url": f"{OGPEEK_BASE}?{urlencode(params)}",
    }

Register it in your Django settings:

# settings.py
TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [BASE_DIR / "templates"],
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
                # Add the OGPeek context processor
                "myapp.context_processors.ogpeek_context",
            ],
        },
    },
]

Now update your base template to use the view-provided URL if available, falling back to the context processor's default:

<!-- templates/base.html -->
<meta property="og:image"
  content="{{ og_image_url|default:default_og_image_url }}">

With this setup, every page in your app gets an OG image automatically. Views that pass their own og_image_url override the default. Views that pass nothing still get a sensible fallback derived from the URL path. Zero pages fall through the cracks.

Why not override og_image_url in the context processor? By using a separate variable name (default_og_image_url), the context processor never clobbers values that views explicitly set. Django template context processors run before views add their variables, so if both used the same key, the view would always win. But using distinct names makes the precedence explicit and avoids confusion.

Per-Model OG Images

For content-heavy Django apps, the cleanest pattern is to put the OG image logic directly on the model. Every model that might be shared on social media gets a get_og_image_url() method:

# models.py
from django.db import models
from urllib.parse import urlencode

OGPEEK_BASE = (
    "https://todd-agent-prod.web.app/api"
    "/ogpeekApi/api/v1/og"
)


class BlogPost(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)
    category = models.ForeignKey("Category", on_delete=models.CASCADE)
    excerpt = models.TextField(max_length=300)
    body = models.TextField()
    published_at = models.DateTimeField(auto_now_add=True)
    status = models.CharField(max_length=20, default="draft")

    def get_og_image_url(self):
        params = {
            "title": self.title[:60],
            "subtitle": f"{self.category.name} · {self.read_time} min read",
            "template": "gradient",
            "theme": "midnight",
            "brandColor": "#F59E0B",
        }
        return f"{OGPEEK_BASE}?{urlencode(params)}"

    @property
    def read_time(self):
        word_count = len(self.body.split())
        return max(1, round(word_count / 250))

    def __str__(self):
        return self.title


class Product(models.Model):
    name = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)
    tagline = models.CharField(max_length=300)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    category = models.ForeignKey("Category", on_delete=models.CASCADE)

    def get_og_image_url(self):
        params = {
            "title": self.name[:60],
            "subtitle": f"${self.price} · {self.category.name}",
            "template": "gradient",
            "theme": "midnight",
            "brandColor": "#F59E0B",
        }
        return f"{OGPEEK_BASE}?{urlencode(params)}"

    def __str__(self):
        return self.name

Now your views become trivially simple because the model knows how to generate its own OG image URL:

# views.py
from django.shortcuts import render, get_object_or_404

def blog_detail(request, slug):
    post = get_object_or_404(BlogPost, slug=slug, status="published")
    return render(request, "blog/detail.html", {
        "post": post,
        "page_title": post.title,
        "page_description": post.excerpt,
        "og_image_url": post.get_og_image_url(),
    })

def product_detail(request, slug):
    product = get_object_or_404(Product, slug=slug)
    return render(request, "shop/detail.html", {
        "product": product,
        "page_title": product.name,
        "page_description": product.tagline,
        "og_image_url": product.get_og_image_url(),
    })

This pattern scales well. When you add a new model—a Course, a UserProfile, an Event—you add a get_og_image_url() method and the OG image handling is self-contained. No global configuration to update, no template logic to change.

Django REST Framework Integration

If your Django app exposes an API via Django REST Framework, you can include the OG image URL in your serializer responses. Frontend clients (React, Vue, mobile apps) can use this URL directly in their meta tags or social sharing features without building the URL themselves:

# serializers.py
from rest_framework import serializers
from urllib.parse import urlencode
from .models import BlogPost

OGPEEK_BASE = (
    "https://todd-agent-prod.web.app/api"
    "/ogpeekApi/api/v1/og"
)


class BlogPostSerializer(serializers.ModelSerializer):
    og_image_url = serializers.SerializerMethodField()
    read_time = serializers.ReadOnlyField()

    class Meta:
        model = BlogPost
        fields = [
            "id", "title", "slug", "excerpt", "body",
            "published_at", "read_time", "og_image_url",
        ]

    def get_og_image_url(self, obj):
        params = {
            "title": obj.title[:60],
            "subtitle": f"{obj.category.name} · {obj.read_time} min read",
            "template": "gradient",
            "theme": "midnight",
            "brandColor": "#F59E0B",
        }
        return f"{OGPEEK_BASE}?{urlencode(params)}"

The API response now includes the OG image URL alongside the content:

{
  "id": 42,
  "title": "Understanding Django Signals",
  "slug": "understanding-django-signals",
  "excerpt": "A deep dive into Django's signal dispatcher...",
  "published_at": "2026-03-28T14:30:00Z",
  "read_time": 7,
  "og_image_url": "https://todd-agent-prod.web.app/api/v1/og?title=Understanding+Django+Signals&subtitle=Python+%C2%B7+7+min+read&template=gradient&theme=midnight&brandColor=%23F59E0B"
}

A React frontend consuming this API can drop the og_image_url directly into a <meta> tag via react-helmet or Next.js metadata. A mobile app can use it as a share image when the user taps a share button. The Django backend provides the URL; the client decides how to use it.

DRF + Model method: If your model already has get_og_image_url(), your serializer can simply call obj.get_og_image_url() in the SerializerMethodField. That keeps the URL logic in one place regardless of whether the image is rendered server-side in a template or returned in an API response.

Testing OG Images in Django

Before deploying, write a test that verifies your OG image meta tags are present and the URLs are valid. Django's test client makes this straightforward:

# tests.py
from django.test import TestCase, Client
from urllib.parse import urlparse, parse_qs
from .models import BlogPost, Category


class OGImageTestCase(TestCase):
    def setUp(self):
        self.client = Client()
        self.category = Category.objects.create(name="Python", slug="python")
        self.post = BlogPost.objects.create(
            title="Testing OG Images in Django",
            slug="testing-og-images-django",
            category=self.category,
            excerpt="How to verify OG meta tags in your test suite.",
            body="Full article body here. " * 100,
            status="published",
        )

    def test_blog_detail_has_og_image(self):
        """Verify the blog detail page includes a valid og:image meta tag."""
        response = self.client.get(f"/blog/{self.post.slug}/")
        self.assertEqual(response.status_code, 200)

        content = response.content.decode()

        # Check og:image meta tag is present
        self.assertIn('property="og:image"', content)
        self.assertIn('property="og:image:width" content="1200"', content)
        self.assertIn('property="og:image:height" content="630"', content)

    def test_og_image_url_contains_title(self):
        """Verify the OG image URL includes the post title."""
        url = self.post.get_og_image_url()
        parsed = urlparse(url)
        params = parse_qs(parsed.query)

        self.assertIn("title", params)
        self.assertEqual(params["title"][0], self.post.title[:60])
        self.assertIn("template", params)
        self.assertEqual(params["template"][0], "gradient")

    def test_og_image_url_encodes_special_characters(self):
        """Verify special characters in titles are URL-encoded."""
        self.post.title = "Django & DRF: A Complete Guide"
        self.post.save()

        url = self.post.get_og_image_url()

        # The ampersand should be encoded as %26, not raw &
        self.assertNotIn("&", url.split("?")[1].split("title=")[1].split("&")[0])
        self.assertIn("%26", url)

    def test_twitter_card_matches_og_image(self):
        """Verify twitter:image matches og:image."""
        response = self.client.get(f"/blog/{self.post.slug}/")
        content = response.content.decode()

        # Extract both URLs — they should be identical
        import re
        og_match = re.search(
            r'property="og:image" content="([^"]+)"', content
        )
        tw_match = re.search(
            r'name="twitter:image" content="([^"]+)"', content
        )

        self.assertIsNotNone(og_match, "og:image meta tag not found")
        self.assertIsNotNone(tw_match, "twitter:image meta tag not found")
        self.assertEqual(og_match.group(1), tw_match.group(1))

    def test_homepage_has_default_og_image(self):
        """Verify the homepage gets a fallback OG image from context processor."""
        response = self.client.get("/")
        content = response.content.decode()
        self.assertIn('property="og:image"', content)
        # Should contain the OGPeek base URL
        self.assertIn("ogpeekApi/api/v1/og", content)

Run these tests with python manage.py test. They verify four things: the meta tags exist in the rendered HTML, the URLs contain the expected parameters, special characters are encoded correctly, and the Twitter card image matches the OG image. Catching a missing meta tag in CI is infinitely cheaper than discovering it when someone shares your post and gets a blank preview.

For manual verification before launch, use the platform debugging tools:

Cache busting: Social platforms cache OG images aggressively, sometimes for days. If you update your OG tags and the old image persists, use the platform's debug tool to force a re-fetch. You can also append &v=2 to the OGPeek URL to generate a new cache key without changing the image content.

Try OGPeek free — 50 images/day, no API key

Generate dynamic Open Graph images for your Django app in minutes. No signup required.

Try OGPeek free →

Wrapping Up

Django gives you multiple clean integration points for OG images: view context variables, custom template tags, context processors, model methods, and DRF serializer fields. Pick the pattern that fits your architecture:

The underlying principle is the same regardless of which pattern you choose: construct a URL string with urllib.parse.urlencode(), point it at the OGPeek API, and let the CDN handle image generation and caching. Your Django server does zero image processing, installs zero image libraries, and manages zero image files.

For more on the OGPeek API, see the complete API reference. For framework-specific guides, check out Flask + OGPeek, Rails + OGPeek, or Next.js App Router + OGPeek.