DriversRecommendedOutdated drivers can make a good PC feel brokenScan driver issues before chasing fixes manually.Scan NowOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsWindows FixRecommendedWindows errors stealing your time? Find the fix fastScan stability, cleanup and performance issues.Fix Now×
Skip to content
EJS

How to Screenshot an EJS Template with Puppeteer, Node.js, and Express

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Render the EJS view through an Express route, open that route in Puppeteer, wait until the content you need is ready, and call page.screenshot(). This captures the HTML Express actually serves, including the template data and client-side rendering that a visitor would see.

How the screenshot flow works

EJS turns a template and its local values into HTML. Express makes that rendered HTML available through a route, and Puppeteer controls a browser to visit the route and capture its pixels. Keeping those steps separate makes it easier to debug template output independently from browser capture.

  1. Configure Express to use EJS and point it at the directory containing your views.
  2. Create a route that calls res.render() with a known template name and the data that template needs.
  3. Start the Express server and wait until it is listening.
  4. Launch Puppeteer, set the browser viewport, navigate to the route, and wait for the relevant content.
  5. Save the screenshot and close the browser, including when navigation or capture throws an error.

Install EJS and Puppeteer

In an existing Node.js project, install Express, EJS, and Puppeteer:

npm install express ejs puppeteer

The Puppeteer documentation version 25.12.0 describes npm i puppeteer as the standard installation and says the package downloads a recent Chrome for Testing and a compatible chrome-headless-shell. The documented download estimates are approximately 170 MB on macOS, 282 MB on Linux, and 280 MB on Windows; actual disk requirements can vary by environment.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

If package installation scripts are disabled, Puppeteer may not download its browser. The documented manual installation command is npx puppeteer browsers install. If you choose puppeteer-core instead, it does not download Chrome: you are responsible for providing an installed browser or a remote browser connection and configuring Puppeteer to use it.

Configure Express to render an EJS view

Express needs a view directory and a view engine. With the conventional views directory at the project root, a minimal server can look like this:

const express = require('express');

const app = express();
app.set('views', './views');
app.set('view engine', 'ejs');

app.get('/preview', (req, res) => {
  res.render('report', {
    title: 'Quarterly report',
    rows: [
      { label: 'Revenue', value: '$24,000' },
      { label: 'Expenses', value: '$9,000' },
    ],
  });
});

app.listen(3000, () => {
  console.log('Preview app listening on http://localhost:3000');
});

Save this as server.js. Create views/report.ejs:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title><%= title %></title>
    <style>
      body { font: 16px system-ui, sans-serif; margin: 0; padding: 32px; }
      main { max-width: 720px; margin: auto; }
      li { padding: 8px 0; }
    </style>
  </head>
  <body>
    <main>
      <h1><%= title %></h1>
      <ul>
        <% for (const row of rows) { %>
          <li><strong><%= row.label %>:</strong> <%= row.value %></li>
        <% } %>
      </ul>
    </main>
  </body>
</html>

In the template, <%= value %> emits HTML-escaped output. EJS’s <%- value %> emits output without escaping, which is useful for trusted markup such as an include but unsafe for untrusted values unless they have been appropriately sanitized. Validate route inputs and keep the template name controlled by your application. EJS warns that rendering with unchecked user input leaves the application responsible for the result; do not give users unrestricted access to rendering.

Capture the Express route with Puppeteer

Run the server in one terminal with node server.js. Once it reports that it is listening, run this capture script from the project directory as screenshot.js:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const puppeteer = require('puppeteer');

