ScreenshotNeo

BlogHow-to

How to Add Custom JavaScript to a Page with Puppeteer

Use Puppeteer’s addScriptTag, evaluate, or evaluateOnNewDocument depending on whether you need to insert code, run a page operation, or act before site scripts.

By the ScreenshotNeo team4 October 20268 min read

Use Puppeteer’s page.addScriptTag() to add a script element to the current page. Use page.evaluate() to run a function in the page context and get its result. If your code must run before the site’s scripts, register it with page.evaluateOnNewDocument() before navigating.

These methods solve different timing and execution needs. The examples below use modern JavaScript modules and are runnable with Puppeteer installed.

1. Set up a Puppeteer page

Install Puppeteer in a new Node.js project:

npm install puppeteer

Save this as inject.js and run it with node inject.js. Puppeteer downloads a compatible browser as part of its installation. If you use puppeteer-core instead, provide an executable path or connect to a browser you manage.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: true });
try {
  const page = await browser.newPage();
  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
  console.log(await page.title());
} finally {
  await browser.close();
}

For predictable examples, replace https://example.com with a page you control. A site’s own security policy or page behavior can affect whether injected code works.

2. Insert inline JavaScript with addScriptTag

page.addScriptTag() inserts a <script> element into the main frame and resolves to a handle for that element. Use the content option for inline JavaScript:

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: true });
try {
  const page = await browser.newPage();
  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });

  const script = await page.addScriptTag({
    content: `
      window.customMessage = 'Added by Puppeteer';
      document.documentElement.dataset.automation = 'ready';
    `,
  });

  console.log('Inserted element:', await script.evaluate(el => el.tagName));
  console.log('Page value:', await page.evaluate(() => window.customMessage));
} finally {
  await browser.close();
}

This runs after navigation reaches domcontentloaded. It is suitable for adding functions, styles, instrumentation, or other script content when the page’s own scripts do not need to see the change first.

Pass data into injected code safely

When values come from Node.js, serialize them as data rather than concatenating arbitrary text into JavaScript source. One option is to pass JSON as a literal:

const config = { theme: 'dark', enabled: true };
const content = `window.widgetConfig = ${JSON.stringify(config)};`;
await page.addScriptTag({ content });

For values that may not serialize cleanly, use page.evaluate() and pass arguments instead.

3. Load a script from a URL or local file

Use the url option to add a remote script, or path for a local file. Relative paths resolve from the Node process working directory, process.cwd().

await page.addScriptTag({ url: 'https://example.test/custom.js' });

await page.addScriptTag({ path: './scripts/custom.js' });

Use type: 'module' when the inserted script is an ES module:

await page.addScriptTag({
  url: 'https://example.test/module.js',
  type: 'module',
});

The documented options are content, url, path, and type. Choose one source for the script; don’t combine conflicting sources. Remote loading depends on the URL being reachable and the page’s policies. A module script follows browser module loading rules, including module-origin and CORS requirements.

4. Run a function with evaluate and get its result

Use page.evaluate() when you want a one-off page operation or a value returned from the page. The function runs in the browser page context, not in Node.js. Pass values as arguments; Puppeteer waits if the function returns a promise.

const pageInfo = await page.evaluate(() => ({
  title: document.title,
  url: location.href,
  heading: document.querySelector('h1')?.textContent?.trim() ?? null,
}));
console.log(pageInfo);

Arguments are passed after the function:

const selector = 'h1';
const heading = await page.evaluate((sel) => {
  return document.querySelector(sel)?.textContent?.trim() ?? null;
}, selector);
console.log(heading);

Use serializable arguments and return values such as strings, numbers, arrays, and plain objects. DOM nodes and browser objects do not cross into Node.js as ordinary values; use an element handle if you need to keep a reference to a page element.

5. Run code before a page’s own scripts

Register a function with page.evaluateOnNewDocument() before navigation when it must run after a new document is created but before that document’s scripts execute. The registration applies to navigations and child frames that attach or navigate.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: true });
try {
  const page = await browser.newPage();
  const registrationId = await page.evaluateOnNewDocument(() => {
    Object.defineProperty(navigator, 'languages', {
      configurable: true,
      get: () => ['en-US', 'en'],
    });
  });

  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
  console.log(await page.evaluate(() => navigator.languages));

  // Remove the registration if it should not affect later documents.
  await page.removeScriptToEvaluateOnNewDocument(registrationId);
} finally {
  await browser.close();
}

Store the returned identifier if you may need to remove the registration. Removing it stops future document evaluations from that registration; it does not undo changes already made in the current document.

6. Choose the right method

