ScreenshotNeo

BlogHow-to

How to Generate Open Graph Images in FastAPI

Generate social preview images in FastAPI, serve them with the right media type, and connect them to Open Graph metadata in your page’s HTML.

By the ScreenshotNeo team29 September 202611 min read

How to Generate Open Graph Images in FastAPI

FastAPI can serve an Open Graph image, but it does not create one automatically. Generate the image with application code, render HTML in a browser, or call a hosted generator; then return the resulting file with an image media type and place its public URL in the HTML metadata of the page people share.

This guide builds a small FastAPI app that generates a PNG with Pillow, serves it at a stable URL, and adds Open Graph tags to a page. It also shows a Playwright route for HTML/CSS layouts, explains caching and deployment choices, and covers the distinction between social metadata and FastAPI’s API documentation.

1. Understand the pieces

An Open Graph image is an image referenced by metadata in a web page’s HTML. The page that users share contains tags such as og:title, og:type, og:url, and og:image. A crawler fetches that HTML and may then request the image URL. Your FastAPI route can provide the image, but the HTML document must expose its URL.

The shared page supplies metadata, while a separate FastAPI route serves the image.
The shared page supplies metadata, while a separate FastAPI route serves the image.

These are separate responsibilities:

  • Generation: create pixels with a drawing library, capture a designed HTML page in a browser, or request an external image service.
  • Delivery: make the finished image available at a stable URL and return the correct media type.
  • Discovery: include that URL in the shared page’s Open Graph metadata.

FastAPI’s title, summary, and description settings describe the API and its generated OpenAPI documentation. They do not generate social images or add metadata to an unrelated web page. See FastAPI’s metadata documentation.

2. Create and serve a PNG with FastAPI

For a simple branded card with text and shapes, drawing the image in application code avoids launching a browser. This example uses Pillow and writes generated files to a local directory. Install the packages:

python -m pip install fastapi uvicorn pillow

Save this as main.py:

from pathlib import Path
from urllib.parse import quote

from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse, HTMLResponse
from PIL import Image, ImageDraw, ImageFont

app = FastAPI(title="Social Preview Service")
OUTPUT_DIR = Path("generated")
OUTPUT_DIR.mkdir(exist_ok=True)


def make_card(title: str, key: str) -> Path:
    # Keep the output name deterministic so the same content can be cached.
    path = OUTPUT_DIR / f"{key}.png"
    if path.exists():
        return path

    image = Image.new("RGB", (1200, 630), "#101827")
    draw = ImageDraw.Draw(image)
    font = ImageFont.load_default(size=54)
    small = ImageFont.load_default(size=25)
    draw.rounded_rectangle((55, 55, 1145, 575), radius=36, fill="#18263a")
    draw.ellipse((920, 115, 1080, 275), fill="#5eead4")
    draw.text((100, 150), "PRODUCT UPDATE", fill="#5eead4", font=small)
    draw.multiline_text((100, 230), title, fill="white", font=font, spacing=12)
    draw.text((100, 505), "example.com", fill="#bac7d8", font=small)
    image.save(path, format="PNG", optimize=True)
    return path


@app.get("/og/{key}.png")
def get_og_image(key: str):
    # In production, look up key in trusted content data and validate it.
    if not key.replace("-", "").isalnum() or len(key) > 80:
        raise HTTPException(status_code=404, detail="Image not found")
    path = make_card(f"A preview for {key}", key)
    return FileResponse(path, media_type="image/png", filename=path.name)


@app.get("/articles/{key}", response_class=HTMLResponse)
def article(key: str):
    if not key.replace("-", "").isalnum() or len(key) > 80:
        raise HTTPException(status_code=404, detail="Page not found")
    base = "https://www.example.com"
    canonical = f"{base}/articles/{quote(key)}"
    image_url = f"{base}/og/{quote(key)}.png"
    safe_key = key.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")
    html = f"""<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{safe_key} | Example</title>
  <meta property="og:title" content="{safe_key}">
  <meta property="og:type" content="article">
  <meta property="og:url" content="{canonical}">
  <meta property="og:image" content="{image_url}">
  <meta property="og:image:type" content="image/png">
</head>
<body><h1>{safe_key}</h1></body>
</html>"""
    return HTMLResponse(html)