async function main() {
  const browser = await puppeteer.launch();
  try {
    const page = await browser.newPage();
    await page.setViewport({ width: 1280, height: 900 });
    await page.goto('http://localhost:3000/preview', {
      waitUntil: 'networkidle2',
    });
    await page.screenshot({ path: 'preview.png', fullPage: true });
  } finally {
    await browser.close();
  }
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Start it with node screenshot.js. The route must be reachable when page.goto() runs; launching Puppeteer does not start your Express server. Setting the viewport before navigation matters when responsive layout affects the result. Puppeteer notes that changing the viewport can resize the page and may cause a reload in some cases.

networkidle2 is the wait condition shown in Puppeteer’s screenshot guide, not a guarantee that every application has finished rendering. Pages with persistent requests or delayed client-side work may need a more specific readiness check.

Choose what to capture

Viewport or full page

By default, a screenshot covers the current viewport. Set fullPage: true to capture the full document height, as in the example. Use viewport capture when the desired image is a browser-window view; use full-page capture for a report or page that should appear in one tall image.

A single element

Wait for the target element, then capture its element handle. Puppeteer scrolls an element into view by default when taking an element screenshot.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const card = await page.waitForSelector('.report-card');
if (!card) throw new Error('Report card was not found');
await card.screenshot({ path: 'report-card.png' });

Use a selector that identifies the intended component uniquely. If a page renders the component asynchronously, waiting for its selector is generally a more meaningful condition than assuming a fixed delay is enough.

File format, clipping, and background

The screenshot path determines where the image is written; relative paths resolve from the process’s current working directory. Puppeteer uses the file extension to infer the screenshot type, and PNG is the default. The type option can select an image format. For a region of the page, provide a clip rectangle; to omit the default white background and allow transparency, use omitBackground: true.

await page.screenshot({
  path: 'transparent.png',
  type: 'png',
  omitBackground: true,
});

Render HTML directly instead of navigating to Express

If you already have rendered HTML and do not need to exercise the HTTP route, Puppeteer supports setting page content directly:

const html = '<main><h1>Preview</h1></main>';
await page.setContent(html);
await page.screenshot({ path: 'preview.png' });

This route is useful for an isolated markup capture, but it skips the real Express request path. For screenshots of the page your application serves, navigate to its route so the browser also loads the page’s linked stylesheets, scripts, and assets in their normal context.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Wait for the right readiness condition

The correct wait depends on how the EJS page works. A page whose HTML is complete in the initial response may need only navigation to finish. If browser JavaScript fills in a chart, fetches data, or reveals a component later, wait for the application-specific signal that means the screenshot content is ready.

  • For a component that appears after rendering, use page.waitForSelector('.report-card').
  • For a fixed, known delay in a controlled workflow, use a delay sparingly; it can be either unnecessarily slow or too short under load.
  • For pages with ongoing network requests, do not assume networkidle2 will represent visual completion. Prefer a selector or other application-specific readiness condition.

Waiting for the main selector before capture is also helpful when the view can legitimately return an empty state. Choose a selector that exists in both expected states if you need to capture either one.

Common failures and fixes

Navigation fails with a connection error

Cause: Express is not running, has not started listening yet, or the script is using the wrong port or route. Fix: start the server first, confirm the URL in a browser, then make the URL passed to page.goto() match the server’s port and route.

Puppeteer reports that Chrome is missing

Cause: browser download scripts may have been blocked, or a separately managed browser was not configured. Fix: with Puppeteer, run npx puppeteer browsers install. With puppeteer-core, install or connect to a compatible browser and configure its executable path or channel.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The screenshot is blank or missing template data

Cause: the route may be returning an error, rendering a different view, or waiting on client-side content that has not appeared. Fix: open the route directly in a browser and inspect the rendered page; check the view filename, Express locals, and readiness selector before changing the screenshot options.

The page is cut off

Cause: the default screenshot captures only the viewport. Fix: set fullPage: true for the entire document, or use an element screenshot when only one component is needed.

Layout differs from the expected design

Cause: capture dimensions or device pixel behavior differ from the intended browser view, or viewport was set after navigation. Fix: set the viewport before page.goto(), use dimensions appropriate to the design, and ensure the page has finished loading its fonts, styles, and relevant content before capture.

Capture hangs or times out

Cause: navigation may be waiting on a persistent request or the server may not be responding. Fix: verify the route is responsive and replace a broad network-idle wait with a specific selector or application readiness condition when the page keeps network activity open.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Reliability, performance, and cost considerations

Launching a browser for every image is straightforward, but adds browser startup and shutdown work. For one-off captures, a single launch with cleanup in finally is simple and robust. In a service that takes many screenshots, browser lifecycle, concurrency, timeouts, and memory use become operational concerns: avoid unbounded simultaneous pages, close pages and browsers when work completes, and test your chosen limits in the environment where the service runs.

Puppeteer’s browser download is a significant installation component, with the version-specific estimates described above. If deployment policy or image size makes bundling that browser unsuitable, puppeteer-core can use a browser managed separately, but that shifts browser compatibility and availability responsibility to your deployment.

For failures, distinguish an app-render error from a capture error. Test the Express route on its own first, then add Puppeteer. Log the route URL and the phase that failed, and ensure cleanup runs even when navigation or screenshot writing throws. This narrows diagnosis without turning transient or incomplete pages into apparently successful images.

Or skip the browser setup

If you want an API to capture a URL rather than install and manage a local browser, ScreenshotNeo takes a screenshot in one GET request. Its API can return PNG, JPEG, WebP, or PDF; the request below saves the result as WebP. See the ScreenshotNeo API documentation for request options.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://your-site.example/preview -o shot.webp

ScreenshotNeo removes supported cookie and consent banners, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status in headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.

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

Frequently asked questions

Can Puppeteer take a screenshot of an EJS file without Express?

Yes, if you render the template to HTML yourself and provide that markup to the page. For an application using Express, navigating to its rendered route is the more representative way to capture the delivered page.

Does EJS itself take the screenshot?

No. EJS produces HTML from a template and locals. Puppeteer controls the browser and captures the rendered page.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Where does Puppeteer save a relative screenshot path?

Relative screenshot paths resolve from the Node.js process’s current working directory, not automatically from the directory containing the EJS view.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Read next

Recommended PC Tool
Recommended PC Tool
Crashes, No Sound, or Screen Glitches?Free driver scan
Windows Errors? Fix Them Before They SpreadFree repair scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.