How to Convert a Web App Page to PDF After Logging In with Puppeteer
Log in through your app’s supported flow, verify the session, and save the protected page as a PDF with Puppeteer. Includes print options and fixes for common failures.
To save a protected web app page as a PDF with Puppeteer, use the authentication method that the app supports, navigate to the page, confirm that the authenticated content is present, then call page.pdf(). For a normal app login, submit the login form or restore a valid session cookie; page.authenticate() is for HTTP authentication and does not fill in a website’s login form.
The example below shows a form-based login followed by a PDF download. Replace the URL, selectors, and post-login readiness condition with those used by your app. Puppeteer’s PDF generation guide documents saving a PDF with page.pdf(); its authentication API and cookie guide cover other session mechanisms.
1. Install Puppeteer and prepare credentials
Start a Node.js project and install Puppeteer:
npm init -y
npm install puppeteer
Set credentials in the environment instead of placing them in source code. For example, in a Unix-like shell:
export APP_USERNAME='your-username'
export APP_PASSWORD='your-password'
Use your deployment platform’s secret manager or equivalent in automated jobs. Do not print credentials or session cookies to logs. The example assumes the login form has username and password fields and a submit button; use the app’s actual selectors.
2. Log in, confirm the target page, and save the PDF
This runnable ES module uses Puppeteer’s locator API. The post-login URL check and report heading check are examples; replace them with a reliable success signal for your app. Some single-page apps update the URL without a full navigation, so the code waits for the report heading after submitting rather than relying only on a navigation event.
import puppeteer from 'puppeteer';
const username = process.env.APP_USERNAME;
const password = process.env.APP_PASSWORD;
if (!username || !password) {
throw new Error('Set APP_USERNAME and APP_PASSWORD first');
}
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
page.setDefaultTimeout(15_000);
await page.goto('https://app.example.com/login', {
waitUntil: 'domcontentloaded',
});
await page.locator('input[name="username"]').fill(username);
await page.locator('input[name="password"]').fill(password);
await page.locator('button[type="submit"]').click();
// Replace this with a stable, app-specific authenticated-page signal.
await page.locator('[data-testid="account-menu"]').wait();
await page.goto('https://app.example.com/report', {
waitUntil: 'domcontentloaded',
});
await page.locator('h1[data-testid="report-title"]').wait();
await page.pdf({
path: 'report.pdf',
format: 'A4',
printBackground: true,
});
console.log('Saved report.pdf');
} finally {
await browser.close();
}
Save this as save-report.mjs and run node save-report.mjs. The browser closes in the finally block even if navigation, login, or PDF generation fails. The example is a pattern, not a tested integration for a specific app. Sites differ in selectors, redirects, MFA, SSO, and rendering behavior.
3. Choose the authentication method your app uses
App login form
For a username-and-password form, navigate to the login page, fill its controls, submit, then wait for a site-specific authenticated state. Some apps require a CSRF token, an email verification step, MFA, or an identity-provider redirect. Follow the app’s intended authentication flow; a browser script cannot safely or reliably skip those requirements.
For navigation-triggered form submissions, waiting for navigation and clicking can be coordinated with Promise.all so the event listener is ready before the click:
await Promise.all([
page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
page.locator('button[type="submit"]').click(),
]);
Use this pattern only if submitting causes a document navigation. For client-side routing, wait for the new route or authenticated content instead; otherwise a navigation wait may time out even when login succeeded.
HTTP Basic or Digest authentication
When the server requests HTTP authentication, use page.authenticate() before navigating:
await page.authenticate({
username: process.env.HTTP_USERNAME,
password: process.env.HTTP_PASSWORD,
});
await page.goto('https://protected.example.com/report');
await page.pdf({ path: 'report.pdf' });
This does not interact with an app’s HTML login form. Puppeteer documents that enabling page.authenticate() turns on request interception behind the scenes, which may affect performance. See the Page.authenticate() reference.
Restore an existing cookie session
If you have a valid session cookie obtained through an approved login flow, you can set it on a page before navigating:
await page.setCookie({
name: 'session',
value: process.env.APP_SESSION_COOKIE,
domain: 'app.example.com',
path: '/',
secure: true,
httpOnly: true,
});
await page.goto('https://app.example.com/report');
Replace the cookie name and attributes with the actual cookie’s scope and requirements. A cookie may be expired, scoped to another host or path, or only one part of a session that also depends on server-side state or other credentials. Puppeteer documents browser cookie operations in its cookie guide and Browser.setCookie() reference. Treat session cookies as credentials: protect them, limit their lifetime, and avoid sharing them between unrelated jobs.
4. Tune the PDF output
page.pdf() renders using print CSS media by default. That means the result can differ from what a user sees in the browser window: print styles may hide navigation, change layout, or omit backgrounds. The Page API documents media emulation, and the PDFOptions reference documents output settings.
| Option | What it controls | Default or note |
|---|---|---|
path |
File path where Puppeteer writes the PDF | Relative paths resolve from the current working directory. |
format |
Named paper size, such as A4 or Letter |
Defaults to Letter; takes precedence over width and height. |
width, height |
Custom paper dimensions | Use when a named format does not fit the document. |
margin |
Top, right, bottom, and left page margins | No margins are set when omitted. |
printBackground |
Whether to include background graphics | Defaults to false. |
pageRanges |
Subset of pages, for example 1-5, 8 |
Defaults to all pages. |
preferCSSPageSize |
Whether CSS @page size takes priority |
By default, content is scaled to fit the configured paper size. |
waitForFonts |
Wait for fonts before rendering | Defaults to true. |
timeout |
PDF generation timeout in milliseconds | Defaults to 30,000 ms; 0 disables it. |
Use print or screen styling
To use the page’s print styles, leave the media type unchanged. To render screen styles instead, emulate screen media before generating the PDF:
await page.emulateMediaType('screen');
await page.pdf({ path: 'report.pdf', format: 'A4', printBackground: true });
When exact printed colors matter, the PDF guide notes that CSS -webkit-print-color-adjust can request exact colors. For example, the page’s stylesheet can include * { -webkit-print-color-adjust: exact; }. This affects print rendering; test it with the app’s own styles.
5. Handle dynamic pages and long reports
Navigation completion does not always mean the report is ready. A page can continue fetching data, render charts later, or load images only when they approach the viewport. Wait for a meaningful app-specific element or state before calling page.pdf(). A fixed delay can help with a known animation or delayed render, but a selector or explicit application readiness condition is usually more reliable than guessing a delay.
PDF generation waits for fonts by default. If a custom font or image never loads, the output may be delayed or may contain a fallback. Large pages and long reports also take more time and memory to render; narrow the content or page range when possible. The PDF options reference documents the generation timeout and font-wait behavior.
If the app uses a single-page route, navigate to it after confirming login and wait for the content state. If the page is behind an SSO popup or requires user interaction that Puppeteer cannot complete unattended, use the app’s supported automation or service-account path where available. Do not assume an interactive login can be replaced by copying one cookie.
6. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The PDF contains the login page | The login failed, session expired, cookie scope is wrong, or target navigation redirected to login. | Check the final URL and wait for a known authenticated marker before generating the PDF. Verify credentials and session validity. |
waitForNavigation() times out |
The app submitted through client-side routing and did not perform a document navigation. | Wait for the route change or an authenticated element instead of a navigation event. |
| Selector wait times out | The selector is incorrect, the page is not authenticated, or the content has not rendered. | Inspect the actual DOM and replace the placeholder selector with a stable app-specific selector. Confirm the page URL and login state. |
| PDF is missing colors or backgrounds | Background printing is disabled by default, or print CSS changes the appearance. | Set printBackground: true. If screen styling is desired, call emulateMediaType('screen') before PDF generation. |
| PDF layout differs from the browser | Print media is active by default; paper size, margins, or CSS @page may also change the layout. |
Choose print or screen media deliberately; tune format, dimensions, margins, and preferCSSPageSize. |
| PDF omits recent data or charts | PDF generation started before the application finished rendering. | Wait for the report’s completion indicator or a known data element. Avoid relying solely on networkidle for apps with persistent requests. |
| Fonts look wrong or generation is slow | Fonts are still loading or the report is large. | Check font availability and page readiness. Keep waitForFonts enabled unless you have a reason to disable it; increase the timeout only when longer rendering is expected. |
| Cookie injection does not authenticate | The cookie may be expired, have the wrong domain/path/security attributes, or be insufficient for the app’s session design. | Use a valid cookie from the app’s supported login flow, set matching scope, and verify an authenticated marker after navigation. |
| HTTP authentication is unexpectedly slow | page.authenticate() enables request interception. |
Use it only for HTTP authentication. For app form login, use the form flow; compare timing in the actual environment. |
| Browser process remains open after an error | Cleanup was skipped on a failing code path. | Put browser.close() in a finally block as in the example. |
7. Reliability, performance, and cost
For repeatable output, use a stable readiness signal, explicit paper settings, and a fresh browser context for each independent account or job. Always close the browser on success and failure. Keep authentication data out of logs and generated artifacts. Browser-based PDF generation consumes browser process time and memory, especially for long reports, large images, or complex charts; control concurrency and avoid opening more pages than the worker can render comfortably.
There is no single reliable wait duration for every web app. Network quiet can be misleading when a site keeps polling, while a short delay can be too short for a slow report. Prefer app-specific readiness checks, and set a timeout that fits the expected report size. Puppeteer PDF rendering follows print media by default, so choose screen emulation only when that better matches the required document.
With a self-hosted Puppeteer flow, your main costs are the compute and operational work of running the browser and maintaining the authentication path; the research sources do not specify a hosting price. If you only need a screenshot or PDF capture endpoint, ScreenshotNeo is a managed alternative described below.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. For a public page, one GET request returns an image or PDF. Its API does not log into protected app sessions, so use the Puppeteer workflow above when the target requires your authenticated browser session.
For a URL that does not require login, this cURL call saves a WebP screenshot. See the ScreenshotNeo API documentation for PDF and capture options.
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)
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}`);
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 and CAPTCHAs, blank pages, failed loads, timeouts, and cache hits cost nothing, and response headers identify the page verdict and whether the request was billed. 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 shots a month with no card; paid plans start at $5 for 3,000. Every feature is on every plan. It also supports PDF output and many capture options; see the docs for the full parameter list.
Sign up free for 1,000 screenshots a month with no card.
FAQ
Can Puppeteer print a page that requires login?
Yes, if the browser session completes the app’s supported authentication flow and remains authenticated when it opens the target page. Verify the protected content before generating the PDF.
Does page.authenticate() submit an app login form?
No. It handles HTTP authentication challenges. A web app form needs its own form submission or another session mechanism the app supports.
Why does the PDF look different from the logged-in page?
Puppeteer uses print CSS media by default, and PDF paper settings can change layout. Emulate screen media if the screen design is the intended output.
Can ScreenshotNeo create a PDF of the protected page?
ScreenshotNeo supports PDF capture, but this article’s authenticated app flow uses the browser session established by Puppeteer. Use ScreenshotNeo for pages its request can access without that logged-in session.


