ScreenshotNeo

BlogHow-to

How to Create Open Graph Images with HTML and CSS

Build a reusable HTML/CSS card, render it to an image, and add Open Graph metadata so social platforms can fetch and display it.

By the ScreenshotNeo team4 October 20268 min read

To create an Open Graph image with HTML and CSS, design a fixed-size card, render it in a browser to a PNG or JPEG, publish the file at a public HTTPS URL, and point the page’s og:image metadata to that URL. A 1200 × 630 pixel canvas (about 1.91:1) is a practical starting point, not a size required by the Open Graph Protocol. Platforms can crop, resize, cache, or apply their own limits, so preview the result where it will be shared.

1. Build a fixed-size HTML and CSS card

Keep the design in a dedicated document or component so the screenshot has a predictable canvas. Make the main message readable at small preview sizes, and keep important content away from the edges in case a platform crops the image.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>OG image card</title>
  <style>
    * { box-sizing: border-box; }
    html, body { margin: 0; width: 1200px; height: 630px; }
    body {
      font-family: Arial, sans-serif;
      color: #f8fafc;
      background: #101827;
    }
    .card {
      width: 1200px;
      height: 630px;
      padding: 72px;
      display: flex;
      flex-direction: column;
      justify-content: space-between;
      background: radial-gradient(circle at 85% 20%, #3654a5, transparent 34%), #101827;
    }
    .eyebrow { color: #9bb8ff; font-size: 22px; letter-spacing: .12em; text-transform: uppercase; }
    h1 { max-width: 980px; margin: 0; font-size: 72px; line-height: 1.06; }
    .footer { color: #c4cede; font-size: 24px; }
  </style>
</head>
<body>
  <main class="card">
    <div class="eyebrow">Engineering guide</div>
    <h1>A clear title for your shared page</h1>
    <div class="footer">example.com</div>
  </main>
</body>
</html>

Use local or reliably accessible assets when possible. If you use web fonts or remote images, wait until they have loaded before rendering; otherwise the capture may contain fallback fonts or missing images. Avoid sizing the card with viewport-relative dimensions when your renderer’s viewport might differ from the intended output.

2. Render the card with Puppeteer and Chromium

Puppeteer controls a browser and saves the rendered card as an image. Install it in a Node.js project with npm install puppeteer; Puppeteer downloads a compatible browser in the standard installation flow. In deployment environments, follow Puppeteer’s current installation guidance for browser dependencies.

// render-og.mjs
import puppeteer from 'puppeteer';
import { pathToFileURL } from 'node:url';
import { resolve } from 'node:path';

const htmlPath = resolve('og-card.html');
const browser = await puppeteer.launch({ headless: true });
try {
  const page = await browser.newPage({
    viewport: { width: 1200, height: 630 },
    deviceScaleFactor: 1
  });
  await page.goto(pathToFileURL(htmlPath).href, { waitUntil: 'networkidle0' });
  await page.evaluate(async () => {
    if (document.fonts?.ready) await document.fonts.ready;
    const images = [...document.images];
    await Promise.all(images.map(img => {
      if (img.complete) return Promise.resolve();
      return new Promise((resolve, reject) => {
        img.addEventListener('load', resolve, { once: true });
        img.addEventListener('error', reject, { once: true });
      });
    }));
  });
  await page.screenshot({ path: 'og-image.png', type: 'png' });
} finally {
  await browser.close();
}

Run it with node render-og.mjs. The resulting file is og-image.png. If the HTML is served by a development server rather than loaded from disk, navigate to that local URL instead. For an existing page, you can capture a particular card element with Puppeteer’s element screenshot API; a dedicated card document is generally easier to make deterministic.

For a static site, generate images during the build and publish them with the site. For page-specific images, use a runtime route that fills a card template with page data, then render it on demand. Ensure the route returns an image with the right content type and is accessible to social crawlers.

3. Choose a rendering approach

Approach Use it when Trade-offs to check
Puppeteer with Chromium Your design uses ordinary browser HTML and CSS, or you already have a component to capture. Manage browser execution, fonts and assets, and screenshot timing in the build or runtime.
Satori with SVG-to-PNG conversion You generate images from data in code and your design fits the renderer’s supported styling. Check current styling support and deployment requirements for Satori and the SVG-to-PNG converter you choose.
Vercel OG ImageResponse Your project uses the relevant Vercel and React ecosystem and a runtime generation path fits. Check current official API documentation and runtime constraints before adopting it.

Compare options based on CSS fidelity, whether images are static or page-specific, runtime and build environment, asset loading, output format, and operational complexity. The available sources do not establish a universal performance, cost, or quality winner.

4. Publish the image and add Open Graph metadata

Upload the generated file to a stable, public HTTPS location that social crawlers can fetch without login or expiring access. Then include the Open Graph properties in the page’s initial HTML response. The protocol defines og:title, og:type, og:image, and og:url as the four required properties for a page.

<!doctype html>
<html prefix="og: https://ogp.me/ns#" lang="en">
<head>
  <meta charset="utf-8">
  <title>A clear title for your shared page</title>
  <meta property="og:title" content="A clear title for your shared page">
  <meta property="og:type" content="website">
  <meta property="og:url" content="https://example.com/page">
  <meta property="og:image" content="https://example.com/images/og-image.png">
  <meta property="og:image:width" content="1200">
  <meta property="og:image:height" content="630">
  <meta property="og:image:alt" content="A guide to the page topic on a dark blue background">
</head>
<body>...</body>
</html>

Replace the example page and image URLs with your actual canonical page and published image. The image URL identifies the finished image file; it does not point to the HTML or CSS source. The protocol permits multiple og:image properties and structured image properties such as width, height, and alt text. Select a type that describes the represented page.

5. Validate the image and platform preview

  1. Open the published image URL directly and confirm it loads without authentication.
  2. Check the actual pixel dimensions and inspect the image for clipping, missing fonts, or unloaded assets.
  3. Fetch the page’s initial HTML response and confirm the Open Graph tags are present there, not only after client-side JavaScript runs.
  4. Use a preview inspector for each destination that matters and review the crop and small-size legibility.
  5. If you replace an image at the same URL, account for platform caching. Use the destination’s available refresh or debugging process, or publish a versioned image URL and update the metadata.

Open Graph metadata describes the page and its representative image; it does not guarantee that every destination will display the image identically. A correct source file and valid tags are necessary, while platform presentation and cache behavior vary.

6. Troubleshooting

Symptom Likely cause Fix
Preview has no image The image URL is private, invalid, blocked, or returns an error. Open the exact HTTPS URL without a logged-in session; check the server response and make the asset publicly fetchable.
Preview shows an older image The destination has cached the previous result. Refresh through that platform’s inspector if available, or change to a versioned image URL and update og:image.
Text is clipped or composition looks wrong The capture viewport or card dimensions differ from the design dimensions, or the platform crops the image. Set both CSS canvas and renderer viewport to 1200 × 630; leave useful space around essential content and inspect the destination preview.
Fonts or images are missing Assets had not loaded when capture began, or remote access failed. Wait for document.fonts.ready and image completion; prefer local assets or verify remote asset reachability.
Metadata appears in browser tools but not in preview Tags are inserted after page load by client-side JavaScript, while a crawler reads the initial response. Render the tags server-side or include them in the static HTML response, then inspect the raw response.
Capture fails in CI or a serverless build The Chromium binary or required system dependencies are unavailable in that environment. Follow the renderer’s current deployment instructions, install the required browser dependencies, and ensure the selected runtime permits browser execution.
Layout differs from local browser Browser version, fonts, viewport, device scale, or asset state differs. Pin the rendering environment where practical and explicitly set viewport and device scale; wait for all visual assets.

7. Performance, reliability, and cost

For a fixed card, build-time rendering avoids regenerating the same image on each page request. For dynamic cards, cache generated output when its inputs have not changed, and keep rendering work bounded so a slow external font or image cannot hold a request indefinitely. The research sources do not establish comparative renderer benchmarks or costs, so measure your own build or route if those constraints matter.

Reliability depends on making fonts and assets available, waiting for them before capture, and serving a stable image URL. If an image changes, a versioned URL makes it easier to distinguish new output from a cached preview. Check the destination preview after deployment rather than assuming that a successful local render guarantees the same display everywhere.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server for developers. A single GET request can return a PNG, JPEG, WebP, or PDF. For a screenshot workflow, it can capture a URL without managing a browser in your own code. It is not a replacement for generating a designed OG card from a custom HTML template; use the DIY workflow above when you need that exact design.

Its clean-shot options accept consent banners like a visitor and remove 60+ known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and responses identify the page verdict and billing status. The MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots.

Example request (see the ScreenshotNeo API documentation for options and setup):

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

Python:

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

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Start with 1,000 free screenshots a month with no card.

FAQ

Can I put HTML or CSS directly in og:image?

No. Render the template to a conventional image file and put that file’s publicly fetchable URL in the metadata.

Does every platform require 1200 × 630?

No. It is a practical general-purpose starting point, not an Open Graph Protocol mandate. Check the destination’s current requirements and preview.

Can one image be used for several pages?

Yes, if it represents each page adequately. Page-specific metadata and images can make previews more relevant when pages have different subjects.

Why does the preview still look old after I changed the file?

The destination may have cached the image or page metadata. Refresh through its inspector if available, or change the image URL and update the tag.

Sources