Using Puppeteer with Scrnify for Advanced Screenshot Workflows

4/6/2025

Hey there, Laura and Heidi here from SCRNIFY! πŸ‘‹

As developers, we've all been thereβ€”spending hours configuring headless browsers, managing dependencies, and scaling infrastructure just to capture reliable screenshots. Whether you're building automated testing workflows, generating visual documentation, or creating dynamic previews, handling screenshot infrastructure can quickly become a major headache. πŸ˜…

We built SCRNIFY after facing these challenges ourselves. In this tutorial, we'll show you how to combine the power of Puppeteer's local automation capabilities with SCRNIFY's scalable screenshot API to create advanced workflows that save you time, resources, and frustration.

By the end of this article, you'll know how to:

  • Set up a local Puppeteer project for complex browser interactions
  • Use SCRNIFY's API to offload screenshot generation
  • Create advanced workflows combining both technologies
  • Save time and resources by letting SCRNIFY handle the heavy lifting

Let's dive in! β˜•

Prerequisites

Before we get started, make sure you have:

Setting Up Your Project

Let's start by creating a new Node.js project and installing the necessary dependencies. Open your terminal and run the following commands:

# Create a new directory and navigate into it
mkdir puppeteer-scrnify-demo
cd puppeteer-scrnify-demo

# Initialize a new Node.js project
npm init -y

# Install Puppeteer and other dependencies
npm install puppeteer axios dotenv

Next, let's create a .env file to store your SCRNIFY API key:

# .env file
SCRNIFY_API_KEY=your_api_key_here

Make sure to replace your_api_key_here with your actual SCRNIFY API key from the SCRNIFY dashboard.

Now, let's create a basic project structure:

puppeteer-scrnify-demo/
β”œβ”€β”€ .env                # Environment variables
β”œβ”€β”€ package.json        # Project configuration
β”œβ”€β”€ src/                # Source code directory
β”‚   β”œβ”€β”€ index.js        # Main script
β”‚   β”œβ”€β”€ local.js        # Local Puppeteer functions
β”‚   └── scrnify.js      # SCRNIFY API functions
└── screenshots/        # Directory for saved screenshots

Let's create these directories and files:

mkdir src screenshots
touch src/index.js src/local.js src/scrnify.js

Local Setup: Configuring Puppeteer

First, let's set up the local Puppeteer configuration in src/local.js. This file will contain functions for taking screenshots using Puppeteer locally:

// src/local.js
const puppeteer = require('puppeteer');
const path = require('path');
const fs = require('fs').promises;

/**
 * Takes a screenshot of a webpage using local Puppeteer
 * @param {string} url - The URL to capture
 * @param {Object} options - Screenshot options
 * @returns {Promise<Buffer>} - Screenshot buffer
 */
async function takeLocalScreenshot(url, options = {}) {
  // Set default options
  const defaultOptions = {
    fullPage: false,
    type: 'png',
    path: null, // If provided, save to this path
    viewport: { width: 1280, height: 720 },
    waitUntil: 'networkidle0'
  };

  const screenshotOptions = { ...defaultOptions, ...options };
  let browser = null;

  try {
    // Launch a browser
    console.log('Launching browser...');
    browser = await puppeteer.launch({
      headless: 'new' // Use the new headless mode
    });

    const page = await browser.newPage();

    // Set viewport
    await page.setViewport(screenshotOptions.viewport);

    // Navigate to URL
    console.log(`Navigating to ${url}...`);
    await page.goto(url, { waitUntil: screenshotOptions.waitUntil });

    // Take screenshot
    console.log('Taking screenshot...');
    const screenshotBuffer = await page.screenshot({
      fullPage: screenshotOptions.fullPage,
      type: screenshotOptions.type,
      path: screenshotOptions.path
    });

    return screenshotBuffer;
  } catch (error) {
    console.error('Error taking local screenshot:', error);
    throw error;
  } finally {
    // Always close the browser
    if (browser) {
      await browser.close();
      console.log('Browser closed.');
    }
  }
}

