ScreenshotNeo

BlogHow-to

Create Social Preview Images for Indian-Language Webpages with Puppeteer

Build social preview images with Puppeteer, script-aware fonts, and grapheme-safe text handling. Includes runnable code, validation steps, and fixes for common rendering issues.

By the ScreenshotNeo team4 October 20269 min read

Create the preview as a fixed-size HTML page, load a font that covers each script you use, wait for fonts and layout to finish, then capture the card with Puppeteer’s Page.screenshot(). Puppeteer performs the browser capture; it does not design the card or guarantee that an installed font can render every Indian-language script. Test the actual output image in the same Chromium environment you plan to use in production.

This guide builds a share card from HTML, captures it as PNG, and covers script-aware fonts, Unicode-safe truncation, validation, production concerns, and common failures.

1. Build the card as a normal web page

Social preview images are ordinary browser-rendered layouts. Set explicit dimensions, typography, colors, and content. The example below uses a 1200 × 630 canvas as an implementation choice, not a dimension mandated by Puppeteer or by the supplied documentation. Adapt dimensions to the destination where you will publish the image.

Install Puppeteer

mkdir indic-social-card
cd indic-social-card
npm init -y
npm install puppeteer

Puppeteer automates Chrome and Firefox, and its page API provides screenshot capture. This example uses Puppeteer’s bundled browser. See the Page.screenshot() API and the Puppeteer guide.

Create the card HTML

Save this as card.html. The font-family names are examples: make sure the corresponding font files are available in the browser environment. Use a script-appropriate family for every script you render. For a mixed Latin and Indian-script card, keep a deliberate fallback stack and verify each text run.

