Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Generate the bitmap in Rust, publish it at a stable unauthenticated URL, and point og:image at that URL. A practical service accepts a small input model (title, subtitle, theme and perhaps a controlled asset), renders a deterministic canvas with imageproc or a specialized OG-image crate, encodes PNG or JPEG, caches the bytes, and emits the required Open Graph metadata. The walkthrough below builds a PNG endpoint with Axum and imageproc, then covers crate choices, metadata, caching, crawler failures and an API alternative.

The Open Graph contract your Rust service must satisfy

The Open Graph protocol turns a page into a rich object. A basic object requires these four properties:

  • og:title
  • og:type
  • og:image
  • og:url

og:image is a URL, not the image bytes embedded in the HTML. The URL must be publicly fetchable by social crawlers without a login, temporary application session or JavaScript challenge. The image response should have a matching MIME type such as image/png or image/jpeg.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

You can add structured image properties when they are true for the file being served:

  • og:image:secure_url — the HTTPS equivalent.
  • og:image:type — for example, image/png.
  • og:image:width and og:image:height — the encoded pixel dimensions.
  • og:image:alt — a concise description for users who cannot see the image.

The protocol does not dictate your Rust rendering library or a single canvas size. The example uses 1200×630 pixels as an implementation choice; select dimensions and format for the destinations you support, and verify their current limits before launch.

Choose the Rust rendering layer

Approach Best fit Control Operational considerations
Specialized OG generator such as ox_content_og_image Documentation or content systems that need repeatable cards with little layout code Template-oriented Fast to adopt; confirm the crate’s current input and font APIs before pinning a release
crates_io_og_image Package or project preview cards modeled on crates.io-style metadata Opinionated composition Its documentation mentions optional oxipng optimization for PNG output
imageproc plus the image crate Product-specific branding, custom geometry, masks and precise placement Lowest-level drawing control You own text wrapping, font files, image decoding, contrast and caching
open_graph Building metadata values in a typed or helper-based way Metadata only Helpers such as create_title, create_image, create_image_type, create_image_url and create_secure_image_url do not render the bitmap

Use a specialized generator when your card is a stable template. Use imageproc when your design needs custom drawing or when you want all layout decisions in your application. Keep the input model constrained either way; accepting arbitrary HTML makes output, security review and caching much harder.

Design the endpoint before writing drawing code

A production endpoint should make the same request produce the same bytes. Define a narrow request such as:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • title: required, length-limited text.
  • subtitle: optional, length-limited text.
  • theme: an allow-listed value such as light or dark.
  • asset: an internal asset identifier rather than an arbitrary remote URL, unless you have an SSRF-safe image fetcher.

Prefer a stable path containing a content hash, for example /og/article-slug-91b7.png. Generate at publish time when possible. If you render on demand, cache by a hash of the normalized input, template version and font version. Include the template version in that hash so a redesign naturally creates a new URL.

Build a complete Rust PNG endpoint

1. Create the project and add dependencies

The following example uses Axum for HTTP, image for the pixel buffer and PNG encoding, imageproc for text drawing, and ab_glyph for font loading. Put a licensed TrueType or OpenType font at assets/Inter-Regular.ttf (or change the path).

[package]
name = "rust-og"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
serde = { version = "1", features = ["derive"] }
image = "0.25"
imageproc = "0.25"
ab_glyph = "0.2"

2. Render a deterministic card and return the correct headers

This endpoint accepts URL-encoded query parameters, draws a background and two text lines, encodes PNG bytes and sets cache headers. It deliberately does not fetch arbitrary web content.

use axum::{
    extract::Query,
    http::header,
    response::IntoResponse,
    routing::get,
    Router,
};
use ab_glyph::{FontArc, PxScale};
use image::{DynamicImage, ImageFormat, Rgba, RgbaImage};
use imageproc::drawing::draw_text_mut;
use serde::Deserialize;
use std::io::Cursor;

#[derive(Debug, Deserialize)]
struct OgQuery {
    title: String,
    subtitle: Option<String>,
    theme: Option<String>,
}

