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:

  1. Local Pre-processing: Using Puppeteer locally to manipulate pages, inject scripts, or handle authentication
  2. Cloud Rendering: Delegating the actual screenshot rendering to Scrnify's API
  3. Custom Post-processing: Processing the resulting screenshots locally with specialized libraries
  4. 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:

  1. Offload infrastructure concerns by using Scrnify for the actual screenshot rendering
  2. Maintain full control over the pre-processing with local Puppeteer
  3. Create sophisticated workflows that would be difficult with either tool alone
  4. 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!


References

Ready to Get Started?

Sign up now and start capturing stunning screenshots in minutes.

Sign Up Now