<!doctype html>
<html lang="hi">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    @font-face {
      font-family: "CardIndic";
      src: url("./fonts/NotoSansDevanagari-Regular.ttf") format("truetype");
      font-style: normal;
      font-weight: 400;
      font-display: block;
    }
    * { box-sizing: border-box; }
    html, body { margin: 0; width: 1200px; height: 630px; }
    body {
      background: #10182b;
      color: #fff;
      font-family: "CardIndic", "Noto Sans", sans-serif;
    }
    .card {
      width: 1200px;
      height: 630px;
      padding: 68px 76px;
      display: flex;
      flex-direction: column;
      justify-content: space-between;
      background: radial-gradient(circle at 85% 20%, #37588b, transparent 34%), #10182b;
    }
    .eyebrow { color: #b8d4ff; font: 600 22px/1.3 sans-serif; }
    h1 { max-width: 1030px; margin: 22px 0; font-size: 60px; line-height: 1.28; font-weight: 700; }
    .summary { max-width: 980px; color: #d8e2f2; font-size: 28px; line-height: 1.45; }
    .site { color: #b8d4ff; font: 500 20px/1.3 sans-serif; }
  </style>
</head>
<body>
  <main class="card">
    <div class="eyebrow">वेब विकास · WEB DEVELOPMENT</div>
    <div>
      <h1>वेबसाइट की झलक बेहतर बनाएँ</h1>
      <p class="summary">भारतीय भाषाओं के लिए सही फ़ॉन्ट, साफ़ लेआउट और साझा करने योग्य कार्ड।</p>
    </div>
    <div class="site">example.com</div>
  </main>
</body>
</html>

The visible copy is sample Hindi. Replace it with your page title and summary. The lang attribute should describe the primary language of the content. It does not install a font or guarantee coverage.

2. Choose fonts by script and rendering context

Do not assume a Latin-oriented system font includes the glyphs needed for Hindi, Tamil, Bengali, or another Indian language. Chromium selects fonts for script-specific runs, shapes text with HarfBuzz, and attempts fallback when glyphs are missing. Fallback can still fail or produce a visual mismatch.

Noto documents distinct families for Bengali, Devanagari, Gujarati, Gurmukhi, Kannada, Malayalam, Oriya, Tamil, and Telugu. Its guidance distinguishes compact UI variants from non-UI families intended for document-style text. A font specimen for one script is not evidence of coverage for every script. See Noto’s UI font guidance, the Noto Sans Devanagari UI specimen, and the Noto Sans Bengali specimen.

Card content Font setup What to verify
One Indian script Load a family with coverage for that script Representative words, vowel signs, conjuncts, punctuation, and line wrapping
Latin plus one Indian script Specify a deliberate stack with suitable coverage for both runs Latin and Indic glyphs have compatible visual weight and readable spacing
Several Indian scripts Include and assign appropriate families per script as needed Test samples from every script; do not infer coverage across scripts
Compact card layout Consider a UI-oriented family where available Line height, clipping, and readability at the final image size

3. Capture the page after fonts and layout are ready

Save the following as capture.mjs alongside card.html and its fonts directory. It opens the local file, waits for font loading and page layout, and writes a PNG. Explicitly set the viewport to match the card canvas.

import puppeteer from "puppeteer";
import path from "node:path";
import { fileURLToPath } from "node:url";

const here = path.dirname(fileURLToPath(import.meta.url));
const cardPath = path.join(here, "card.html");
const outputPath = path.join(here, "social-preview.png");
const width = 1200;
const height = 630;

const browser = await puppeteer.launch({ headless: true });
try {
  const page = await browser.newPage({
    viewport: { width, height, deviceScaleFactor: 1 }
  });
  await page.goto(`file://${cardPath}`, { waitUntil: "load" });
  await page.evaluate(async () => {
    await document.fonts.ready;
    await new Promise(requestAnimationFrame);
    await new Promise(requestAnimationFrame);
  });
  await page.screenshot({ path: outputPath, type: "png" });
  console.log(`Wrote ${outputPath}`);
} finally {
  await browser.close();
}

Run it with node capture.mjs. Puppeteer’s screenshot options include output format and capture behavior; consult the API documentation for the current supported options. The two animation frames give layout a chance to settle after fonts resolve, but they do not prove that the intended font rendered every glyph.

Capture a specific card element

If the page contains other content, capture the card element rather than the full viewport:

const card = await page.$(".card");
if (!card) throw new Error("Card element .card was not found");
await card.screenshot({ path: outputPath, type: "png" });

Keep the element’s CSS dimensions explicit. If you instead need a full-page capture of a longer page, Puppeteer supports a full-page screenshot; that is a different output shape from a fixed social card.

4. Keep text shortening grapheme-safe

A displayed character may contain multiple Unicode code points. Cutting a JavaScript string at an arbitrary UTF-16 index can split the sequence that forms a visible grapheme, causing broken marks or shaping. Chromium’s Unicode overview states: “Graphemes are not breakable!” Read the Chromium Unicode overview.

Prefer CSS wrapping and a layout that allows the title to fit. If you must truncate by a visible-character count, segment by grapheme cluster. The code below requires a Node.js runtime with Intl.Segmenter support:

function truncateGraphemes(text, limit, locale = "hi") {
  const segmenter = new Intl.Segmenter(locale, { granularity: "grapheme" });
  const parts = [...segmenter.segment(text)];
  if (parts.length <= limit) return text;
  return parts.slice(0, limit).map(part => part.segment).join("") + "…";
}

const safeTitle = truncateGraphemes(
  "वेबसाइट की झलक बेहतर बनाएँ",
  24
);

Grapheme-safe truncation avoids splitting a cluster, but it cannot guarantee that the shortened text fits the design. Render and check the result; adjust the font size, line count, or copy if needed.

5. Validate the actual generated image

Font coverage and shaping depend on the browser and available font files. Chromium documents font selection, fallback, and text shaping in its RenderText overview. Validation in the intended runtime is therefore part of the implementation, not an assumption based on how the source HTML looks on a developer’s laptop.

  1. Prepare representative real titles and summaries for every target script, including mixed-script cases if your pages use them.
  2. Render those examples in the production browser image and environment, with the same font assets and CSS.
  3. Inspect the PNG itself at its intended display size and at a larger zoom.
  4. Check missing-glyph boxes, conjuncts, vowel signs, punctuation, line breaks, clipped ascenders or descenders, and text that touches card edges.
  5. Repeat after changing fonts, browser versions, operating-system packages, CSS, or card copy.

This checklist is implementation guidance based on the documented behavior of shaping and fallback; it is not a claim of cross-platform testing.

6. Common problems and fixes

Symptom Likely cause Fix
Boxes or blank glyphs The active font lacks glyph coverage, the font file failed to load, or fallback is unavailable Bundle a font family with the target script’s coverage, check the font URL/path, wait for document.fonts.ready, and inspect the output in the production runtime
Text looks different on a server than locally The environments have different installed fonts or browser/runtime configuration Ship the required font assets with the renderer and validate using the same browser image used for deployment
Marks or conjuncts appear separated or corrupted Text was split or transformed at an unsafe code-unit/code-point boundary, or the chosen font/render path does not shape it as expected Preserve original Unicode text, use grapheme-aware truncation, and verify the actual shaped output
Headline is clipped The font’s metrics, line-height, or text length exceed the fixed card layout Increase available space, tune line-height/font size, shorten by grapheme cluster, and inspect long representative strings
Screenshot is blank or incomplete Capture happened before navigation or assets completed, or the wrong page/element was selected Check navigation completion, confirm the target selector exists, wait for fonts and required assets, and fail visibly when expected content is absent
Font renders locally but not when deployed Relative asset paths may resolve differently, or the file is missing from the deployment package Verify the deployed path and network/file access from the page; package the font with the rendering job

7. Production, reliability, and cost considerations

Performance

  • Reuse a browser process for a batch of cards when your worker architecture allows it, while isolating page state between jobs.
  • Keep fonts local to the rendering environment where practical so card generation does not depend on a third-party font request at capture time.
  • Use only the weights and script families required by the content set. Large font assets and repeated browser startup can add work; measure your own workload rather than relying on an assumed benchmark.
  • Set fixed dimensions and avoid unnecessary page resources. Social cards rarely need a full application shell.

Reliability

  • Use a bounded timeout in the job runner and close pages/browser instances in cleanup paths.
  • Make asset readiness explicit. Navigation reaching load alone does not confirm that a remote font or late application update has finished.
  • Keep a small rendered regression set containing real samples from each supported script, and inspect changes to generated images when fonts or browser builds change.
  • Record the renderer version and font asset versions with generated artifacts if you need to reproduce a card later.

Cost and deployment

A self-hosted Puppeteer pipeline gives you control over browser and font assets, while requiring you to operate the browser workers and validate their runtime. The supplied research does not include provider pricing or performance comparisons, so choose infrastructure based on your deployment constraints and measured workload. For any option, account for browser capacity, storage, retries, font assets, and queue behavior in your own cost model.

Or skip the browser setup

If you need a screenshot of the rendered webpage rather than a custom-designed social card, ScreenshotNeo provides a screenshot API and MCP server. Its API captures a URL as PNG, JPEG, WebP, or PDF. It removes cookie and consent banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, and failed loads are never billed. AI agents can use its MCP server for screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Every feature is available on every plan.

For a page screenshot, make one GET request (replace the target URL and key):

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://example.com \
  -o page-shot.webp
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()
with open("page-shot.webp", "wb") as output:
    output.write(r.content)
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 request failed: ${res.status}`);
await Bun.write('page-shot.webp', new Uint8Array(await res.arrayBuffer()));

The API call captures a webpage; it does not replace the custom HTML card design in the Puppeteer walkthrough. See the ScreenshotNeo API documentation for setup and available parameters.

Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.

Frequently asked questions

Does Puppeteer choose the right font for an Indian language automatically?

It relies on Chromium’s font selection and fallback behavior. Supply fonts with appropriate script coverage and verify the rendered image; fallback is not a coverage guarantee.

Can one Noto font cover every Indian language?

The cited Noto documentation lists separate script families. Choose and validate fonts for the scripts present in your content.

Should I use a UI font or a document font?

Use the font variant that fits the card’s density and line-height needs, then judge the final image. Noto’s guidance identifies UI variants for tighter interfaces and non-UI families for documents.

Can I trust the HTML preview without opening the PNG?

No. Inspect the captured image generated by the target browser environment, since font loading, fallback, shaping, and layout all affect the final pixels.