Using Puppeteer with Scrnify for Advanced Screenshot Workflows
3/29/2025
Hey there, Laura and Heidi here from SCRNIFY! š¦š¹
Ever found yourself wrestling with browser automation for screenshots? Maybe you've set up a complex Puppeteer infrastructure, only to watch it crumble under real-world load? Or perhaps you're tired of maintaining Chrome installations across different environments? We've definitely been there! š
As developers who've built a screenshot API service, we know firsthand the challenges of managing screenshot automation at scale. That's why we're excited to show you how combining Puppeteer with SCRNIFY can give you the best of both worlds - the flexibility of local browser automation with the scalability of a cloud API.
In this tutorial, we'll explore how to create advanced screenshot workflows by integrating Puppeteer with SCRNIFY. You'll learn how to offload the heavy lifting of browser management while maintaining full control over your screenshot process. Let's dive in!
What Makes Advanced Screenshot Workflows with Scrnify and Puppeteer?
Before we start coding, let's understand what we mean by "advanced workflows" in this context:
- Local Pre-processing: Using Puppeteer locally to manipulate pages, inject scripts, or handle authentication
- Cloud Rendering: Delegating the actual screenshot rendering to Scrnify's API
- Custom Post-processing: Processing the resulting screenshots locally with specialized libraries
- Hybrid Approaches: Intelligently deciding when to use local Puppeteer vs. Scrnify API
This approach gives you incredible flexibility while eliminating the infrastructure headaches. Let's set up our project to see how it works.
Prerequisites
Before we begin, make sure you have:
- Node.js installed (version 14+ recommended)
- Basic familiarity with JavaScript and async/await syntax
- Puppeteer installed in your project
- A Scrnify API key (sign up for the beta to get yours!)
Setting Up Your Project
Let's start by creating a new Node.js project and installing the necessary dependencies:
# Create a new directory for our project
mkdir puppeteer-scrnify-advanced
cd puppeteer-scrnify-advanced
# Initialize a new Node.js project
npm init -y
# Install dependencies
npm install puppeteer axios dotenv sharp fs-extra
Now, let's create a .env
file to store our Scrnify API key:
SCRNIFY_API_KEY=your_api_key_here
Next, create an index.js
file which will be our main entry point:
// index.js
require('dotenv').config();
const puppeteer = require('puppeteer');
const axios = require('axios');
const fs = require('fs-extra');
const sharp = require('sharp');
const path = require('path');
// Create output directory
fs.ensureDirSync(path.join(__dirname, 'screenshots'));
// We'll add our code here
Local Setup: Configuring Puppeteer
Now, let's create a basic setup for Puppeteer that we'll use throughout our examples:
// puppeteer-setup.js
const puppeteer = require('puppeteer');
/**
* Creates a configured Puppeteer browser instance
* @param {Object} options - Configuration options
* @returns {Promise<Browser>} - Puppeteer browser instance
*/
async function setupBrowser(options = {}) {
const defaultOptions = {
headless: 'new', // Use the new headless mode
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--disable-gpu',
],
};
const browser = await puppeteer.launch({
...defaultOptions,
...options,
});
return browser;
}
/**
* Creates a new page with default viewport and settings
* @param {Browser} browser - Puppeteer browser instance
* @param {Object} options - Page configuration options
* @returns {Promise<Page>} - Configured Puppeteer page
*/
async function setupPage(browser, options = {}) {
const defaultOptions = {
viewport: { width: 1920, height: 1080 },
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
timeout: 30000,
};
const mergedOptions = { ...defaultOptions, ...options };
const page = await browser.newPage();
// Configure page
await page.setViewport(mergedOptions.viewport);
await page.setUserAgent(mergedOptions.userAgent);
page.setDefaultTimeout(mergedOptions.timeout);
return page;
}
module.exports = {
setupBrowser,
setupPage,
};
This module provides reusable functions for setting up Puppeteer with sensible defaults, which we'll use in our examples.
Creating a Scrnify API Client
Next, let's create a simple client for the Scrnify API:
// scrnify-client.js
const axios = require('axios');
const fs = require('fs-extra');
const path = require('path');
class ScrnifyClient {
constructor(apiKey) {
if (!apiKey) {
throw new Error('Scrnify API key is required');
}
this.apiKey = apiKey;
this.baseUrl = 'https://api.scrnify.com/capture';
}
/**
* Take a screenshot using the Scrnify API
* @param {Object} options - Screenshot options
* @returns {Promise<Buffer>} - Screenshot data as Buffer
*/
async takeScreenshot(options = {}) {
const defaultOptions = {
type: 'image',
format: 'png',
width: 1920,
height: 1080,
fullPage: false,
cache_ttl: 0, // No caching by default
};
const params = new URLSearchParams({
...defaultOptions,
...options,
key: this.apiKey,
});
try {
const response = await axios({
method: 'get',
url: `${this.baseUrl}?${params.toString()}`,
responseType: 'arraybuffer',
});
return Buffer.from(response.data, 'binary');
} catch (error) {
if (error.response) {
const errorData = Buffer.from(error.response.data).toString('utf-8');
throw new Error(`Scrnify API error (${error.response.status}): ${errorData}`);
}
throw error;
}
}
/**
* Take a screenshot and save it to a file
* @param {Object} options - Screenshot options
* @param {string} outputPath - Path to save the screenshot
* @returns {Promise<string>} - Path to the saved screenshot
*/
async takeAndSaveScreenshot(options, outputPath) {
const screenshotBuffer = await this.takeScreenshot(options);
await fs.writeFile(outputPath, screenshotBuffer);
return outputPath;
}
}
module.exports = ScrnifyClient;
This client handles the communication with the Scrnify API and provides methods for taking and saving screenshots.
Advanced Workflow #1: Hybrid Authentication Approach
One common scenario involves authenticated content. Since Scrnify doesn't support setting cookies directly, we'll use a hybrid approach where Puppeteer handles the authentication and takes screenshots of authenticated content:
// auth-workflow.js
require('dotenv').config();
const path = require('path');
const { setupBrowser, setupPage } = require('./puppeteer-setup');
const ScrnifyClient = require('./scrnify-client');
/**
* Demonstrates a hybrid approach for handling authenticated content
*/
async function authScreenshotWorkflow() {
const browser = await setupBrowser();
const scrnify = new ScrnifyClient(process.env.SCRNIFY_API_KEY);
try {
console.log('š Starting authenticated screenshot workflow...');
// 1. Use Puppeteer to log in
const page = await setupPage(browser);
await page.goto('https://example.com/login', { waitUntil: 'networkidle2' });
// Fill login form (replace with actual selectors and credentials)
await page.type('#username', 'your-username');
await page.type('#password', 'your-password');
await page.click('#login-button');
// Wait for login to complete
await page.waitForNavigation({ waitUntil: 'networkidle2' });
console.log('ā
Successfully logged in');
// 2. Take screenshot of authenticated content with Puppeteer
const authOutputPath = path.join(__dirname, 'screenshots', 'authenticated-dashboard.png');
await page.goto('https://example.com/dashboard', { waitUntil: 'networkidle2' });
await page.screenshot({
path: authOutputPath,
fullPage: true
});
console.log(`ā
Authenticated screenshot saved to ${authOutputPath}`);
// 3. Use Scrnify for public pages that don't require authentication
const publicOutputPath = path.join(__dirname, 'screenshots', 'public-page.png');
await scrnify.takeAndSaveScreenshot({
url: 'https://example.com/public-page',
type: 'image',
format: 'png',
width: 1920,
height: 1080,
fullPage: true,
}, publicOutputPath);
console.log(`ā
Public page screenshot saved to ${publicOutputPath}`);
} catch (error) {
console.error('Error in workflow:', error);
} finally {
await browser.close();
}
}
// Run the workflow
authScreenshotWorkflow().catch(console.error);
This hybrid approach uses Puppeteer for authenticated content and Scrnify for public pages, giving you the best of both worlds.
Advanced Workflow #2: Dynamic Content Generation and Screenshots
Another powerful workflow is generating dynamic content locally with Puppeteer and then taking screenshots with Scrnify:
// dynamic-content-workflow.js
require('dotenv').config();
const path = require('path');
const fs = require('fs-extra');
const { setupBrowser, setupPage } = require('./puppeteer-setup');
const ScrnifyClient = require('./scrnify-client');
/**
* Demonstrates generating dynamic content with Puppeteer
* and taking screenshots with Scrnify
*/
async function dynamicContentWorkflow() {
const browser = await setupBrowser();
const scrnify = new ScrnifyClient(process.env.SCRNIFY_API_KEY);
const tempDir = path.join(__dirname, 'temp');
try {
await fs.ensureDir(tempDir);
console.log('šØ Starting dynamic content workflow...');
// 1. Generate dynamic HTML content with Puppeteer
const page = await setupPage(browser);
// Create dynamic content based on data
// For this example, we'll create a simple chart using HTML/CSS
await page.setContent(`
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
.chart { display: flex; align-items: flex-end; height: 300px; }
.bar { width: 50px; margin-right: 10px; background: linear-gradient(to top, #3498db, #2980b9); }
.bar-label { text-align: center; margin-top: 10px; }
</style>
</head>
<body>
<h1>Dynamic Sales Report - ${new Date().toLocaleDateString()}</h1>
<div class="chart">
${[75, 120, 90, 180, 220, 150, 100].map((value, index) => `
<div>
<div class="bar" style="height: ${value}px;"></div>
<div class="bar-label">Day ${index + 1}</div>
</div>
`).join('')}
</div>
</body>
</html>
`);
// 2. Save the HTML to a temporary file
const htmlPath = path.join(tempDir, 'dynamic-report.html');
const htmlContent = await page.content();
await fs.writeFile(htmlPath, htmlContent);
console.log(`ā
Dynamic content generated and saved to ${htmlPath}`);
// 3. Take screenshot of the HTML file with Scrnify
const outputPath = path.join(__dirname, 'screenshots', 'dynamic-report.png');
// For a file:// URL, we'd typically use Scrnify, but it might not support local file:// URLs
// So we'll use Puppeteer in this case
await page.screenshot({
path: outputPath,
fullPage: true
});
console.log(`š Screenshot saved to ${outputPath}`);
// 4. Alternatively, host the file temporarily or use a data URI
// This is a more advanced approach if you need to use Scrnify for this
} catch (error) {
console.error('Error in workflow:', error);
} finally {
// Clean up temporary files
await fs.remove(tempDir);
await browser.close();
}
}
// Run the workflow
dynamicContentWorkflow().catch(console.error);
This workflow demonstrates how to generate dynamic content with Puppeteer and capture it - a powerful approach for reports and visualizations.
Advanced Workflow #3: Multi-Device Screenshots with Post-Processing
With this workflow, we'll show how to take screenshots of a website on multiple devices and post-process them to create a composite image:
// multi-device-workflow.js
require('dotenv').config();
const path = require('path');
const fs = require('fs-extra');
const sharp = require('sharp');
const ScrnifyClient = require('./scrnify-client');
/**
* Takes screenshots of a website on multiple devices and creates a composite image
*/
async function multiDeviceWorkflow() {
const scrnify = new ScrnifyClient(process.env.SCRNIFY_API_KEY);
const outputDir = path.join(__dirname, 'screenshots');
try {
console.log('š± Starting multi-device screenshot workflow...');
// Define device configurations
const devices = [
{ name: 'desktop', width: 1920, height: 1080, userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' },
{ name: 'tablet', width: 768, height: 1024, userAgent: 'Mozilla/5.0 (iPad; CPU OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1' },
{ name: 'mobile', width: 375, height: 812, userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1' },
];
const targetUrl = 'https://example.com';
const screenshotPaths = [];
// 1. Take screenshots for each device configuration
for (const device of devices) {
console.log(`šø Taking screenshot for ${device.name}...`);
const outputPath = path.join(outputDir, `${device.name}.png`);
await scrnify.takeAndSaveScreenshot({
url: targetUrl,
type: 'image',
format: 'png',
width: device.width,
height: device.height,
fullPage: false,
// Set user agent through the userAgent parameter if supported by Scrnify
// Otherwise, we'd need to use Puppeteer for specific device emulation
}, outputPath);
screenshotPaths.push(outputPath);
console.log(`ā
${device.name} screenshot saved`);
}
// 2. Create a composite image with sharp
console.log('š¼ļø Creating composite image...');
// Adjust as needed to create your desired layout
const compositeImage = await sharp({
create: {
width: 2400,
height: 1200,
channels: 4,
background: { r: 245, g: 245, b: 245, alpha: 1 }
}
})
// Place desktop screenshot (scaled down)
.composite([{
input: await sharp(screenshotPaths[0]).resize(1600, null).toBuffer(),
top: 100,
left: 100
},
// Place tablet screenshot
{
input: await sharp(screenshotPaths[1]).resize(500, null).toBuffer(),
top: 300,
left: 1800
},
// Place mobile screenshot
{
input: await sharp(screenshotPaths[2]).resize(300, null).toBuffer(),
top: 700,
left: 1900
}])
.toBuffer();
// Save the composite image
const compositePath = path.join(outputDir, 'responsive-composite.png');
await fs.writeFile(compositePath, compositeImage);
console.log(`š Composite image saved to ${compositePath}`);
} catch (error) {
console.error('Error in workflow:', error);
}
}
// Run the workflow
multiDeviceWorkflow().catch(console.error);
This workflow demonstrates taking screenshots on multiple devices using Scrnify and then using Sharp to create a composite image that showcases the responsive design.
Advanced Workflow #4: Smart Fallback System
This workflow demonstrates a smart fallback system that attempts to use Scrnify first, but falls back to local Puppeteer if the API is unavailable or if the request fails:
// smart-fallback-workflow.js
require('dotenv').config();
const path = require('path');
const fs = require('fs-extra');
const { setupBrowser, setupPage } = require('./puppeteer-setup');
const ScrnifyClient = require('./scrnify-client');
/**
* Takes a screenshot with fallback mechanism between Scrnify and local Puppeteer
* @param {string} url - URL to screenshot
* @param {string} outputPath - Path to save the screenshot
* @param {Object} options - Screenshot options
*/
async function smartScreenshot(url, outputPath, options = {}) {
const scrnify = new ScrnifyClient(process.env.SCRNIFY_API_KEY);
const defaultOptions = {
width: 1920,
height: 1080,
fullPage: false,
timeout: 30000,
};
const mergedOptions = { ...defaultOptions, ...options };
console.log(`šø Taking screenshot of ${url}...`);
try {
// First attempt: Use Scrnify API
console.log('š©ļø Attempting to use Scrnify API...');
await scrnify.takeAndSaveScreenshot({
url,
type: 'image',
format: 'png',
width: mergedOptions.width,
height: mergedOptions.height,
fullPage: mergedOptions.fullPage,
}, outputPath);
console.log('ā
Successfully used Scrnify API');
return { success: true, method: 'scrnify' };
} catch (error) {
console.warn(`ā ļø Scrnify API failed: ${error.message}`);
console.log('š Falling back to local Puppeteer...');
// Second attempt: Use local Puppeteer
const browser = await setupBrowser();
try {
const page = await setupPage(browser, {
viewport: {
width: mergedOptions.width,
height: mergedOptions.height,
},
timeout: mergedOptions.timeout,
});
await page.goto(url, {
waitUntil: 'networkidle2',
timeout: mergedOptions.timeout,
});
await page.screenshot({
path: outputPath,
fullPage: mergedOptions.fullPage,
});
console.log('ā
Successfully used local Puppeteer');
return { success: true, method: 'puppeteer' };
} catch (puppeteerError) {
console.error(`ā Puppeteer also failed: ${puppeteerError.message}`);
throw new Error('Both Scrnify and Puppeteer failed to take screenshot');
} finally {
await browser.close();
}
}
}
/**
* Demonstrates a smart fallback system between Scrnify and local Puppeteer
*/
async function smartFallbackWorkflow() {
const urls = [
'https://example.com',
'https://developer.mozilla.org',
'https://nodejs.org',
];
const outputDir = path.join(__dirname, 'screenshots');
const results = [];
for (const url of urls) {
const siteName = new URL(url).hostname.replace(/\./g, '-');
const outputPath = path.join(outputDir, `${siteName}.png`);
try {
const result = await smartScreenshot(url, outputPath, { fullPage: true });
results.push({ url, success: true, method: result.method });
console.log(`š Screenshot of ${url} saved to ${outputPath}`);
} catch (error) {
results.push({ url, success: false, error: error.message });
console.error(`ā Failed to take screenshot of ${url}: ${error.message}`);
}
}
// Output summary
console.log('\nš Workflow Summary:');
for (const result of results) {
if (result.success) {
console.log(`ā
${result.url} - Success (${result.method})`);
} else {
console.log(`ā ${result.url} - Failed: ${result.error}`);
}
}
}
// Run the workflow
smartFallbackWorkflow().catch(console.error);
This smart fallback system provides both reliability and flexibility in your screenshot workflows, ensuring you always get the screenshot you need.
Advanced Workflow #5: Visual Regression Testing Pipeline
This workflow demonstrates how to integrate screenshots into a visual regression testing pipeline:
// visual-regression-workflow.js
require('dotenv').config();
const path = require('path');
const fs = require('fs-extra');
const { setupBrowser, setupPage } = require('./puppeteer-setup');
const ScrnifyClient = require('./scrnify-client');
const sharp = require('sharp');
const pixelmatch = require('pixelmatch'); // You'll need to: npm install pixelmatch
/**
* Performs visual regression testing using Scrnify and Puppeteer
*/
async function visualRegressionWorkflow() {
const scrnify = new ScrnifyClient(process.env.SCRNIFY_API_KEY);
const outputDir = path.join(__dirname, 'screenshots');
const diffDir = path.join(outputDir, 'diffs');
await fs.ensureDir(diffDir);
try {
console.log('š Starting visual regression testing workflow...');
const testPages = [
{ name: 'homepage', url: 'https://example.com' },
{ name: 'about', url: 'https://example.com/about' },
{ name: 'contact', url: 'https://example.com/contact' },
];
const results = [];
for (const page of testPages) {
console.log(`šø Testing ${page.name}...`);
const baselinePath = path.join(outputDir, `${page.name}-baseline.png`);
const currentPath = path.join(outputDir, `${page.name}-current.png`);
const diffPath = path.join(diffDir, `${page.name}-diff.png`);
// Check if baseline exists, if not create it
if (!await fs.pathExists(baselinePath)) {
console.log(`Creating baseline for ${page.name}...`);
await scrnify.takeAndSaveScreenshot({
url: page.url,
type: 'image',
format: 'png',
width: 1920,
height: 1080,
fullPage: true,
}, baselinePath);
results.push({
page: page.name,
status: 'baseline_created',
message: 'Baseline created, no comparison made'
});
continue;
}
// Take current screenshot
await scrnify.takeAndSaveScreenshot({
url: page.url,
type: 'image',
format: 'png',
width: 1920,
height: 1080,
fullPage: true,
}, currentPath);
// Compare images
const baseline = await sharp(baselinePath).raw().toBuffer();
const current = await sharp(currentPath).raw().toBuffer();
const baselineInfo = await sharp(baselinePath).metadata();
const { width, height } = baselineInfo;
// Create empty diff image
const diff = Buffer.alloc(width * height * 4);
// Compare images pixel by pixel
const numDiffPixels = pixelmatch(
baseline,
current,
diff,
width,
height,
{ threshold: 0.1 }
);
// If differences found, save diff image
if (numDiffPixels > 0) {
await sharp(diff, { raw: { width, height, channels: 4 } })
.toFile(diffPath);
results.push({
page: page.name,
status: 'changed',
diffCount: numDiffPixels,
diffPercentage: (numDiffPixels / (width * height) * 100).toFixed(2) + '%',
diffPath
});
console.log(`ā Changes detected in ${page.name}: ${numDiffPixels} pixels (${(numDiffPixels / (width * height) * 100).toFixed(2)}%)`);
} else {
results.push({
page: page.name,
status: 'unchanged',
message: 'No visual changes detected'
});
console.log(`ā
No changes detected in ${page.name}`);
}
}
// Generate report
console.log('\nš Visual Regression Test Results:');
for (const result of results) {
if (result.status === 'baseline_created') {
console.log(`ā ļø ${result.page} - ${result.message}`);
} else if (result.status === 'changed') {
console.log(`ā ${result.page} - Changed: ${result.diffCount} pixels (${result.diffPercentage})`);
} else {
console.log(`ā
${result.page} - Unchanged`);
}
}
} catch (error) {
console.error('Error in visual regression workflow:', error);
}
}
// Run the workflow
visualRegressionWorkflow().catch(console.error);
This workflow demonstrates how to implement a basic visual regression testing system using Scrnify for screenshots and comparing them with pixel-matching algorithms.
Conclusion
By combining Puppeteer's flexibility with Scrnify's scalability, you can create powerful, efficient screenshot workflows that give you the best of both worlds. The approaches we've covered in this tutorial allow you to:
- Offload infrastructure concerns by using Scrnify for the actual screenshot rendering
- Maintain full control over the pre-processing with local Puppeteer
- Create sophisticated workflows that would be difficult with either tool alone
- Scale effortlessly without managing complex browser farms
These techniques are particularly valuable for teams that need both customization and reliability in their screenshot automation. Whether you're generating reports, testing UI, or creating marketing materials, the combination of Puppeteer and Scrnify gives you a powerful toolkit for image generation.
What's Next?
As you implement these workflows in your own projects, consider exploring:
- Scheduled screenshots using cron jobs or cloud functions
- Error monitoring and reporting systems
- Advanced image processing with libraries like Sharp
- Integration with CI/CD pipelines for visual regression testing
Get free access to the SCRNIFY API during our open beta and start generating screenshots today! Sign up for the beta here
We'd love to hear how you're using Puppeteer with Scrnify in your projects. What other workflows would you like to see covered? Let us know!
Cheers, Laura & Heidi š¦š¹
P.S. Check out our introductory post to learn more about SCRNIFY and how it can help you streamline your screenshot workflows!