Run it with uvicorn main:app --reload. Open http://127.0.0.1:8000/articles/launch to inspect the HTML and http://127.0.0.1:8000/og/launch.png to retrieve the image. Replace www.example.com with the public origin serving the routes. A social crawler cannot fetch a URL that only exists on localhost or behind an inaccessible network.

The example uses a fixed canvas size to make the drawing concrete, not to assert a universal platform requirement. Confirm current image dimensions, formats, file-size limits, and cache behavior for each destination where the page will be shared. The available FastAPI material does not establish one specification that applies everywhere.

Use actual content and escape metadata

The example makes a card from the route key for clarity. A real app should retrieve a title and other fields from its trusted content store, then pass those values to the renderer. Never interpolate arbitrary request text into HTML or a file path. Escape HTML attribute values (for example with a template engine’s autoescaping) and validate identifiers against the records the caller may access. The simple escaping shown is only illustrative; production HTML should use a proper templating mechanism.

3. Document the image response in OpenAPI

When a route returns an image, declare its media type in the route’s response metadata so generated API documentation describes the payload. FastAPI documents returning a FileResponse with media_type="image/png", and documents response content types under responses. Its additional responses guide notes that the same parameter can describe different media types for a response.

@app.get(
    "/og/{key}.png",
    responses={
        200: {
            "content": {"image/png": {}},
            "description": "Generated Open Graph image",
        }
    },
)
def get_og_image(key: str):
    path = make_card(f"A preview for {key}", key)
    return FileResponse(path, media_type="image/png")

Do not add a JSON response declaration if this endpoint always returns an image. If it can return either JSON or an image, document both and make the actual response behavior match. The OpenAPI declaration documents the API for developers; crawlers still need the og:image tag in the page HTML.

4. Choose a generation approach

Draw in application code

Use a drawing library when the composition is fixed or can be expressed as shapes, text, and images. This keeps generation within the Python process and avoids browser management. Consider font availability, text wrapping, long titles, Unicode glyph coverage, and image assets. Load a known font file rather than relying on a system default if the appearance must remain consistent across containers.

Browser rendering turns an HTML/CSS card into an image file that FastAPI can serve.
Browser rendering turns an HTML/CSS card into an image file that FastAPI can serve.

Render HTML and capture it with Playwright

Choose browser rendering when the card design already exists as HTML and CSS or depends on web layout. Playwright’s Python Page API documents page.screenshot(path="screenshot.png"); see its screenshot API. Install the package and browser binaries in the deployment image:

python -m pip install playwright
python -m playwright install chromium

A minimal renderer function:

from pathlib import Path
from playwright.async_api import async_playwright

async def render_html_card(html: str, output: Path) -> None:
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page(
            viewport={"width": 1200, "height": 630},
            device_scale_factor=1,
        )
        await page.set_content(html, wait_until="networkidle", timeout=20000)
        await page.screenshot(path=str(output), type="png")
        await browser.close()

In a real app, create the HTML from escaped, trusted data. If CSS uses remote fonts or images, ensure those resources are reachable and decide what to do when they fail. Repeatedly starting a browser for each request can add overhead; a production design may manage browser processes and pages with bounded concurrency, but lifecycle and isolation need careful handling. This is an operational design consideration, not a measured performance claim.

Use a hosted image generator

A hosted generator can accept data or templates and return an image, shifting some rendering operations to another service. Imejis.io publishes a FastAPI integration guide describing a FastAPI endpoint that proxies its image API. The vendor describes og-image.org as an API-first generator. These are examples of vendor offerings, not evidence that a hosted service is required or faster. With this option, account for credentials, service availability, response validation, network timeouts, and vendor pricing in your own architecture.

5. Or skip the browser setup

If the image you need is a screenshot of an existing public page, ScreenshotNeo can return an image from one GET request. Its website describes a screenshot API and MCP server; see the API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents use screenshot tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. These shots capture a web page; they are not a replacement for a custom branded card generated from article data.

Sign up for 1,000 free screenshots a month, with no card required.

6. Keep routes stable and control cache behavior

Social systems may fetch a page and image at different times, and may retain fetched results. A stable URL per content item makes the reference predictable. If image content changes while its URL stays the same, a consumer may continue showing a cached version; use a versioned URL, such as a content revision or hash, when you need a new asset to have a distinct address. Confirm the destination’s current cache-refresh behavior rather than assuming a universal refresh mechanism.

