ScreenshotNeo

BlogHow-to

Fix Broken Layouts in Puppeteer Screenshots of Indian Government Websites

Diagnose broken Puppeteer screenshots of Indian government websites by checking viewport settings, page readiness, responsive breakpoints, and regional-language fonts.

By the ScreenshotNeo team4 October 202610 min read

If a Puppeteer screenshot of an Indian government website has a broken layout, first make the capture repeatable: set the intended viewport before navigation, keep the browser environment fixed, and wait for the page content you need to render. Then compare screenshots at the target width and a nearby width, and check regional-language font loading and text wrapping. These are diagnostic steps, not a claim that one particular cause affects every government site.

This guide addresses “Puppeteer screenshot layout broken,” “Puppeteer screenshot Hindi font issue,” and “Puppeteer screenshot responsive layout wrong.” Without the affected URL, browser versions, capture code, and screenshots, there is no basis to name a definitive root cause.

1. Record the failing capture before changing it

Start by collecting enough information to reproduce the same rendering. Change one variable at a time so you can tell whether viewport, browser, fonts, or page readiness affects the result.

  • The exact page URL and whether it requires a login, session cookie, or particular navigation path.
  • Puppeteer version, Chromium version, operating system, and available fonts.
  • Viewport width and height in CSS pixels; device scale factor; whether mobile or touch emulation is enabled.
  • The screenshot options, including full-page versus viewport capture and any element selector.
  • Navigation and waiting conditions, plus console errors and failed network requests.
  • A screenshot of the expected result and the broken capture, both taken at a known page state.

Keep those values fixed for the initial reproduction. If the issue happens only in one browser or machine, compare environments after capturing a baseline.

2. Set the intended viewport before navigation

Set the viewport before page.goto(). Puppeteer’s Page API notes that many websites do not expect phone-sized dimensions and recommends setting the viewport before navigating. It also documents that changing viewport properties such as mobile or touch emulation can cause a page reload. [Puppeteer Page API]

Use the CSS viewport dimensions the page is meant to render at. Do not confuse those dimensions with the output image’s pixel dimensions: device scale factor affects raster output, while CSS media queries generally respond to viewport dimensions.

const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();

await page.setViewport({
  width: 1365,
  height: 900,
  deviceScaleFactor: 1,
  isMobile: false,
  hasTouch: false,
});

await page.goto('https://example.gov.in/', {
  waitUntil: 'domcontentloaded',
  timeout: 60000,
});

Replace the example URL and dimensions with the values for your capture. If you use a device preset or mobile emulation, record its dimensions and touch/mobile settings too. Do not set a desktop viewport and then toggle mobile properties after navigation without accounting for a possible reload.

3. Wait for the page state you need

A successful navigation does not guarantee that the layout is visually settled. A page may populate content after the initial document loads, fetch data in the background, or continue polling. Puppeteer’s screenshot guide demonstrates waiting for networkidle2 before taking a screenshot, and its Page API also provides waitForFunction() and waitForNetworkIdle(). Network quiet is a useful starting point, not a universal readiness signal. [Puppeteer screenshots guide, Puppeteer Page API]

Prefer a page-specific signal when you know what content must appear. The example below waits for a heading and for the document’s fonts to finish loading, then captures a full page. If the site has a stable application-ready marker, wait for that marker as well.

import puppeteer from 'puppeteer';

const url = 'https://example.gov.in/';
const browser = await puppeteer.launch({ headless: true });

try {
  const page = await browser.newPage();
  await page.setViewport({
    width: 1365,
    height: 900,
    deviceScaleFactor: 1,
    isMobile: false,
    hasTouch: false,
  });

  page.on('console', message => {
    if (message.type() === 'error') console.error('Browser console:', message.text());
  });
  page.on('requestfailed', request => {
    console.error('Request failed:', request.url(), request.failure()?.errorText);
  });

  const response = await page.goto(url, {
    waitUntil: 'domcontentloaded',
    timeout: 60000,
  });
  console.log('HTTP status:', response?.status());

  // Replace this with a selector that identifies the content you need.
  await page.waitForSelector('main', { visible: true, timeout: 30000 });

  // Useful when the page uses web fonts. This does not prove every glyph
  // uses the intended font, so inspect the result and font requests too.
  await page.evaluate(async () => {
    if (document.fonts?.ready) await document.fonts.ready;
  });

  // Optional: use only when the page's requests are expected to go quiet.
  // await page.waitForNetworkIdle({ idleTime: 1000, timeout: 15000 });

  await page.screenshot({ path: 'government-page.png', fullPage: true });
} finally {
  await browser.close();
}