fn limit_chars(value: &str, max: usize) -> String {
    value.chars().take(max).collect()
}

fn render_png(q: &OgQuery) -> Vec<u8> {
    let dark = q.theme.as_deref() == Some("dark");
    let background = if dark { Rgba([18, 24, 38, 255]) } else { Rgba([247, 249, 252, 255]) };
    let foreground = if dark { Rgba([245, 247, 250, 255]) } else { Rgba([22, 28, 45, 255]) };
    let accent = if dark { Rgba([93, 190, 255, 255]) } else { Rgba([35, 103, 214, 255]) };

    let mut canvas = RgbaImage::from_pixel(1200, 630, background);
    let font = FontArc::try_from_slice(include_bytes!("../assets/Inter-Regular.ttf"))
        .expect("valid embedded font");
    let title = limit_chars(q.title.trim(), 90);
    let subtitle = q.subtitle.as_deref().map(|s| limit_chars(s.trim(), 140));

    draw_text_mut(&mut canvas, accent, 72, 68, PxScale::from(64.0), &font, &title);
    if let Some(line) = subtitle.filter(|s| !s.is_empty()) {
        draw_text_mut(&mut canvas, foreground, 76, 190, PxScale::from( thirty_two ), &font, &line);
    }
    draw_text_mut(&mut canvas, foreground, 76, 530, PxScale::from(28.0), &font, "example.com");

    let mut output = Cursor::new(Vec::new());
    DynamicImage::ImageRgba8(canvas)
        .write_to(&mut output, ImageFormat::Png)
        .expect("PNG encoding should succeed");
    output.into_inner()
}