/**
 * Takes a screenshot of a specific element
 * @param {string} url - The URL to capture
 * @param {string} selector - CSS selector for the element
 * @param {Object} options - Screenshot options
 * @returns {Promise<Buffer>} - Screenshot buffer
 */
async function takeElementScreenshot(url, selector, options = {}) {
  let browser = null;

  try {
    // Launch a browser
    console.log('Launching browser...');
    browser = await puppeteer.launch({
      headless: 'new' // Use the new headless mode
    });

    const page = await browser.newPage();

    // Set viewport
    await page.setViewport(options.viewport || { width: 1280, height: 720 });

    // Navigate to URL
    console.log(`Navigating to ${url}...`);
    await page.goto(url, { waitUntil: options.waitUntil || 'networkidle0' });

    // Wait for the element to be visible
    console.log(`Waiting for selector: ${selector}`);
    await page.waitForSelector(selector, { visible: true });

    // Take screenshot of the element
    const element = await page.$(selector);
    if (!element) {
      throw new Error(`Element with selector "${selector}" not found`);
    }

    console.log('Taking element screenshot...');
    const screenshotBuffer = await element.screenshot({
      type: options.type || 'png',
      path: options.path
    });

    return screenshotBuffer;
  } catch (error) {
    console.error('Error taking element screenshot:', error);
    throw error;
  } finally {
    // Always close the browser
    if (browser) {
      await browser.close();
      console.log('Browser closed.');
    }
  }
}

/**
 * Takes a screenshot after interacting with the page
 * @param {string} url - The URL to capture
 * @param {Function} interactionFn - Function to perform interactions
 * @param {Object} options - Screenshot options
 * @returns {Promise<Buffer>} - Screenshot buffer
 */
async function takeInteractiveScreenshot(url, interactionFn, options = {}) {
  let browser = null;

  try {
    // Launch a browser
    console.log('Launching browser...');
    browser = await puppeteer.launch({
      headless: 'new' // Use the new headless mode
    });

    const page = await browser.newPage();

    // Set viewport
    await page.setViewport(options.viewport || { width: 1280, height: 720 });

    // Navigate to URL
    console.log(`Navigating to ${url}...`);
    await page.goto(url, { waitUntil: options.waitUntil || 'networkidle0' });

    // Execute the interaction function
    console.log('Performing page interactions...');
    await interactionFn(page);

    // Take screenshot
    console.log('Taking screenshot after interaction...');
    const screenshotBuffer = await page.screenshot({
      fullPage: options.fullPage || false,
      type: options.type || 'png',
      path: options.path
    });

    return screenshotBuffer;
  } catch (error) {
    console.error('Error taking interactive screenshot:', error);
    throw error;
  } finally {
    // Always close the browser
    if (browser) {
      await browser.close();
      console.log('Browser closed.');
    }
  }
}

module.exports = {
  takeLocalScreenshot,
  takeElementScreenshot,
  takeInteractiveScreenshot
};

Integrating with SCRNIFY API

Now, let's create the SCRNIFY integration in src/scrnify.js. This file will handle communication with the SCRNIFY API:

// src/scrnify.js
const axios = require('axios');
const fs = require('fs').promises;
const path = require('path');
require('dotenv').config();

// Get API key from environment variables
const SCRNIFY_API_KEY = process.env.SCRNIFY_API_KEY;
const SCRNIFY_API_URL = 'https://api.scrnify.com/capture';

/**
 * Takes a screenshot using SCRNIFY API
 * @param {string} url - The URL to capture
 * @param {Object} options - Screenshot options
 * @returns {Promise<Buffer>} - Screenshot buffer
 */