Install Puppeteer in a project with npm install puppeteer, save the code as an .mjs file, and run it with Node.js. The main selector is an example: replace it with an element that exists on the target page. If content appears only after an interaction or a specific API response, wait for the corresponding visible state or condition instead of adding an arbitrary long delay.

For an element-only screenshot, wait for the element and capture its handle. Puppeteer documents that ElementHandle.screenshot() scrolls a hidden target into view by default. [Puppeteer screenshots guide]

const target = await page.waitForSelector('#content-to-capture', {
  visible: true,
  timeout: 30000,
});
if (!target) throw new Error('Capture target was not found');
await target.screenshot({ path: 'section.png' });

4. Check Hindi and regional-language font rendering

When Hindi or another regional language looks clipped, substituted, or wrapped differently, check the font path as well as the viewport. GIGW guideline 13 calls for testing Hindi and regional-language fonts on popular browsers for layout inconsistencies. It also recommends Unicode and testing across browsers and versions, operating systems, connection speeds, and screen resolutions. [GIGW guidelines]

  1. Inspect the captured text for missing glyphs, replacement boxes, unexpected line breaks, or text that overlaps nearby elements.
  2. In browser request logs or DevTools, check whether the intended web-font files loaded successfully. Look for failed requests, blocked resources, and font responses that are not valid font files.
  3. Wait for document.fonts.ready before capture if the page uses web fonts. Then inspect the rendered result; a resolved promise alone does not establish that the intended font supplied every glyph.
  4. Compare the same URL, viewport, and page state on the same operating system with the intended font available. A system fallback can help isolate a font-availability problem, but do not replace the site’s intended font in a production capture without confirming that change is appropriate.
  5. Check line wrapping and the surrounding layout after the correct font renders. A font with different glyph widths can change heading height, navigation width, and downstream alignment.

GIGW is context for what to test; it does not establish that an unspecified government website has a font defect or is noncompliant.

5. Compare responsive behavior at the exact width

GIGW guideline 15 calls for CSS-controlled responsive layouts and testing on different devices. Capture at the intended CSS viewport width, then repeat at a nearby width while holding browser, page state, and other settings constant. [GIGW guidelines]

  • If the layout breaks at just one width, inspect the media query or breakpoint around that width.
  • If a navigation bar, table, or long label extends beyond the viewport, inspect the overflowing element and its container.
  • If only a full-page capture looks wrong, compare it with a viewport capture at the same state. Look for content that loads late, sticky or fixed elements, and changes triggered while scrolling.
  • If mobile emulation changes the result, compare mobile/touch settings separately from viewport dimensions. Change one axis at a time.

Do not infer a breakpoint from a screenshot alone. Record the exact widths used and inspect the page’s computed styles or responsive CSS to identify which rule changes.

6. Use this ordered diagnostic workflow

  1. Reproduce: run the same URL with a fixed Puppeteer/Chromium version, operating system, viewport, scale factor, and screenshot options.
  2. Confirm dimensions: set the viewport before navigation and make sure it matches the target CSS width and height.
  3. Confirm page readiness: wait for a meaningful content selector or page-specific condition; use network idle only if the site’s requests can become quiet.
  4. Check fonts and text: inspect font requests, wait for font readiness where applicable, and look for script-specific glyph and wrapping differences.
  5. Compare widths: capture at the target width and a nearby width. Inspect the responsive breakpoint and any overflowing content.
  6. Compare capture modes: capture the viewport and full page with the same page state; for a local defect, capture the relevant element.
  7. Compare environments: if the difference persists, vary browser version, operating system/font availability, device emulation, or ready condition one at a time.
  8. Keep evidence: retain the URL, environment values, logs, and before/after images so another developer can reproduce the result.

