ScreenshotNeo

BlogHow-to

HTML to Image Conversion for Indian Food Delivery Menu Cards

Build an HTML food-delivery menu card, render it in a real browser, and export a crisp image with the right crop, scale, and format.

By the ScreenshotNeo team4 October 202611 min read

To convert an HTML menu card to an image, render the card in a real browser, wait until its fonts and food images are ready, and capture the card element as PNG, JPEG, or WebP. This preserves the browser’s actual CSS layout. For repeatable output, set the card’s dimensions and browser viewport deliberately, capture the card instead of the whole page, and inspect the result at phone size.

This guide uses Playwright with Node.js. It includes a complete local example, export settings, platform-upload caveats, troubleshooting, and a hosted alternative for capture jobs.

1. Build the menu card at its intended size

Start with the asset you actually need: a single dish tile, a category card, a promotional graphic, or a longer menu page. Set its dimensions in CSS so the browser has a stable layout to render. The sample card below is 900 × 1200 CSS pixels; change those dimensions and the content to match your design and the destination’s current requirements.

Keep the dish name and price easy to scan, use sufficient contrast, and leave breathing room around the edges. A card that looks balanced on a desktop monitor can become hard to read when reduced to phone width, so preview it at the size customers are likely to see.

Save this as menu.html. The example is self-contained and uses CSS-drawn food artwork, so it does not depend on external image assets.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Saffron Kitchen menu card</title>
  <style>
    * { box-sizing: border-box; }
    html, body { margin: 0; }
    body {
      width: 900px;
      min-height: 1200px;
      padding: 52px;
      background: #f7f1e7;
      color: #24221e;
      font-family: Arial, sans-serif;
    }
    .card {
      width: 796px;
      min-height: 1096px;
      overflow: hidden;
      border-radius: 28px;
      background: #fffdf8;
      box-shadow: 0 18px 50px #35220e1a;
    }
    .hero {
      height: 440px;
      display: grid;
      place-items: center;
      background: radial-gradient(ellipse at 50% 48%, #f5bf55 0 25%, #d85a2d 26% 43%, #7d281c 44% 46%, #f0d6a7 47% 62%, #e9c98e 63%);
    }
    .hero span { font-size: 110px; filter: drop-shadow(0 8px 8px #54200c55); }
    .content { padding: 38px 44px 44px; }
    .eyebrow { color: #a64024; font-size: 17px; font-weight: 700; letter-spacing: 2px; text-transform: uppercase; }
    h1 { margin: 12px 0 8px; font-size: 48px; line-height: 1.05; }
    .description { margin: 0; color: #625d54; font-size: 21px; line-height: 1.45; }
    .items { margin-top: 32px; border-top: 1px solid #e8dfd0; }
    .item { display: flex; justify-content: space-between; gap: 20px; padding: 19px 0; border-bottom: 1px solid #e8dfd0; font-size: 23px; }
    .item strong { white-space: nowrap; }
    .note { margin-top: 24px; color: #625d54; font-size: 17px; }
  </style>
</head>
<body>
  <main class="card" id="menu-card">
    <div class="hero" aria-label="Illustration of a plated meal"><span aria-hidden="true">🍛</span></div>
    <section class="content">
      <div class="eyebrow">Saffron Kitchen · Bengaluru</div>
      <h1>House favorites</h1>
      <p class="description">Comforting classics, cooked fresh and packed with care.</p>
      <div class="items">
        <div class="item"><span>Paneer tikka biryani</span><strong>₹289</strong></div>
        <div class="item"><span>Masala dosa</span><strong>₹179</strong></div>
        <div class="item"><span>Dal makhani</span><strong>₹229</strong></div>
        <div class="item"><span>Mango lassi</span><strong>₹99</strong></div>
      </div>
      <p class="note">Prices shown in INR. Please ask us about allergens.</p>
    </section>
  </main>
</body>
</html>

For production cards, replace the emoji illustration with your own image or an approved asset. If you use a remote image, make sure it is publicly reachable by the browser process and wait for it to finish loading before capture. For local assets, serve the HTML and assets over a local HTTP server or use correctly resolved file paths.

2. Render and capture the card with Playwright

Install Node.js, then create a small project and install Playwright:

npm init -y
npm install playwright
npx playwright install chromium

Save the following as capture.mjs beside menu.html. It opens the local file, waits for fonts and images, captures only #menu-card, and saves a WebP image. The locator screenshot avoids capturing page margins or browser chrome. Playwright documents element screenshots, file output, PNG/JPEG/WebP, clipping, scaling, quality, and transparency in its Page screenshot API.

import { chromium } from 'playwright';
import { pathToFileURL } from 'node:url';
import path from 'node:path';

const htmlPath = path.resolve('menu.html');
const browser = await chromium.launch({ headless: true });
try {
  const page = await browser.newPage({
    viewport: { width: 900, height: 1200 },
    deviceScaleFactor: 1
  });
  await page.goto(pathToFileURL(htmlPath).href, { waitUntil: 'load' });
  await page.evaluate(async () => {
    await document.fonts.ready;
    await Promise.all([...document.images].map(img => {
      if (img.complete) return Promise.resolve();
      return new Promise(resolve => {
        img.addEventListener('load', resolve, { once: true });
        img.addEventListener('error', resolve, { once: true });
      });
    }));
  });

  const card = page.locator('#menu-card');
  await card.screenshot({
    path: 'menu-card.webp',
    type: 'webp',
    quality: 90,
    animations: 'disabled',
    scale: 'css'
  });
  console.log('Saved menu-card.webp');
} finally {
  await browser.close();
}

Run it with:

node capture.mjs

For a remote page instead of a local HTML file, navigate to its URL using page.goto('https://your-site.example/menu') and select the card with a stable CSS selector. If the page renders content after initial load, wait for a meaningful selector or application-ready signal before taking the screenshot. Avoid relying only on a fixed delay when the page can expose a reliable ready state.

3. Choose crop, pixel scale, and output format

Decision Use this approach Trade-off
Capture scope Screenshot the card locator, or use a deliberate clip rectangle. A full-page screenshot includes unrelated content and can produce a very tall image. Playwright supports both element and full-page capture.
Scale scale: 'css' for one output pixel per CSS pixel. scale: 'device' uses device pixels and may create substantially larger dimensions and files. Set the browser context’s device scale factor when you want predictable high-density output.
PNG Use for lossless output or transparency with omitBackground: true. PNG ignores the quality option and can be larger.
JPEG Use when lossy compression is acceptable and transparency is unnecessary. Set quality from 0–100; JPEG does not support transparent backgrounds.
WebP Use where the destination accepts it and you want a modern image format. Playwright supports WebP; quality 100 is lossless, lower values are lossy.

To save PNG instead, change the path to menu-card.png and set type: 'png'. For JPEG, use menu-card.jpg, type: 'jpeg', and a quality value such as 85. If transparent output is needed, set omitBackground: true and choose PNG or WebP; it has no effect for JPEG.

You can capture a fixed region with page.screenshot({ path: 'menu.png', clip: { x: 0, y: 0, width: 900, height: 1200 }, scale: 'css' }). Coordinates are in CSS pixels. A card locator is generally safer because its bounds follow the element’s actual layout; a clip is useful when the design intentionally uses a fixed canvas.

Other useful screenshot controls include animations: 'disabled' for a stable frame, caret: 'hide' to avoid a visible text cursor, style to apply capture-only CSS, mask to cover selected dynamic regions, and a timeout for the screenshot operation. Set these only when the page needs them; a deterministic card should need little special handling.

4. Check delivery requirements before uploading

Do not assume there is one universal image size for Indian food delivery platforms. A card for a promotion, a dish photo, a menu page, and a restaurant cover image can have different rules. The research available for this guide did not verify current pixel dimensions, formats, file-size limits, or crop behavior for HTML-generated menu cards on Swiggy or Zomato.

An older surfaced Zomato guideline gives 650 × 700 pixels as a maximum for menu pages and recommends 1200 × 600 pixels or greater for cover photos. Those refer to different asset types and should not be treated as current general specifications for a delivery menu card. Check the current partner upload flow and requirements for the exact destination. Swiggy’s 2023 partner article describes capturing and uploading menu images through the Swiggy Owner app but does not provide dimensions for HTML-generated cards.

Sources: Zomato’s menu imagery article and Swiggy’s Photoshoot feature article. Confirm accepted formats and limits in the current partner interface before generating a production batch.

5. Quality checklist for a menu-card export

  • Correct content: names, prices, currency symbol, spelling, and offer dates match the live menu.
  • Correct crop: no heading, dish image, price, or disclaimer is clipped at the edges.
  • Readable on a phone: preview the exported file reduced to its likely display size.
  • Fonts loaded: verify that intended typefaces appear, with no fallback font changing line breaks.
  • Images loaded: check remote image requests and ensure the final output does not contain blank placeholders.
  • Stable rendering: disable animations and avoid random, time-dependent, or personalized page content.
  • File accepted: confirm destination dimensions, format, file size, and crop behavior in the current upload workflow.
  • Factual presentation: keep dish images and descriptions representative of what customers will receive.

6. Batch generation, reliability, and cost

For a handful of cards, one browser launch and one capture script is straightforward. For many cards, reuse a browser process and create a fresh page or context per render rather than launching a new browser each time. Limit concurrent pages to the memory and CPU available to the worker, and close pages even when a capture fails.

Make renders reproducible by pinning the browser and dependency versions in the project, using fixed viewport dimensions, and keeping fonts and assets available. Record the URL or source data, output dimensions, format, and render error for each job. Retry transient navigation failures with a small bounded retry policy; do not endlessly retry invalid selectors, missing files, or permanent HTTP errors. For important batches, write to a temporary filename and rename only after a successful capture so incomplete files are not mistaken for finished assets.

The primary costs of self-hosted rendering are your compute, storage, and maintenance; there is no per-image Playwright API charge described here. High device scale, long full-page captures, large source images, and excessive parallelism increase processing and storage needs. Keep scale and dimensions to what the destination needs, and compress only after checking that small text and fine edges remain clear.

7. Troubleshooting common capture failures

Symptom Likely cause Fix
Browser executable missing Playwright package is installed but Chromium was not downloaded. Run npx playwright install chromium in the project environment.
Selector timeout or no element found The selector is wrong, the card has not rendered, or it is inside an iframe. Inspect the page DOM, use a stable card selector, and wait for that locator before capture. Handle frames through the appropriate frame locator.
Image is blank or shows placeholders Remote assets are blocked, failed, or still loading. Check browser network errors and image URLs; wait for images to load and ensure the renderer can access the host.
Unexpected font or wrapping Font request failed or capture began before web fonts were ready. Wait for document.fonts.ready, verify font URLs and CORS, and consider bundling fonts with the project.
Card is clipped or too tall Capture targeted the viewport or the card content exceeds its intended frame. Use the card locator screenshot, inspect its dimensions, and adjust its CSS. Use full-page capture only when the full document is the deliverable.
Output dimensions are unexpectedly large scale: 'device' or a high device scale factor was used. Use scale: 'css' for CSS-pixel dimensions, or calculate the required device-pixel output explicitly.
Transparent background appears white The page or card has an explicit background color, or JPEG was selected. Remove the background fill where transparency is desired and use PNG/WebP with omitBackground: true.
Screenshot changes between runs Animations, changing content, dynamic prices, or layout shifts. Disable animations, fix data and viewport, wait for layout completion, and mask genuinely irrelevant dynamic regions if appropriate.
Capture hangs or navigation fails Slow or unreachable host, network restrictions, or an overly broad wait condition. Set a navigation timeout, wait for the specific card rather than every network connection to become idle, and log navigation failures.
Upload rejected by delivery platform Wrong asset type, dimensions, format, file size, or content violates current requirements. Check the platform’s current partner workflow and regenerate to its specific accepted constraints.

8. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. One GET request can render a URL as an image; put your menu card on a reachable page, then call the API. The ScreenshotNeo API documentation covers its parameters.

cURL

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

Python

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={
        "access_key": "YOUR_API_KEY",
        "url": "https://your-site.example/menu-card",
    },
    timeout=90,
)
r.raise_for_status()
with open("menu-card.webp", "wb") as image:
    image.write(r.content)

Node.js

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://your-site.example/menu-card'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('menu-card.webp', Buffer.from(await res.arrayBuffer())));

ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot; bot checks, blank pages, and failed loads are never billed; its MCP server lets AI agents take screenshots; and 1,000 screenshots per month are free with no card, with paid plans starting at $5 for 3,000. Each feature is on every plan. Create a free account and get 1,000 screenshots a month with no card.

9. FAQ

Can I make a menu image without a public website?

Yes. Render a local HTML file with Playwright as shown above. A hosted screenshot API needs a URL it can reach, so a file on your laptop alone is not sufficient for that route.

Should every dish have its own image?

That depends on the platform’s menu structure and your content. This workflow exports a designed card; it does not decide which asset type or quantity a delivery platform accepts.

Does a screenshot contain selectable text?

No. PNG, JPEG, and WebP exports are raster images. Keep the HTML or source data so you can edit prices and regenerate the card later.

Can the same HTML produce both a square and a vertical card?

Yes. Use responsive CSS or explicit variants, then render each at its intended viewport and export dimensions. Review each crop independently.