async function takeScrnifyScreenshot(url, options = {}) {
  // Set default options
  const defaultOptions = {
    type: 'image',
    format: 'png',
    width: 1280,
    height: 720,
    fullPage: false,
    savePath: null // If provided, save to this path
  };

  const apiOptions = { ...defaultOptions, ...options };

  try {
    console.log(`Taking screenshot of ${url} using SCRNIFY API...`);

    // Build query parameters
    const params = new URLSearchParams({
      key: SCRNIFY_API_KEY,
      url: url,
      type: apiOptions.type,
      format: apiOptions.format,
      width: apiOptions.width,
      height: apiOptions.height,
      fullPage: apiOptions.fullPage
    });

    // Make API request
    const response = await axios({
      method: 'get',
      url: `${SCRNIFY_API_URL}?${params.toString()}`,
      responseType: 'arraybuffer'
    });

    const buffer = Buffer.from(response.data, 'binary');

    // Save to file if path is provided
    if (apiOptions.savePath) {
      await fs.writeFile(apiOptions.savePath, buffer);
      console.log(`Screenshot saved to ${apiOptions.savePath}`);
    }

    return buffer;
  } catch (error) {
    console.error('Error taking SCRNIFY screenshot:', error.message);
    if (error.response) {
      console.error(`Status: ${error.response.status}`);
      console.error(`Data: ${error.response.data.toString()}`);
    }
    throw error;
  }
}

/**
 * Takes multiple screenshots using SCRNIFY API
 * @param {Array<string>} urls - Array of URLs to capture
 * @param {Object} options - Screenshot options
 * @returns {Promise<Array<Buffer>>} - Array of screenshot buffers
 */
async function takeBatchScreenshots(urls, options = {}) {
  console.log(`Taking batch screenshots of ${urls.length} URLs...`);

  const results = [];
  const errors = [];

  // Process URLs in batches of 5 to avoid overwhelming the API
  const batchSize = 5;

  for (let i = 0; i < urls.length; i += batchSize) {
    const batch = urls.slice(i, i + batchSize);
    const batchPromises = batch.map(async (url, index) => {
      try {
        // Create unique filename if saving
        let savePath = null;
        if (options.savePath) {
          const filename = `batch_${i + index}_${new URL(url).hostname}.${options.format || 'png'}`;
          savePath = path.join(options.savePath, filename);
        }

        const buffer = await takeScrnifyScreenshot(url, {
          ...options,
          savePath
        });

        return { url, buffer, success: true };
      } catch (error) {
        errors.push({ url, error: error.message });
        return { url, buffer: null, success: false, error: error.message };
      }
    });

    // Wait for current batch to complete
    const batchResults = await Promise.all(batchPromises);
    results.push(...batchResults);

    // Add a small delay between batches
    if (i + batchSize < urls.length) {
      await new Promise(resolve => setTimeout(resolve, 1000));
    }
  }

  // Log errors if any
  if (errors.length > 0) {
    console.warn(`${errors.length} screenshots failed:`);
    errors.forEach(err => console.warn(`- ${err.url}: ${err.error}`));
  }

  console.log(`Batch screenshot process completed. Success: ${results.filter(r => r.success).length}/${urls.length}`);

  return results;
}

module.exports = {
  takeScrnifyScreenshot,
  takeBatchScreenshots
};

Creating Advanced Workflows

Now, let's create our main script in src/index.js that combines local Puppeteer capabilities with SCRNIFY's API for advanced workflows:

// src/index.js
const path = require('path');
const fs = require('fs').promises;
require('dotenv').config();

const { takeLocalScreenshot, takeElementScreenshot, takeInteractiveScreenshot } = require('./local');
const { takeScrnifyScreenshot, takeBatchScreenshots } = require('./scrnify');

// Create screenshots directory if it doesn't exist
async function ensureDirectoryExists(directory) {
  try {
    await fs.access(directory);
  } catch (error) {
    await fs.mkdir(directory, { recursive: true });
    console.log(`Created directory: ${directory}`);
  }
}