async fn og(Query(query): Query<OgQuery>) -> impl IntoResponse {
    let png = render_png(&query);
    (
        [
            (header::CONTENT_TYPE, "image/png"),
            (header::CACHE_CONTROL, "public, max-age=86400, immutable"),
        ],
        png,
    )
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/og", get(og));
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

Replace thirty_two with 32.0 in the subtitle line; it is written as a named placeholder here only to make the intended scale obvious. In production code, add proper line wrapping rather than allowing long titles to run off the canvas, and return a 4xx response when a required value is empty.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

3. Test the endpoint

cargo run
curl -G "http://localhost:3000/og" 
  --data-urlencode "title=Rust Open Graph Images" 
  --data-urlencode "subtitle=Deterministic social cards" 
  --data-urlencode "theme=dark" 
  -o preview.png
file preview.png

The final command should identify a PNG. Deploy the route behind HTTPS and make the resulting URL reachable without authentication, an expiring session or a robots-blocking rule that prevents the social crawler from fetching it.

Emit metadata in each page

Generate these tags from the same canonical page record that produced the image. Escape all values for HTML and keep og:url canonical.

<meta property="og:title" content="How to Generate Open Graph Images in Rust">
<meta property="og:type" content="website">
<meta property="og:url" content="https://example.com/rust-og">
<meta property="og:image" content="https://example.com/og/rust-og-91b7.png">
<meta property="og:image:secure_url" content="https://example.com/og/rust-og-91b7.png">
<meta property="og:image:type" content="image/png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="Rust Open Graph image generation guide">

Only publish width, height, MIME type and alt text that describe the actual file. If you switch to JPEG, change both the encoder and og:image:type, and return image/jpeg from the endpoint.

Fonts, wrapping and optional assets

Font handling

Embed or ship a known font with the service instead of depending on whatever fonts happen to be installed on a host. Record the font version in your cache key. A missing glyph can produce boxes or silently change line metrics, so test punctuation, accented characters and the scripts your titles support.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Text layout

Measure text before drawing, wrap by measured width, and reserve a fixed number of lines. Truncate by Unicode scalar values rather than byte offsets. Keep a contrast check between foreground and background, especially when themes or user-supplied colors are allowed.

Remote images

If cards include a logo or article image, prefer an internal asset ID. A fetcher that accepts arbitrary URLs needs timeouts, response-size limits, content-type validation, redirect controls and private-network blocking to avoid SSRF. Decode only supported formats and reject decompression bombs before allocating a large canvas.

Rendering, caching and reliability choices

  • Precompute: render when content is published and store the bytes in object storage. Page requests then perform no drawing.
  • Render on first request: useful for unbounded content, but protect the route with a concurrency limit and cache the result before sending it.
  • Content-hash URLs: include normalized input, template version and font version in the hash. This gives immutable caching and avoids stale cards after a design change.
  • PNG versus JPEG: PNG preserves sharp text and transparency. JPEG can be smaller for photographic backgrounds but introduces compression artifacts and has no transparency.
  • Failure behavior: return a deterministic fallback image for missing optional data, and return a clear 4xx for invalid required data. Do not expose stack traces or internal file paths.
  • Observability: log render duration, cache hit/miss, output dimensions and encoding failures. The protocol has no performance benchmark you can assume; measure with your own title lengths, fonts and traffic.

Troubleshooting common failures

The preview is blank or shows the old card

Fetch the exact og:image URL with a command-line client and inspect the status, redirects, response headers and first bytes. A stable URL can remain cached by a crawler after you replace its bytes; content-hash URLs avoid that ambiguity. Ensure the page itself emits one canonical og:image value.

The crawler reports an unsupported image

Check that the response is the encoded image, not an HTML error page, and that Content-Type matches the bytes. Remove authentication and challenge pages from the image route. Verify that HTTPS certificates and redirects work for a client that does not run browser JavaScript.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Text is clipped or overlaps

Measure and wrap before drawing, enforce title and subtitle limits, and test the longest localized strings. A fixed canvas with a fixed font size cannot safely accommodate arbitrary text without a layout policy.

The Rust build fails around text drawing

Check the versions of imageproc and ab_glyph together; current imageproc releases use ab_glyph-compatible fonts. Confirm that the embedded font path exists and that the file is a valid TrueType or OpenType font.

Memory use spikes when adding user images

Limit download size before decoding, reject huge dimensions, set a request timeout and cap concurrent renders. Decode into a bounded working size instead of retaining an original multi-megapixel asset.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your social image should be a clean capture of an already-rendered page rather than a hand-drawn card, ScreenshotNeo provides a one-request screenshot API. It accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms, newsletter popups and chat widgets before capture; each cleanup step can be disabled. Only clean shots are billed: bot checks or CAPTCHAs, blank pages, timeouts, failed loads and cache hits cost nothing, and the response identifies the result with X-Page-Verdict and X-Billed headers.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use the API documentation at https://screenshotneo.com/docs/ for authentication and optional parameters. A minimal call is:

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

The same request from Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

And from Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
require('fs').writeFileSync('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo also exposes an MCP server with take_screenshot, get_page_info and capture_pdf for Claude, Cursor and other MCP clients. Plans include every feature: Free provides 1,000 shots per month with no card; Starter is $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000 and Business $249 for 1,000,000. Yearly billing gives two months free, and paid plans start at $5 for 3,000 shots. If that workflow fits your page, sign up for 1,000 free screenshots a month with no card.

FAQ

Should an OG endpoint accept arbitrary HTML?

No. A constrained data model is easier to validate, cache and secure. Arbitrary HTML also makes font loading, external requests and deterministic layout unpredictable.

When should I regenerate an image?

Regenerate when the title, subtitle, theme, asset, template or font changes. Encoding those inputs in a content hash lets you publish a new immutable URL instead of guessing whether a crawler has refreshed an old one.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Can metadata helpers render the PNG for me?

No. The open_graph crate helps construct metadata values; your application or an image-generation crate still has to create and serve the bitmap.

Frequently Asked Questions

Does Open Graph require a particular Rust framework?

No. The protocol defines metadata and a fetchable image URL, not your server framework. Axum is used here only as a concrete endpoint implementation.

Is a transparent background always safe for social previews?

No. Some consumers display transparency over their own background. Use transparency only when you have checked the destinations that matter; otherwise render an opaque background.

What should I version when redesigning the card?

Version the template and font inputs in the image key. That makes a redesign produce a new URL and prevents old cached bytes from being mistaken for the new design.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.