ScreenshotNeo

BlogHow-to

How to Create Open Graph Images from a Database with Playwright

Build record-specific Open Graph images with Playwright, serve them at stable public URLs, and make metadata available to social crawlers.

By the ScreenshotNeo team4 October 202610 min read

Read a record from your database, render its fields into a controlled HTML card, and use Playwright to capture that card as an image. Serve the image at a public URL, then place that URL in the record page’s og:image metadata. Return the Open Graph tags in the initial HTML response when the target crawler might not run page JavaScript.

The examples below use Node.js and Playwright because the title calls for Playwright. They show the rendering boundary, not a particular database or web framework: connect the marked lookup and storage steps to your application’s existing data and deployment layers. Set the viewport and output dimensions deliberately, escape record data, wait for fonts and images, and make the image URL identify the correct record and version.

1. Choose the rendering and delivery shape

There are two common ways to generate these images:

  • On demand: render when an image URL is requested. This keeps the image close to current database data, but puts browser work on the request path. Add caching or request coalescing if repeated visits would render the same record.
  • Precomputed: render when a record is created or updated, then store the output and serve it as a static image. This moves browser work to a job or update path, but requires a way to replace or invalidate stale images.

For either approach, use a stable public image URL, return the correct image content type, and avoid requiring a logged-in browser session to fetch it. Include a content version or update the image when relevant record fields change. There is no universally correct storage provider or caching policy; choose based on traffic, freshness needs, and deployment constraints.

2. Render a database record with Playwright

Install Playwright in your Node.js project and install its Chromium browser using the official Playwright installation instructions. The following is a runnable rendering function. It accepts a prepared view model, escapes text before inserting it into HTML, sets a fixed viewport, waits for fonts and images, and returns PNG bytes.

import { chromium } from 'playwright';

function escapeHtml(value) {
  return String(value ?? '').replace(/[<>&"']/g, (char) => ({
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#39;'
  })[char]);
}

function renderCardHtml(record) {
  const title = escapeHtml(record.title || 'Untitled record');
  const description = escapeHtml(record.description || '');
  const label = escapeHtml(record.label || '');

  return `<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <style>
    * { box-sizing: border-box; }
    html, body { margin: 0; width: 1200px; height: 630px; }
    body {
      display: grid;
      place-items: center;
      padding: 64px;
      color: #f8fafc;
      background: #111827;
      font-family: Arial, sans-serif;
    }
    main { width: 100%; }
    .label { color: #93c5fd; font-size: 24px; margin-bottom: 24px; }
    h1 { font-size: 64px; line-height: 1.08; margin: 0 0 22px; }
    p { color: #cbd5e1; font-size: 28px; line-height: 1.3; margin: 0; }
  </style>
</head>
<body>
  <main>
    <div class="label">${label}</div>
    <h1>${title}</h1>
    <p>${description}</p>
  </main>
</body>
</html>`;
}

export async function createOgImage(record) {
  const browser = await chromium.launch();
  try {
    const page = await browser.newPage({
      viewport: { width: 1200, height: 630 },
      deviceScaleFactor: 1
    });
    await page.setContent(renderCardHtml(record), { waitUntil: 'load' });
    await page.evaluate(() => document.fonts.ready);
    await page.evaluate(async () => {
      await Promise.all(Array.from(document.images, (image) => {
        if (image.complete) return Promise.resolve();
        return new Promise((resolve) => {
          image.addEventListener('load', resolve, { once: true });
          image.addEventListener('error', resolve, { once: true });
        });
      }));
    });
    return await page.screenshot({ type: 'png' });
  } finally {
    await browser.close();
  }
}

// Integrate these steps with your application's database and storage layer:
// const record = await database.records.findById(recordId);
// if (!record) return notFound();
// const png = await createOgImage(toOgViewModel(record));
// await storage.put(`og/records/${record.id}.png`, png, { contentType: 'image/png' });

Replace the integration comments with your database query and image delivery code. Build a small view model containing only fields the card needs; provide predictable fallbacks for missing fields. Do not interpolate untrusted HTML or JavaScript into the template. Escaping text is necessary, but it does not make arbitrary user-provided markup safe.

The example uses a 1200 × 630 viewport as an illustration of a fixed card size, not as a universal platform requirement. Confirm dimensions, format, and any byte limits for the platforms where the image will be shared. Playwright can capture PNG, JPEG, or WebP; it also supports options such as quality for lossy formats, scale, clipping, and stylesheets. See the Playwright screenshot API for current options.

3. Publish the image and emit record-specific metadata

Once the image is generated, serve its bytes or stored file from a public URL that resolves to that record’s current image. A stored object is convenient for stable caching; an on-demand route can avoid a separate storage step if it still returns the right bytes and headers reliably. Set an accurate content type such as image/png and make sure a crawler can fetch the URL without cookies or application login.

Put the metadata in the page’s initial HTML head. The Open Graph protocol defines og:title, og:type, og:image, and og:url as required properties. It also defines image dimensions and alt text. Use absolute, record-specific URLs and meaningful alt text:

<meta property="og:title" content="Record title">
<meta property="og:type" content="website">
<meta property="og:url" content="https://example.com/records/123">
<meta property="og:image" content="https://example.com/og/records/123.png">
<meta property="og:image:alt" content="A card showing the record title and summary">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">

The dimensions shown are examples, not a universal requirement. Check the current guidance for each platform you target. The Open Graph protocol says that when multiple og:image values appear, the first has priority in a conflict; structured properties such as width and height belong to the image root they describe. Keep the image tags ordered and avoid stale alternatives that could cause a consumer to pick an unintended image.