// Example 1: Basic comparison between local Puppeteer and SCRNIFY
async function basicComparison() {
  const url = 'https://example.com';
  const screenshotsDir = path.join(__dirname, '../screenshots');

  await ensureDirectoryExists(screenshotsDir);

  console.log('=== Basic Comparison Example ===');

  // Take local screenshot with Puppeteer
  console.log('\n1. Taking screenshot with local Puppeteer...');
  const startLocal = Date.now();
  await takeLocalScreenshot(url, {
    path: path.join(screenshotsDir, 'local_example.png')
  });
  console.log(`Local screenshot completed in ${Date.now() - startLocal}ms`);

  // Take screenshot with SCRNIFY API
  console.log('\n2. Taking screenshot with SCRNIFY API...');
  const startScrnify = Date.now();
  await takeScrnifyScreenshot(url, {
    savePath: path.join(screenshotsDir, 'scrnify_example.png')
  });
  console.log(`SCRNIFY screenshot completed in ${Date.now() - startScrnify}ms`);

  console.log('\nBasic comparison completed! Check the screenshots directory.');
}

// Example 2: Capture element with local Puppeteer, then use SCRNIFY for full page
async function elementAndFullPageWorkflow() {
  const url = 'https://news.ycombinator.com';
  const screenshotsDir = path.join(__dirname, '../screenshots');

  await ensureDirectoryExists(screenshotsDir);

  console.log('\n=== Element and Full Page Workflow Example ===');

  // Capture specific element with Puppeteer
  console.log('\n1. Capturing header element with Puppeteer...');
  await takeElementScreenshot(url, '.hnname', {
    path: path.join(screenshotsDir, 'element_header.png')
  });

  // Use SCRNIFY for full page screenshot
  console.log('\n2. Capturing full page with SCRNIFY...');
  await takeScrnifyScreenshot(url, {
    fullPage: true,
    savePath: path.join(screenshotsDir, 'scrnify_fullpage.png')
  });

  console.log('\nElement and full page workflow completed!');
}

// Example 3: Interactive workflow - Puppeteer for interaction, SCRNIFY for final capture
async function interactiveWorkflow() {
  const url = 'https://news.ycombinator.com';
  const screenshotsDir = path.join(__dirname, '../screenshots');

  await ensureDirectoryExists(screenshotsDir);

  console.log('\n=== Interactive Workflow Example ===');

  // Define interaction function
  const interactionFn = async (page) => {
    // Click on the "new" link
    await page.click('a[href="newest"]');
    // Wait for page to load
    await page.waitForSelector('.itemlist');
    console.log('Clicked on "new" and waited for page to load');
  };

  // First, use Puppeteer to interact with the page
  console.log('\n1. Using Puppeteer to interact with the page...');
  await takeInteractiveScreenshot(url, interactionFn, {
    path: path.join(screenshotsDir, 'interactive_local.png')
  });

  // Now, capture the result URL with SCRNIFY
  console.log('\n2. Capturing the result with SCRNIFY...');
  await takeScrnifyScreenshot('https://news.ycombinator.com/newest', {
    savePath: path.join(screenshotsDir, 'interactive_scrnify.png')
  });

  console.log('\nInteractive workflow completed!');
}

// Example 4: Batch processing with SCRNIFY
async function batchProcessing() {
  const urls = [
    'https://example.com',
    'https://news.ycombinator.com',
    'https://github.com',
    'https://stackoverflow.com',
    'https://nodejs.org'
  ];

  const screenshotsDir = path.join(__dirname, '../screenshots/batch');

  await ensureDirectoryExists(screenshotsDir);

  console.log('\n=== Batch Processing Example ===');
  console.log(`Processing ${urls.length} URLs with SCRNIFY...`);

  const results = await takeBatchScreenshots(urls, {
    savePath: screenshotsDir,
    format: 'png'
  });

  console.log(`\nBatch processing completed! Success rate: ${results.filter(r => r.success).length}/${urls.length}`);
}