7. Troubleshooting common symptoms

Symptom Likely diagnostic area What to check
Desktop page appears mobile-sized Viewport or emulation was set incorrectly or too late Set viewport before navigation; record width, height, isMobile, and hasTouch.
Layout shifts between runs Page state or environment varies Fix browser and viewport; wait for a content-specific signal; inspect late content and font requests.
Hindi characters are missing or replaced Font, glyph coverage, or font request failure Inspect font network responses, wait for font loading, and compare on an environment with the intended font.
Text overlaps or wraps differently Viewport breakpoint, font metrics, or long content Record exact CSS width; compare a nearby width; inspect computed styles and overflowing elements.
Capture contains a loader or blank content area Screenshot ran before the needed content rendered, or a request failed Wait for a visible selector or page-specific condition; inspect response status, console errors, and failed requests.
networkidle times out Persistent polling, streaming, or other ongoing requests Use a meaningful page-ready condition instead of requiring all network activity to stop.
Full-page image differs from viewport image Scrolling, lazy loading, sticky elements, or content added after initial render Compare at the same state; inspect the affected region and whether scrolling or later loading changes it.
Selector wait times out Selector is wrong, content is absent, or navigation did not reach the expected page Check the final URL and response status; inspect the DOM; use a selector that identifies content actually present on the page.

These are hypotheses to test, not confirmed causes for a particular site. Puppeteer, Chromium, the page, and the capture environment can each affect the output.

8. Performance, reliability, and cost

For repeatable captures, keep the browser version, viewport, emulation, and readiness condition consistent. Reuse a browser process when capturing multiple pages in a controlled job, but create a fresh page or browser context when session state could leak between captures. Close pages and browsers after work so failed runs do not leave processes consuming resources.

Waiting only for networkidle can waste time or never finish on pages with persistent requests. A targeted selector or application-ready condition is often more dependable when it represents the content you need. A fixed delay is simple, but may be too short on a slow connection and unnecessarily long on a fast one. Set explicit navigation and selector timeouts, and log failures so retries do not hide a reproducible problem.

Full-page screenshots can use more memory and time than viewport or element captures, especially on long pages. Capture only the region you need when that meets the task. If a capture is part of a paid workflow, account for browser runtime, retries, and failed navigation in your own operating cost; the supplied research does not establish a universal timing or cost benchmark for Puppeteer.

9. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server for developers. It can be useful when you want to avoid maintaining a browser capture environment. This one-call example saves a WebP screenshot; see the ScreenshotNeo API documentation for supported parameters and formats.

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

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.gov.in/"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.gov.in/',
});
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);

In Node.js, the example uses the built-in fetch and Bun’s Bun.write to save the response. If you run Node.js without Bun, write the response bytes with your preferred file API.

  • Cookie/consent banners are accepted and removed, along with 60+ known consent platforms, newsletter popups, and chat widgets; each step can be turned off.
  • Bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing. Response headers say which page verdict applied and whether it was billed.
  • An MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.
  • The free plan includes 1,000 screenshots a month with no card. Paid plans start at $5 for 3,000 screenshots; every feature is on every plan.

Sign up free for 1,000 screenshots a month, with no card required.

10. FAQ

Should I always wait for networkidle2?

No. It is an official guide example and can work for pages whose requests settle. For polling or other persistent requests, wait for the specific content or ready state you need.

Does a larger device scale factor fix responsive layout?

It changes raster output scale, not the CSS viewport width that usually controls responsive breakpoints. Verify viewport dimensions first.

Does a broken screenshot prove the government website is defective?

No. First reproduce it with the page URL, browser environment, viewport, wait condition, and screenshot options recorded. The available information here does not identify a specific site or root cause.

Which information should I include when asking for help?

Share the URL if it can be shared, Puppeteer and browser versions, operating system, viewport and emulation settings, minimal capture code, console and network errors, and both expected and broken screenshots.

Sources