DEV Community

Custodia-Admin
Custodia-Admin

Posted on • Originally published at pagebolt.dev

How to Take a Website Screenshot in Ruby (Without Selenium)

How to Take a Website Screenshot in Ruby (Without Selenium)

You're building a Rails app that needs screenshots. You reach for Selenium and suddenly you're managing headless browsers, handling timeouts, and scaling test suites.

There's a simpler way: use a screenshot API.

One HTTP request. Binary PNG response. No browser overhead.

The Basic Pattern: Net::HTTP Binary Download

require 'net/http'
require 'json'

def take_screenshot(url, output_path)
  uri = URI('https://api.pagebolt.dev/v1/screenshot')

  request = Net::HTTP::Post.new(uri)
  request['Authorization'] = "Bearer #{ENV['PAGEBOLT_API_KEY']}"
  request['Content-Type'] = 'application/json'
  request.body = JSON.generate({
    url: url,
    format: 'png',
    width: 1280,
    height: 720,
    fullPage: true,
    blockBanners: true
  })

  http = Net::HTTP.new(uri.hostname, uri.port)
  http.use_ssl = true

  response = http.request(request)

  if response.code.to_i == 200
    # Response is binary PNG — write directly to file
    File.write(output_path, response.body)
    puts "Screenshot saved to #{output_path}"
  else
    raise "API error: #{response.code}"
  end
end

# Usage
take_screenshot('https://example.com', 'screenshot.png')
Enter fullscreen mode Exit fullscreen mode

Key points:

  • response.body is binary PNG data (not JSON)
  • File.write() handles binary content correctly
  • No JSON decoding — write bytes directly to disk

Production Pattern: Reusable Client Class

require 'net/http'
require 'json'

class PageBoltClient
  def initialize(api_key)
    @api_key = api_key
    @base_uri = URI('https://api.pagebolt.dev/v1')
  end

  def screenshot(options = {})
    url = options[:url]
    output_path = options[:path]

    raise 'url is required' unless url

    uri = URI.join(@base_uri, 'screenshot')

    request = Net::HTTP::Post.new(uri)
    request['Authorization'] = "Bearer #{@api_key}"
    request['Content-Type'] = 'application/json'
    request.body = JSON.generate({
      url: url,
      format: options[:format] || 'png',
      width: options[:width] || 1280,
      height: options[:height] || 720,
      fullPage: options[:fullPage] != false,
      blockBanners: options[:blockBanners] != false,
      blockAds: options[:blockAds] || false,
      darkMode: options[:darkMode] || false
    })

    http = Net::HTTP.new(uri.hostname, uri.port)
    http.use_ssl = true

    response = http.request(request)

    unless response.code.to_i == 200
      raise "API error: #{response.code} #{response.body}"
    end

    if output_path
      File.write(output_path, response.body)
      return { success: true, path: output_path }
    end

    { success: true, data: response.body }
  end

  def batch(urls, output_dir, options = {})
    Dir.mkdir(output_dir) unless Dir.exist?(output_dir)

    results = urls.map.with_index do |url, idx|
      filename = "#{Time.now.to_i}-#{idx}.png"
      filepath = File.join(output_dir, filename)

      screenshot(options.merge(url: url, path: filepath))
      { url: url, path: filepath }
    end

    results
  end
end

# Usage
client = PageBoltClient.new(ENV['PAGEBOLT_API_KEY'])

# Single screenshot
client.screenshot(
  url: 'https://example.com',
  fullPage: true,
  path: 'homepage.png'
)

# Batch screenshots
urls = ['https://example.com', 'https://example.com/about', 'https://example.com/pricing']
results = client.batch(urls, './screenshots')
puts "Batch complete: #{results.length} screenshots"
Enter fullscreen mode Exit fullscreen mode

Rails Integration: Controller Action

Use PageBolt to generate screenshots on demand:

# app/controllers/screenshots_controller.rb
class ScreenshotsController < ApplicationController
  def create
    @client = PageBoltClient.new(ENV['PAGEBOLT_API_KEY'])

    url = params[:url]
    return render json: { error: 'url required' }, status: 400 unless url

    result = @client.screenshot(
      url: url,
      fullPage: params[:fullPage] != 'false',
      path: Rails.root.join('tmp', "screenshot-#{Time.now.to_i}.png")
    )

    send_file(result[:path], type: 'image/png', disposition: 'inline')
  end
end