// Run all examples
async function runExamples() {
  try {
    await basicComparison();
    await elementAndFullPageWorkflow();
    await interactiveWorkflow();
    await batchProcessing();

    console.log('\nπŸŽ‰ All examples completed successfully!');
  } catch (error) {
    console.error('Error running examples:', error);
  }
}

// Run the examples
runExamples();

Running the Examples

To run the examples, execute the following command in your terminal:

node src/index.js

This will run all the example workflows, demonstrating how to combine local Puppeteer capabilities with SCRNIFY's API for advanced screenshot workflows.

Advanced Use Cases

Now that we have our basic setup working, let's explore some advanced use cases where combining Puppeteer with SCRNIFY really shines:

1. Visual Regression Testing

Visual regression testing is a perfect use case for combining Puppeteer with SCRNIFY. Here's how you might implement it:

// src/visual-regression.js
const puppeteer = require('puppeteer');
const { takeScrnifyScreenshot } = require('./scrnify');
const fs = require('fs').promises;
const path = require('path');
const pixelmatch = require('pixelmatch');
const { PNG } = require('pngjs');

async function visualRegressionTest(url, selector) {
  const screenshotsDir = path.join(__dirname, '../screenshots/regression');
  await ensureDirectoryExists(screenshotsDir);

  // Step 1: Use Puppeteer to interact with the page (e.g., fill a form)
  const browser = await puppeteer.launch({ headless: 'new' });
  const page = await browser.newPage();

  try {
    await page.goto(url, { waitUntil: 'networkidle0' });

    // Perform some interaction (e.g., toggle dark mode)
    await page.click('#theme-toggle');
    await page.waitForTimeout(1000); // Wait for transition

    // Step 2: Use SCRNIFY to capture the result
    const afterChangeBuffer = await takeScrnifyScreenshot(url, {
      savePath: path.join(screenshotsDir, 'after_change.png')
    });

    // Step 3: Reset the state
    await page.click('#theme-toggle');
    await page.waitForTimeout(1000);

    // Step 4: Capture the original state
    const originalBuffer = await takeScrnifyScreenshot(url, {
      savePath: path.join(screenshotsDir, 'original.png')
    });

    // Step 5: Compare the screenshots
    const original = PNG.sync.read(originalBuffer);
    const afterChange = PNG.sync.read(afterChangeBuffer);
    const { width, height } = original;
    const diff = new PNG({ width, height });

    const numDiffPixels = pixelmatch(
      original.data,
      afterChange.data,
      diff.data,
      width,
      height,
      { threshold: 0.1 }
    );

    // Save diff image
    await fs.writeFile(
      path.join(screenshotsDir, 'diff.png'),
      PNG.sync.write(diff)
    );

    console.log(`Visual difference: ${numDiffPixels} pixels`);
    return numDiffPixels;
  } finally {
    await browser.close();
  }
}

2. Dynamic Content Capture

This example shows how to capture screenshots of dynamic content that requires JavaScript interaction:

// src/dynamic-content.js
const puppeteer = require('puppeteer');
const { takeScrnifyScreenshot } = require('./scrnify');
const path = require('path');

async function captureDynamicContent(url) {
  const screenshotsDir = path.join(__dirname, '../screenshots/dynamic');
  await ensureDirectoryExists(screenshotsDir);

  // Step 1: Use Puppeteer to interact with the page and extract data
  const browser = await puppeteer.launch({ headless: 'new' });
  const page = await browser.newPage();

  try {
    await page.goto(url, { waitUntil: 'networkidle0' });

    // Interact with the page (e.g., click tabs, load more content)
    await page.click('#tab-2');
    await page.waitForSelector('#tab-2-content', { visible: true });

    // Extract the current URL after interaction
    const currentUrl = page.url();

    // Step 2: Use SCRNIFY to capture the final state
    await takeScrnifyScreenshot(currentUrl, {
      savePath: path.join(screenshotsDir, 'dynamic_content.png'),
      fullPage: true
    });

    console.log('Dynamic content captured successfully!');
  } finally {
    await browser.close();
  }
}