For production, consider generating the image when content is published and storing it in object storage or a static asset directory. This moves image work away from the first crawler request and can make the response independent of rendering load. Alternatively, generate on demand and cache the result. In either case:

  • Set an explicit content type such as image/png, image/jpeg, or image/webp to match the bytes returned.
  • Set cache headers intentionally. Long-lived caching works best with versioned URLs; short caching makes updates easier but can increase origin requests.
  • Bound title length and wrap or truncate text so long content does not overflow the design.
  • Use a deterministic key derived from a content ID or revision, not unsanitized user input.
  • Choose public access only when the image is intended to be public. Signed or authenticated routes may not be fetchable by social crawlers.

7. Performance, reliability, and cost

There is no single best rendering path for every app. Drawing directly avoids browser layout work but offers a different design workflow. A browser can reuse HTML and CSS but brings browser binaries, memory use, fonts, network assets, and concurrency management. A hosted generator adds a network dependency and whatever pricing or limits its provider applies. The research sources provide no benchmark or universal cost comparison, so measure with your own card designs, deployment size, request volume, and latency target.

For reliability, separate content publication from crawler traffic where practical: pre-render and store assets, retry transient upstream failures with limits, and preserve an older valid image when a refresh fails. Do not let one slow external font or asset hang a request indefinitely. Add a bounded timeout to browser navigation and remote generator calls. Log the content identifier and failure stage without logging API secrets or private content.

Cost depends on compute and storage for self-hosted generation, or provider pricing for an external service. Estimate the expected number of unique images and revisions, not only page views: a cached immutable image may be fetched many times but generated once. Monitor cache misses, failed renders, output sizes, and render queue depth before scaling workers.

8. Troubleshooting

Symptom Likely cause Fix
The preview has no image The shared HTML lacks og:image, or the image URL is private/unreachable. Inspect the final HTML returned to a crawler and fetch the image URL from outside your network.
The endpoint returns JSON in docs but a PNG at runtime OpenAPI response metadata does not match the route. Declare image/png under the route’s 200 response content and keep the runtime media type consistent.
The downloaded file is corrupt or displayed as text The media type or file extension does not match the encoded bytes, or an error body was saved as an image. Check the status code, inspect response headers, and ensure PNG bytes are paired with image/png.
Playwright times out Remote resources or page scripts never reach the selected wait condition. Use a bounded timeout, wait for a specific element or readiness signal where appropriate, and avoid unnecessary remote assets.
Text is clipped or a glyph is missing Titles exceed the design area or the selected font lacks a character. Wrap, shrink, or truncate according to an explicit rule, and bundle a font with adequate glyph coverage.
Changes do not appear in a social preview A crawler or intermediary may have cached the old page or image. Use a revisioned image URL for new content and consult the target platform’s current cache tools and rules.
Works locally but fails in deployment The container lacks browser binaries, fonts, write access, or the expected output directory. Package required assets, create writable storage, and verify the deployed process can launch its renderer.

9. Frequently asked questions

Does FastAPI generate Open Graph images by itself?

No. FastAPI handles the HTTP route and response. Your code or another service must generate the image, and your page HTML must reference it.

Can I return an image directly instead of saving a file?

Yes. FastAPI supports response types suited to byte content as well as file responses. For generated files, FileResponse is straightforward; for in-memory bytes, use a response class with the correct media type. Document the response content type in OpenAPI when useful.

Should I use PNG, JPEG, or WebP?

Choose according to the destination’s current support and the visual characteristics of the card. Verify format and size requirements with the platform that will fetch it; the cited material does not establish one universal format.

Can a social crawler execute JavaScript to discover the image?

Do not depend on that. Put the intended metadata in the HTML response available to the crawler, rather than relying on client-side code to add it later.

Implementation checklist

  1. Generate a card from trusted content data using a drawing library, browser renderer, or hosted API.
  2. Serve it at a stable, public URL with a matching image media type.
  3. Put the absolute URL in og:image on the HTML page being shared.
  4. Check dimensions, format, access, and caching against each target destination.
  5. Document the image response in OpenAPI and add bounded timeouts, caching, and failure handling appropriate to your deployment.