How to Create Website Preview Images for an Indian Real Estate Portal with Puppeteer
Build reliable real estate listing previews with Puppeteer: set the viewport, wait for listing content, capture a page or element, and troubleshoot common issues.
Use Puppeteer to render a property listing in Chromium, set the viewport for the layout you want, wait for the listing content to be ready, then capture the page or a specific listing element. For a compact preview image, capturing the listing card element usually gives more predictable results than capturing an entire long page. The example below writes a PNG to disk and can be adapted to a portal URL and its listing-card selector.
1. Install Puppeteer and choose your capture target
Puppeteer controls a browser and exposes screenshots through page.screenshot() and elementHandle.screenshot(). Install it in a Node.js project:
npm install puppeteer
Identify the URL for the listing and, if you want a single property card rather than the whole page, inspect the page to find a stable CSS selector for that card. Portal markup varies, so selectors such as .listing-card below are examples; replace them with the selector actually used by your target page.
2. Runnable Puppeteer example
This script uses a desktop viewport, waits for navigation and then waits for a property-card selector before capturing that element. If you want a screenshot of the visible viewport instead, replace the element screenshot call as shown after the code.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setViewport({ width: 1200, height: 800, deviceScaleFactor: 1 });
await page.goto('https://example.com/property/listing', {
waitUntil: 'domcontentloaded',
timeout: 60000,
});
// Replace this selector with the property card's real selector.
const cardSelector = '.listing-card';
await page.waitForSelector(cardSelector, { timeout: 30000 });
// Optional: wait for the card's primary image to finish loading.
await page.waitForFunction((selector) => {
const card = document.querySelector(selector);
if (!card) return false;
const image = card.querySelector('img');
return !image || (image.complete && image.naturalWidth > 0);
}, { timeout: 30000 }, cardSelector);
const card = await page.$(cardSelector);
await card.screenshot({ path: 'property-preview.png', type: 'png' });
} finally {
await browser.close();
}
})();
Run it with node capture.js. The script writes property-preview.png in the current directory. To capture the current viewport instead of the selected element, use:
await page.screenshot({ path: 'property-preview.png', type: 'png' });
For the full document, use fullPage: true:
await page.screenshot({ path: 'property-full-page.png', fullPage: true });
3. Set the viewport for the preview destination
Set viewport dimensions before navigation so the page can choose its desktop or mobile layout before it renders. There is no universal pixel size for previews across Indian real estate portals or their sharing surfaces: determine the dimensions required by your application, then use them consistently.
await page.setViewport({
width: 1200,
height: 800,
deviceScaleFactor: 1,
});
For a mobile layout, a viewport can include mobile and touch properties:
await page.setViewport({
width: 390,
height: 844,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
});
Changing viewport-related mobile or touch settings can trigger a page reload in some cases, so configure them before navigating when possible. A larger deviceScaleFactor produces a higher-density capture and can increase the output dimensions and file size.
4. Choose what part of the page to capture
| Capture | Puppeteer option | Use it when |
|---|---|---|
| Current viewport | page.screenshot() |
The preview should show the layout visible at the chosen viewport dimensions. |
| Full document | page.screenshot({ fullPage: true }) |
The complete page is needed, rather than a bounded preview. |
| One element | elementHandle.screenshot() |
You need a property card or other component isolated from surrounding content. |
| Defined rectangle | page.screenshot({ clip: { x, y, width, height } }) |
You know the page coordinates and dimensions of the region to capture. |
Element screenshots are often convenient for listing previews because the selected component defines the crop. Puppeteer scrolls a hidden element into view before capturing it. A clipped screenshot instead uses page coordinates; check that the target region is within the rendered viewport and has the intended dimensions.
await page.screenshot({
path: 'property-region.png',
clip: { x: 80, y: 120, width: 640, height: 420 },
});
5. Wait for the right content, not just a generic page state
Puppeteer’s screenshot guide demonstrates navigation with waitUntil: 'networkidle2', which waits for a period with no more than two network connections. That can be useful, but it is not a guarantee that every listing image or dynamically populated field is ready. Some pages continue making requests or load content after navigation settles. Prefer a readiness condition tied to the content your preview must contain.
await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
await page.waitForSelector('.listing-card', { timeout: 30000 });
Other navigation states include load, domcontentloaded, and networkidle0. Choose based on the page’s behavior. A practical sequence is to wait for the document navigation to reach a useful baseline, then wait for a selector, image, or other concrete property of the listing. If the page uses a lazy-loaded image, ensure that the card has been brought into view and verify the image has loaded before capture.
To inspect the values that will appear in the image, validate the rendered title, price, location, primary image, and any branding against the desired preview composition. These checks are application-specific: the target portal determines the relevant selectors and content.
6. Select an output format and capture options
Puppeteer uses PNG by default. For JPEG, set type: 'jpeg' and a quality from 0 to 100. Quality applies to JPEG, not PNG. Use a format supported by the system that will display the image, and inspect the output at the size and quality your product requires.
// PNG (default)
await page.screenshot({ path: 'preview.png', type: 'png' });
// JPEG with quality from 0 to 100
await page.screenshot({ path: 'preview.jpg', type: 'jpeg', quality: 85 });
Other useful options include:
path: write the screenshot to a file. Without a path, Puppeteer returns image data.fullPage: capture beyond the viewport to the full document height.clip: capture a rectangular region.omitBackground: omit the default background when a transparent capture is appropriate and supported by the chosen output.
Do not set JPEG quality for PNG. Select output dimensions, format, and quality by checking the requirements of the destination that will store or display the preview.
7. cURL, Python, and Node.js alternatives
Puppeteer is a Node.js browser automation library, so the runnable do-it-yourself implementation above is in JavaScript. cURL and Python do not directly invoke Puppeteer’s browser API; they can call an HTTP screenshot service that performs browser capture for you. The following examples use ScreenshotNeo’s API and its documented endpoint. See the ScreenshotNeo API documentation for request parameters.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/property/listing -o shot.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com/property/listing"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com/property/listing',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', bytes));
8. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request can return a screenshot as PNG, JPEG, or WebP, or a PDF. It also supports full-page and element capture, viewport and device settings, waits, custom CSS and JavaScript, cookies, headers, caching, async jobs, bulk capture, and other options. Its parameter names used by other screenshot APIs also work. See ScreenshotNeo and the API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/property/listing -o shot.webp
ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 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 are not billed, and each response reports the page verdict and billing status in headers. Its 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 per month with no card; paid plans start at $5 for 3,000 screenshots. Every feature is on every plan.
Create a free ScreenshotNeo account for 1,000 screenshots a month, with no card required.
9. Troubleshooting
| Symptom | Likely cause | What to change |
|---|---|---|
| Navigation times out | The page keeps connections open or takes longer than the default timeout. | Use a suitable navigation state such as domcontentloaded, set an explicit timeout, and wait separately for the listing content you need. Do not assume network idle is appropriate for every page. |
| Selector wait times out | The selector is wrong, the listing is not present on that route, or content has not rendered. | Inspect the page DOM and replace the example selector. Confirm the listing is visible at the selected viewport. |
| Preview has a blank or missing property image | The image may be lazy-loaded or still loading when capture starts. | Wait for the relevant image to complete and have a nonzero natural width; bring the card into view if necessary. |
| The capture contains the wrong layout | The viewport was set after navigation or is not the desired desktop/mobile size. | Set dimensions and mobile/touch settings before navigating, then verify the resulting layout. |
| Element capture fails or crops unexpectedly | The selector matched no element, the target has zero size, or the component is not in the expected state. | Check the handle exists, inspect its bounding box, and wait for the component to render before calling its screenshot method. |
| Image is unexpectedly large | The capture uses a large viewport, full-page mode, or a high device scale factor. | Capture only the needed element or clip, and use dimensions and a supported output format suitable for the destination. |
| Transparent background is absent | The page itself paints a background or the capture option is unsupported for the chosen output behavior. | Use omitBackground where appropriate and inspect page styles and output format. |
| Browser process remains open after an error | The script exited before closing Chromium. | Put browser.close() in a finally block, as in the example. |
10. Performance, reliability, and cost
Each Puppeteer capture requires a browser page to navigate and render the target. Reuse a browser process for multiple captures when building a service, while creating isolated pages for separate jobs and ensuring each page is closed. Set timeouts and handle navigation and selector failures so one slow listing does not stall a batch indefinitely. Avoid waiting for a global network-idle state when a page’s request behavior makes it unreliable; wait for the specific content that determines a usable preview.
Capture only the area needed for the product surface. Full-page images and high-density viewports can contain more pixels and take longer to encode or transfer. JPEG quality trades image detail against file size; PNG does not use the quality option. Puppeteer itself has no per-screenshot API charge, but running Chromium consumes compute, memory, storage, and bandwidth in your environment. The actual cost depends on deployment and workload, which are outside the cited Puppeteer documentation.
ScreenshotNeo offers a hosted alternative with explicit plan quotas: Free provides 1,000 shots monthly, Starter is $5 for 3,000, Growth is $15 for 15,000, Pro is $39 for 60,000, Scale is $99 for 250,000, and Business is $249 for 1,000,000. Yearly billing gives two months free. Only clean shots are billed; the service identifies the verdict and billing status in response headers.
11. Frequently asked questions
Can I create previews from a URL that requires a login?
A Puppeteer page can use browser-context state such as cookies, but the login flow and permissions depend on the target portal. Do not capture or redistribute pages unless you have the necessary authorization.
Should I use a fixed preview size for every Indian property portal?
The research does not establish a universal size. Use the dimensions required by your own application or sharing surface, and confirm that each target page fits at that viewport.
Does networkidle2 mean the preview is complete?
No. It is a useful navigation condition shown in Puppeteer’s guide, but dynamic content or images can still require a target-specific wait.
Can Puppeteer capture only one property card?
Yes. Query the card element and call its screenshot() method. Use a selector that matches the page’s actual markup.
Sources
- Puppeteer screenshots guide — page and element capture examples, including navigation.
- Puppeteer ScreenshotOptions — capture scope and output options.
- Puppeteer Page.setViewport() — viewport settings and behavior.