Goal Method When it runs What you get
Insert inline, remote, or local script code page.addScriptTag() When called in the current page Handle to the inserted script element
Run a page operation and return data page.evaluate() When called in the current document Serialized result, with promises awaited
Set up code before site scripts page.evaluateOnNewDocument() For each new document after registration Registration identifier for later removal

These APIs do not guarantee that every site will accept every injection. In particular, site policy, page lifecycle, and the chosen frame can change the outcome.

7. Target the correct frame

page.addScriptTag() is a shortcut for adding the script to the main frame. An iframe has its own JavaScript context; evaluating code in the main frame does not change that iframe. Find the frame and call its frame-level method when the code belongs there.

const frame = page.frames().find(f => f.url().includes('/embedded/'));
if (!frame) {
  throw new Error('Target frame was not found');
}

await frame.addScriptTag({
  content: `window.frameFlag = true;`,
});
const result = await frame.evaluate(() => window.frameFlag);
console.log(result);

Frame URLs can change during navigation. If the frame appears asynchronously, wait for it or inspect page.frames() after the relevant navigation. Cross-origin frames still have separate execution contexts; use Puppeteer’s frame API to execute in the intended frame rather than trying to reach into it from the parent page.

8. Troubleshooting

Symptom Likely cause Fix
addScriptTag rejects or the script is absent Invalid local path, unreachable remote URL, or a browser/page loading error Check process.cwd(), confirm the file or URL is available to the browser, and inspect the thrown error and page console.
Injected code runs, but site code did not see it The injection happened after the site’s scripts ran Register the setup with evaluateOnNewDocument before goto().
A variable is undefined in Node.js The callback runs in the browser context, where Node globals such as process are not available Pass the required value as an argument to evaluate, or serialize it into the script content.
A change works on the top-level page but not an iframe The code ran in the main frame’s context Select the intended frame and use its evaluate or addScriptTag method.
The remote script does not load Network failure, an invalid URL, or browser policy such as CSP/CORS/module restrictions Verify the resource URL and browser console. If the policy blocks the load, use an allowed method on a page you control; Puppeteer’s API does not promise to bypass site policy.
The expected result is missing after navigation Navigation replaced the document, or asynchronous page code had not completed Register pre-document code before navigating; otherwise wait for the specific selector or application state before evaluating.
Execution context was destroyed A navigation or frame replacement occurred while evaluation was in progress Wait for the intended navigation to finish, then evaluate in the new context. Avoid carrying element handles across document changes.
The returned value is empty or unusable The result is not serializable or the function returned before asynchronous work finished Return a plain serializable value and return/await the promise from the evaluated function.

During diagnosis, listen for browser output and page errors:

page.on('console', message => console.log('PAGE:', message.type(), message.text()));
page.on('pageerror', error => console.error('PAGE ERROR:', error));

9. Performance, reliability, and cost

  • Keep injection small. Every added script can trigger more page work. Avoid repeating the same insertion on every operation when a single setup is enough.
  • Wait for a condition, not an arbitrary long delay. Use a selector or application-specific readiness signal when subsequent code depends on rendered content.
  • Clean up registrations. Remove pre-document registrations when they should not apply to later navigations in a reused page.
  • Close browser resources. Use try/finally so the browser closes even when navigation or injection fails.
  • Plan for page variation. Network state, navigation redirects, frames, and page updates can change timing. Make automation idempotent where possible and handle missing elements explicitly.
  • Cost depends on your runtime. Puppeteer itself is a Node.js browser automation library; the browser and machine or hosting environment you run it on determine infrastructure use. This research does not establish a universal runtime price or speed.

10. Or skip the browser setup

If your goal is a clean screenshot rather than executing custom page code, ScreenshotNeo returns an image or PDF from one GET request. It removes 60+ known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Only clean shots are billed, and responses say whether a result was billed, including bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits. Its MCP server gives AI agents tools for screenshots, page information, and PDFs. The free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 shots. Every feature is on every plan. See the API documentation.

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

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

For custom capture settings and parameter details, read the ScreenshotNeo docs. Sign up free for 1,000 screenshots a month, no card required.

Frequently asked questions

Does addScriptTag return the script’s output?

No. It returns a handle to the inserted script element. Use page.evaluate() to read a value from page context.

Can I run a script before the first page load?

Yes. Call page.evaluateOnNewDocument() before page.goto() so the function runs in the new document before its page scripts.

Can injected code access Node.js modules?

No. The callback runs in the browser context. Pass data from Node.js explicitly and keep browser code separate from Node.js code.

Does this work on every website?

Puppeteer documents how to execute or insert code, but it does not guarantee success on every site. The target’s policies, frame, navigation, and runtime behavior matter.