Google documents limitations in JavaScript rendering and notes that other search engines may ignore JavaScript-generated content. That is search-crawler guidance, not a guarantee about every social crawler, but it supports returning metadata in server-rendered or statically generated HTML. Google describes dynamic rendering as a workaround rather than a long-term solution and recommends server-side rendering, static rendering, or hydration. See its guidance on dynamic rendering and JavaScript SEO basics. Verify behavior with the specific sharing platforms you support; their fetching and cache behavior can differ.

4. Make rendering repeatable

A screenshot is the result of both the record data and the rendering environment. Keep the viewport, browser version, fonts, assets, and template controlled. If the card uses remote images, make them available before capture or provide a deterministic fallback. Wait for custom fonts and required images, as in the example, and decide what should happen when an asset fails rather than allowing random blank areas.

  • Use a stable output size and device scale factor.
  • Use known fonts and ensure they are loaded before taking the screenshot.
  • Set deterministic defaults for missing titles, descriptions, labels, and images.
  • Keep dynamic decorations such as timestamps out of the image unless they are required.
  • Run visual comparisons in the same browser and environment. Playwright notes that screenshots can vary with operating system, browser version, settings, hardware, power conditions, and headless mode.

Google’s image guidance advises against using a generic image such as a site logo when an image should represent specific content. Generate a card that actually reflects the record, and provide descriptive metadata. See Google’s image SEO guidance.

5. Choose when to render, cache, and refresh

Rendering a browser page costs CPU and memory, so avoid launching a fresh browser for every duplicate request if traffic makes that wasteful. Reuse browser processes where your service architecture supports it, limit concurrent pages, and put a bound on render time. Measure your own workload; the research for this guide establishes no universal latency or capacity figures.

On-demand generation is useful when content changes frequently and request volume is modest. Precomputation is useful when many people may request the same image or when image delivery should not wait for a browser render. In either design, decide how source-record updates produce a new image: version the image URL, replace its contents and manage caches, or invalidate the cached response. The right choice depends on how quickly changes must appear and how your hosting layer caches public files.

For reliability, close pages and browsers in cleanup paths, bound database, browser, and storage operations, and record enough context to diagnose failed renders without logging sensitive record contents. If rendering runs in a background job, make retries safe: writing the same record version twice should not produce inconsistent URLs or orphaned outputs.

6. Validate the HTML and image as separate responses

Check both what a browser renders and what a crawler can fetch over HTTP. A visually correct page can still publish a missing, private, or stale image URL; correct metadata can still point to an image with clipped text.

  1. Try representative records: short and long titles, missing optional fields, non-Latin characters, emoji, and unusually long descriptions.
  2. Inspect the generated image at its final dimensions. Check legibility, line wrapping, clipping, and fallbacks.
  3. Fetch the record page without a browser session and inspect the returned HTML head for the required Open Graph tags.
  4. Fetch the og:image URL independently. Confirm the response contains image bytes, has the expected content type, and is publicly accessible.
  5. Use the current preview or debugging tools for each target social platform to check crawler-specific behavior and cache effects.

Do not assume an update appears immediately in every sharing preview. A platform may cache fetched metadata or images; follow that platform’s current refresh and cache workflow.

7. Troubleshoot common failures

Symptom Likely cause Fix
Image is blank or missing assets Capture happened before fonts or images loaded, or a remote asset was unavailable. Wait for required assets, check their URLs and access, and render a deliberate fallback on failure.
Text or user content changes the page structure Record data was inserted as raw HTML rather than escaped text. Escape text values, use a narrow view model, and do not accept arbitrary markup as card content.
Image is clipped or layout differs between runs Content exceeds the fixed card space, fonts differ, or rendering conditions changed. Test long-field cases, define wrapping or truncation rules, and keep browser, fonts, and viewport consistent.
Social preview shows an old or generic image The page emits stale metadata, an earlier image tag takes precedence, or the platform has cached a prior fetch. Inspect the actual returned head, put the intended image first, update the image URL/version when appropriate, and use the platform’s current refresh workflow.
Preview tool cannot fetch the image The image URL is private, redirects unexpectedly, requires cookies, or returns the wrong content type. Make the intended image publicly fetchable, verify redirects and response headers, and test the image URL independently.
Browser render hangs or consumes too many resources A page waits indefinitely for network activity, too many pages run concurrently, or browser resources are not closed on error. Set an application-level time bound, cap concurrency, close pages and browsers in cleanup paths, and avoid waiting for unrelated long-lived requests.
Some crawlers see no Open Graph tags Tags are added only after client-side JavaScript runs, or crawler access is restricted. Return the tags in server-rendered or static HTML and check access rules for the crawler and image URL.

8. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. It can return an image or PDF from one GET request, with options for formats, full-page capture, CSS selectors, custom CSS and JavaScript, waits, headers, cookies, caching, and more. See the ScreenshotNeo API documentation for parameters and examples.

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

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);

ScreenshotNeo accepts cookie and consent banners like a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets before the capture; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers say which page verdict applied and whether the request was billed. Its MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Every feature is on every plan.

Sign up for ScreenshotNeo’s free plan to get 1,000 screenshots a month with no card.

FAQ

Can I generate the image from a database record without saving a file?

Yes. Return the screenshot buffer from an image route with the matching image content type. The route still needs to be publicly fetchable and stable enough for the URL in og:image.

Should I use multiple og:image tags?

Only when you have a reason to offer alternatives. Open Graph gives the first image priority in a conflict, so keep its ordering intentional and group each image’s structured properties with its image tag.

Is 1200 × 630 the required size?

No universal size requirement was established for this guide. Treat that size as an example and confirm the current specifications of the sharing platforms you target.

Will all social crawlers run JavaScript?

Do not assume so. Emit metadata in the initial HTML response, then validate with each platform’s current crawler or preview workflow.