# config/routes.rb
post '/screenshots', to: 'screenshots#create'
Enter fullscreen mode Exit fullscreen mode

Test it:

curl -X POST http://localhost:3000/screenshots \
  -d "url=https://example.com&fullPage=true" \
  > screenshot.png
Enter fullscreen mode Exit fullscreen mode

Real Use Case: Website Monitoring Script

Monitor websites for changes:

require 'digest'

class SiteMonitor
  def initialize(api_key)
    @client = PageBoltClient.new(api_key)
  end

  def check(url)
    baseline_file = "baseline-#{Digest::MD5.hexdigest(url)}.png"
    current_file = "current-#{Digest::MD5.hexdigest(url)}.png"

    # First run: create baseline
    unless File.exist?(baseline_file)
      @client.screenshot(url: url, path: baseline_file)
      puts "Baseline created for #{url}"
      return
    end

    # Subsequent runs: compare
    @client.screenshot(url: url, path: current_file)

    baseline_hash = Digest::MD5.file(baseline_file).hexdigest
    current_hash = Digest::MD5.file(current_file).hexdigest

    if baseline_hash != current_hash
      puts "⚠️  #{url} has changed!"
      puts "Baseline: #{baseline_hash}"
      puts "Current:  #{current_hash}"
      return true
    end

    puts "✓ #{url} unchanged"
    false
  end
end

# Usage: run daily via cron
monitor = SiteMonitor.new(ENV['PAGEBOLT_API_KEY'])
['https://example.com', 'https://example.com/status'].each { |url| monitor.check(url) }
Enter fullscreen mode Exit fullscreen mode

Error Handling Patterns

class PageBoltClient
  class APIError < StandardError; end
  class RateLimitError < StandardError; end

  def screenshot_with_retry(options = {}, max_retries = 3)
    retries = 0

    begin
      screenshot(options)
    rescue RateLimitError => e
      if retries < max_retries
        wait_time = 1000 * (2 ** retries)
        puts "Rate limited. Waiting #{wait_time}ms..."
        sleep(wait_time / 1000.0)
        retries += 1
        retry
      end
      raise
    rescue => e
      puts "Error: #{e.message}"
      raise
    end
  end

  private

  def request_screenshot(uri, request)
    http = Net::HTTP.new(uri.hostname, uri.port)
    http.use_ssl = true
    response = http.request(request)

    case response.code.to_i
    when 200
      response
    when 429
      raise RateLimitError, "Rate limit exceeded"
    else
      raise APIError, "API error: #{response.code} #{response.body}"
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

curl + Ruby Comparison

curl approach:

curl -X POST https://api.pagebolt.dev/v1/screenshot \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com","format":"png","fullPage":true}' \
  > screenshot.png
Enter fullscreen mode Exit fullscreen mode

Ruby approach:

client = PageBoltClient.new(ENV['PAGEBOLT_API_KEY'])
client.screenshot(url: 'https://example.com', path: 'screenshot.png')
Enter fullscreen mode Exit fullscreen mode

Both hit the same API. Ruby gives you:

  • ✅ Type-safe options
  • ✅ Reusable client
  • ✅ Error handling
  • ✅ Batch processing
  • ✅ Production patterns (retry logic, monitoring)

Testing: Mock the API

require 'minitest/autorun'

class PageBoltClientTest < Minitest::Test
  def test_screenshot_saves_file
    client = PageBoltClient.new('test-key')

    # Stub Net::HTTP
    mock_response = MiniTest::Mock.new
    mock_response.expect(:code, '200')
    mock_response.expect(:body, "\x89PNG\r\n\x1a\n...")

    Net::HTTP.stub(:request, mock_response) do
      result = client.screenshot(
        url: 'https://example.com',
        path: 'test.png'
      )

      assert result[:success]
      assert File.exist?('test.png')
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Pricing

Plan Requests/Month Cost Best For
Free 100 $0 Learning, low-volume projects
Starter 5,000 $29 Small teams, moderate use
Growth 25,000 $79 Production apps, frequent calls
Scale 100,000 $199 High-volume automation

Summary

  • ✅ Idiomatic Ruby with Net::HTTP
  • ✅ Binary response handling with File.write()
  • ✅ Reusable client class for production
  • ✅ Rails integration examples
  • ✅ Monitoring and error handling patterns
  • ✅ No Selenium, no browser management
  • ✅ Works in tests and background jobs

Get started free: pagebolt.dev — 100 requests/month, no credit card required.

Top comments (0)