3. Multi-Device Screenshot Comparison

This example demonstrates how to capture the same page across multiple device viewports:

// src/multi-device.js
const { takeScrnifyScreenshot } = require('./scrnify');
const path = require('path');

async function captureMultiDeviceScreenshots(url) {
  const screenshotsDir = path.join(__dirname, '../screenshots/devices');
  await ensureDirectoryExists(screenshotsDir);

  // Define device configurations
  const devices = [
    { name: 'desktop', width: 1920, height: 1080 },
    { name: 'tablet', width: 768, height: 1024 },
    { name: 'mobile', width: 375, height: 667 }
  ];

  console.log(`Capturing ${url} across ${devices.length} devices...`);

  // Capture screenshots for each device
  const promises = devices.map(device =>
    takeScrnifyScreenshot(url, {
      width: device.width,
      height: device.height,
      savePath: path.join(screenshotsDir, `${device.name}.png`)
    })
  );

  await Promise.all(promises);
  console.log('Multi-device screenshots completed!');
}

When to Use Puppeteer vs. SCRNIFY

Understanding when to use each tool is key to creating efficient workflows:

Use Puppeteer locally when:

  • You need complex page interactions before taking a screenshot
  • You're working with authentication or session-based workflows
  • You need to execute custom JavaScript on the page
  • You're developing and testing locally

Use SCRNIFY API when:

  • You need to scale your screenshot generation
  • You want to avoid the overhead of maintaining browser infrastructure
  • You need consistent, high-quality screenshots across different environments
  • You're deploying to serverless or container environments with limited resources
  • You need to capture many screenshots in parallel

Best Practices

Here are some best practices for working with Puppeteer and SCRNIFY together:

  1. Error Handling: Always implement proper error handling and browser cleanup in your Puppeteer scripts.

  2. Resource Management: Close browser instances as soon as possible to free up resources.

  3. Batch Processing: When capturing multiple screenshots, use batch processing to avoid overwhelming your local machine or the API.

  4. Caching: Implement caching for screenshots that don't change frequently.

  5. Environment-Based Logic: Use Puppeteer for development and SCRNIFY for production to optimize for both flexibility and reliability.

  6. Timeouts: Set appropriate timeouts for both Puppeteer operations and API calls.

  7. Viewport Consistency: Ensure consistent viewport settings between local Puppeteer instances and SCRNIFY API calls for comparable results.

Conclusion

In this tutorial, we've explored how to combine the power of Puppeteer's local automation capabilities with SCRNIFY's scalable screenshot API to create advanced screenshot workflows. This hybrid approach gives you the best of both worlds:

  • Puppeteer's flexibility for complex browser interactions
  • SCRNIFY's reliability and scalability for high-quality screenshot generation

By offloading the heavy lifting of screenshot generation to SCRNIFY, you can focus on building great features rather than managing browser infrastructure. This approach is particularly valuable for teams that need to scale their screenshot capabilities without the operational overhead.

The next time you find yourself configuring yet another headless browser environment or troubleshooting screenshot inconsistencies across different platforms, remember that SCRNIFY is here to make your life easier. We handle the complex infrastructure so you can focus on creating awesome screenshot workflows.

Get free access to the SCRNIFY API during our open beta and start generating screenshots today! Sign up at scrnify.com

Have you built any interesting screenshot workflows combining local automation with cloud services? We'd love to hear about your experiences in the comments!

Cheers, Laura & Heidi πŸ‡¦πŸ‡Ή

P.S. Want to see how we built SCRNIFY's own documentation screenshots? We actually "dogfooded" our own API to generate all the code examples and UI screenshots you see in our docs. πŸ§™β€β™€οΈ And don't forget to follow us on Twitter @ScrnifyHQ for more tips and updates!

Additional Resources

Ready to Get Started?

Sign up now and start capturing stunning screenshots in minutes.

Sign Up Now