# Website screenshot code examples in 8 languages

> The shortest working code to save a full-page screenshot of a website in curl, Node.js, Python, PHP, Go, Ruby, Java and C#, side by side.

Rosetta stone by Serghei Pogor, founder of CurlShot (https://curlshot.com/about/serghei-pogor). Published 27 October 2026. Page for people: https://curlshot.com/blog/screenshot-code-examples

There are two ways to take a website screenshot from code. Drive a headless browser yourself: Playwright has official libraries for Node.js, Python, Java and .NET, Puppeteer for Node.js, and the call is page.screenshot with fullPage, full_page, setFullPage or FullPage set to true. Or send one HTTP request to a screenshot API such as CurlShot, which works in any language with an HTTP client: the versions on this page take 6 to 31 lines. Every version has the same four parts: the address, the options, the access key and writing the bytes to a file.

## Key facts

- Playwright publishes official libraries for four of the eight languages: Node.js, Python, Java and .NET. Puppeteer is for Node.js. PHP, Go and Ruby rely on community libraries or Selenium.
- The full-page option has four spellings for one idea: fullPage (Node.js), full_page (Python), setFullPage (Java) and FullPage (.NET). In the HTTP API it is the parameter full_page=true.
- Counting non-blank lines on this page, the HTTP request takes 6 lines in curl, 10 in Node.js, 12 in Python, 12 in PHP, 31 in Go, 13 in Ruby, 18 in Java and 7 in C#.
- With a headless browser the code is 6 lines in Node.js and 7 in Python, plus a browser to install, update and close.
- A failed API request returns JSON with a status of 400 or higher, not an image. Check the status before saving, retry only 429 and 5xx, and wait the seconds given in Retry-After.
- Give your HTTP client a timeout longer than the render: the API timeout option allows up to 90 seconds, so 120 seconds on the client is a safe value.

## The four parts every version shares

The task is the same everywhere: save a full-page PNG of https://example.com to a file called example.png. Every version below, in every language, is made of the same four parts.

| Part | Name | In the HTTP request (track B) | With your own browser (track A) |
| --- | --- | --- | --- |
| 1 | The address | The `url` parameter: a full address that starts with `http://` or `https://`, up to 4,096 characters. It must be percent-encoded, which every snippet leaves to its HTTP client. | The argument of `page.goto` (`page.navigate` in Java, `GotoAsync` in .NET). |
| 2 | The options | `full_page=true` captures the whole page instead of the first screen (the default is `false`). `format=png` is the default and is written out so it is easy to change to `jpeg`, `webp` or `pdf`. | The full-page flag of `page.screenshot`: `fullPage` in Node.js, `full_page` in Python, `setFullPage` in Java, `FullPage` in .NET. The file ending chooses PNG. |
| 3 | The access key | The `X-Access-Key` header. A header keeps the key out of the address, so it does not end up in logs and browser history. | There is no key. Its place is taken by a browser you launch, wait for and close yourself. |
| 4 | The file | The response body is the PNG itself, with the content type `image/png`. Write the bytes to a file unchanged, in binary mode. | The `path` given to `page.screenshot`. The library writes the file. |

## The anatomy of the request

Written out as HTTP, the request that all eight track B versions send, and the answer, look like this:

```http
GET https://curlshot.com/api/v1/screenshot
    ?url=https%3A%2F%2Fexample.com
    &full_page=true
    &format=png
X-Access-Key: YOUR_ACCESS_KEY

200 OK
Content-Type: image/png
(the bytes of example.png)
```

The access key can travel three ways: the `X-Access-Key` header, the standard `Authorization: Bearer` header, or the `access_key` query parameter. See [authentication](https://curlshot.com/docs/authentication).

## Track B: one HTTP request to a screenshot API, in eight languages

Each version sends the same GET request to CurlShot and writes the answer to example.png. Swap YOUR_ACCESS_KEY for your own access key. These are the shortest versions: they do not check the status yet (see "What every version needs next").

### curl

```bash
curl -G "https://curlshot.com/api/v1/screenshot" \
  --data-urlencode "url=https://example.com" \
  -d "full_page=true" \
  -d "format=png" \
  -H "X-Access-Key: YOUR_ACCESS_KEY" \
  --output example.png
```

Run: Paste it into a terminal. `-G` sends the `-d` lines as a query string and `--data-urlencode` encodes the address. On an error, curl still writes the answer to the file unless you add `--fail`. Status: run and syntax-checked.

### Node.js (fetch)

```javascript
import { writeFile } from 'node:fs/promises'

const params = new URLSearchParams({
  url: 'https://example.com',
  full_page: 'true',
  format: 'png',
})
const response = await fetch(`https://curlshot.com/api/v1/screenshot?${params}`, {
  headers: { 'X-Access-Key': 'YOUR_ACCESS_KEY' },
})
await writeFile('example.png', Buffer.from(await response.arrayBuffer()))
```

Run: node screenshot.mjs. Node.js 18 or newer, no packages. `fetch` is built into Node.js 18 and newer. `URLSearchParams` encodes the address. Status: run and syntax-checked.

### Python (requests)

```python
import requests

response = requests.get(
    "https://curlshot.com/api/v1/screenshot",
    params={
        "url": "https://example.com",
        "full_page": "true",
        "format": "png",
    },
    headers={"X-Access-Key": "YOUR_ACCESS_KEY"},
)
with open("example.png", "wb") as f:
    f.write(response.content)
```

Run: pip install requests, then python screenshot.py. `requests` is one package to install. The standard library can do the same with `urllib.request`, in a few more lines. Status: run and syntax-checked.

### PHP (curl extension)

```php
<?php
$query = http_build_query([
    'url' => 'https://example.com',
    'full_page' => 'true',
    'format' => 'png',
]);
$ch = curl_init("https://curlshot.com/api/v1/screenshot?$query");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['X-Access-Key: YOUR_ACCESS_KEY'],
]);
file_put_contents('example.png', curl_exec($ch));
```

Run: php screenshot.php. Needs the curl extension, which most PHP installs have. `CURLOPT_RETURNTRANSFER` makes `curl_exec` give the body back as a string, and `http_build_query` encodes the address. Status: checked against the documentation only.

### Go (net/http)

```go
package main

import (
	"io"
	"log"
	"net/http"
	"net/url"
	"os"
)

func main() {
	params := url.Values{}
	params.Set("url", "https://example.com")
	params.Set("full_page", "true")
	params.Set("format", "png")

	req, err := http.NewRequest("GET", "https://curlshot.com/api/v1/screenshot?"+params.Encode(), nil)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("X-Access-Key", "YOUR_ACCESS_KEY")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	if err := os.WriteFile("example.png", body, 0o644); err != nil {
		log.Fatal(err)
	}
}
```

Run: go run main.go. Standard library only. Go makes you handle each error, which is most of the extra lines. `url.Values` encodes the address. Status: run and syntax-checked.

### Ruby (net/http)

```ruby
require "net/http"

uri = URI("https://curlshot.com/api/v1/screenshot")
uri.query = URI.encode_www_form(
  url: "https://example.com",
  full_page: "true",
  format: "png"
)
request = Net::HTTP::Get.new(uri)
request["X-Access-Key"] = "YOUR_ACCESS_KEY"

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
  http.request(request)
end
File.binwrite("example.png", response.body)
```

Run: ruby screenshot.rb. Ruby 2.6 or newer, nothing to install. `File.binwrite` writes the bytes unchanged, and `URI.encode_www_form` encodes the address. Status: run and syntax-checked.

### Java (java.net.http.HttpClient)

```java
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;

public class Screenshot {
    public static void main(String[] args) throws Exception {
        String query = "url=" + URLEncoder.encode("https://example.com", StandardCharsets.UTF_8)
            + "&full_page=true"
            + "&format=png";
        HttpRequest request = HttpRequest.newBuilder(URI.create("https://curlshot.com/api/v1/screenshot?" + query))
            .header("X-Access-Key", "YOUR_ACCESS_KEY")
            .build();
        HttpClient.newHttpClient().send(request, BodyHandlers.ofFile(Paths.get("example.png")));
    }
}
```

Run: java Screenshot.java. Java 11 or newer, standard library only. `BodyHandlers.ofFile` writes the body straight to the file, so the image is never held in memory. `HttpClient` is in the standard library since Java 11. [10][11][12] Status: checked against the documentation only.

### C# (HttpClient)

```csharp
var query = "url=" + Uri.EscapeDataString("https://example.com")
    + "&full_page=true"
    + "&format=png";

using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-Access-Key", "YOUR_ACCESS_KEY");

var bytes = await client.GetByteArrayAsync($"https://curlshot.com/api/v1/screenshot?{query}");
await File.WriteAllBytesAsync("example.png", bytes);
```

Run: dotnet run, in a console project made with dotnet new console. .NET 6 or newer. `GetByteArrayAsync` throws an `HttpRequestException` when the status is outside 200 to 299, so this version never saves an error as an image. [13] Status: checked against the documentation only.

## Track A: drive a headless browser yourself

Playwright publishes official libraries for Node.js, Python, Java and .NET, and each one takes a full-page screenshot with page.screenshot [1][2][3][4]. Puppeteer is a Node.js library with the same shape [5][6]. There is no access key in this track; you install, launch and close a browser instead.

### Node.js, Playwright

```javascript
import { chromium } from 'playwright'

const browser = await chromium.launch()
const page = await browser.newPage()
await page.goto('https://example.com')
await page.screenshot({ path: 'example.png', fullPage: true })
await browser.close()
```

Run: npm install playwright, npx playwright install chromium, then node screenshot.mjs. The option is `fullPage`. [1] Status: run and syntax-checked.

### Node.js, Puppeteer

```javascript
import puppeteer from 'puppeteer'

const browser = await puppeteer.launch()
const page = await browser.newPage()
await page.goto('https://example.com')
await page.screenshot({ path: 'example.png', fullPage: true })
await browser.close()
```

Run: npm install puppeteer (it downloads a browser), then node screenshot.mjs. The option is `fullPage`, and it is `false` unless you set it. [5][6] Status: run and syntax-checked.

### Python, Playwright

```python
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("https://example.com")
    page.screenshot(path="example.png", full_page=True)
    browser.close()
```

Run: pip install playwright, playwright install chromium, then python screenshot.py. The option is `full_page`. Playwright for Python also has an async API with the same calls. [2] Status: run and syntax-checked.

### Java, Playwright

```java
import com.microsoft.playwright.*;
import java.nio.file.Paths;

public class Screenshot {
    public static void main(String[] args) {
        try (Playwright playwright = Playwright.create()) {
            Browser browser = playwright.chromium().launch();
            Page page = browser.newPage();
            page.navigate("https://example.com");
            page.screenshot(new Page.ScreenshotOptions()
                .setPath(Paths.get("example.png"))
                .setFullPage(true));
        }
    }
}
```

Run: Add the com.microsoft.playwright dependency to a Maven or Gradle project and run the class. The option is `setFullPage`, on `Page.ScreenshotOptions`. [3] Status: checked against the documentation only.

### C#, Playwright

```csharp
using Microsoft.Playwright;

using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync();
var page = await browser.NewPageAsync();
await page.GotoAsync("https://example.com");
await page.ScreenshotAsync(new() { Path = "example.png", FullPage = true });
```

Run: Add the Microsoft.Playwright package to a console project, install its browsers as the Playwright for .NET documentation describes, then dotnet run. The option is `FullPage`. [4] Status: checked against the documentation only.

### PHP, Go and Ruby

Neither Playwright nor Puppeteer publishes a library for PHP, Go or Ruby. These projects exist, listed in the order of the languages and not ranked:

- **PHP: php-webdriver.** A community project that describes itself as the "PHP language binding for Selenium WebDriver". It drives a browser through a WebDriver server that you run next to your code. [9]
- **Go: chromedp.** A Go package that "drives browsers that speak the Chrome DevTools Protocol" and "needs no external driver". Its examples repository includes a full-page screenshot. [7]
- **Ruby: selenium-webdriver.** The Ruby binding of the Selenium project, which documents it next to its Java, Python, C# and JavaScript bindings. `driver.save_screenshot('./image.png')` saves the current browsing context; the Selenium page on screenshots does not describe a full-page option. [8]

## Matrix: eight languages, two tracks

Lines are the non-blank lines of the snippets on this page, counted by the code that builds the page.

| Language | Official headless-browser library | HTTP client used here | Lines, track A | Lines, track B | As written here |
| --- | --- | --- | --- | --- | --- |
| curl | None. curl sends requests; it does not drive a browser | curl | none | 6 | a command |
| Node.js | Playwright, Puppeteer | fetch (standard library) | 6 (Playwright), 6 (Puppeteer) | 10 | async |
| Python | Playwright | requests (one package) | 7 (Playwright) | 12 | sync |
| PHP | None from Playwright or Puppeteer. Community: php-webdriver | curl extension (standard library) | none | 12 | sync |
| Go | None from Playwright or Puppeteer. Community: chromedp | net/http (standard library) | none | 31 | sync |
| Ruby | None from Playwright or Puppeteer. Selenium has a Ruby binding | net/http (standard library) | none | 13 | sync |
| Java | Playwright | java.net.http.HttpClient (standard library) | 14 (Playwright) | 18 | sync |
| C# | Playwright | HttpClient (standard library) | 6 (Playwright) | 7 | async |

## How the snippets were checked

| Snippet | Status |
| --- | --- |
| Track B: curl | Run and syntax-checked |
| Track B: Node.js | Run and syntax-checked |
| Track B: Python | Run and syntax-checked |
| Track B: PHP | Checked against the documentation only |
| Track B: Go | Run and syntax-checked |
| Track B: Ruby | Run and syntax-checked |
| Track B: Java | Checked against the documentation only |
| Track B: C# | Checked against the documentation only |
| Track A: Node.js, Playwright | Run and syntax-checked |
| Track A: Node.js, Puppeteer | Run and syntax-checked |
| Track A: Python, Playwright | Run and syntax-checked |
| Track A: Java, Playwright | Checked against the documentation only |
| Track A: C#, Playwright | Checked against the documentation only |

- Run on 11 October 2026. The track B versions ran against a local test server that checks the query string and the header and answers with a PNG; they were not run against the live API. The track A versions captured https://example.com.

- No toolchain for PHP, Java or .NET was on the machine this article was written on. These versions use only calls that appear in the official documentation cited next to them, and were not executed.

## What every version needs next

### 1. Check the HTTP status before you save

A failed request answers with a status of 400 or higher and a short JSON message, not an image. Seven of the eight versions above would save that JSON as `example.png`. Save the body only when the status is 200 and the content type starts with `image/`.

Node.js:

```javascript
if (!response.ok) {
  const error = await response.json()
  throw new Error(`${response.status} ${error.error_code}: ${error.error_message}`)
}
```

### 2. Set a timeout longer than the render

A render can take as long as the `timeout` option allows: 30 seconds by default and 90 at most. Give your own HTTP client more than that, for example 120 seconds, or a slow page will look like a network failure.

Ruby:

```ruby
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", read_timeout: 120) do |http|
  http.request(request)
end
```

### 3. Retry 429 and 5xx with a pause

Status 429 means you are sending too fast or too many renders are running. Statuses 500, 502, 503 and 504 are temporary. On 429 and 503 the `Retry-After` header says how many seconds to wait; otherwise wait 1, 2, then 4 seconds. Do not retry 400, 401, 402 or 403: the same request fails the same way.

Python:

```python
for attempt in range(4):
    response = requests.get(api, params=params, headers=headers, timeout=120)
    if response.status_code not in (429, 500, 502, 503, 504):
        break
    time.sleep(int(response.headers.get("Retry-After") or 2 ** attempt))
```

### 4. Keep the key in an environment variable

A key typed into a file ends up in version control and in every copy of the code. Read it from the environment instead, and set the variable where the code runs.

curl:

```bash
export SCREENSHOT_ACCESS_KEY="YOUR_ACCESS_KEY"   # once, outside the code

curl -G "$API/screenshot" \
  --data-urlencode "url=https://example.com" \
  -H "X-Access-Key: $SCREENSHOT_ACCESS_KEY" \
  --output example.png
```

### 5. Stream large responses to disk

A full-page PNG of a long page or a video can be many megabytes. Copy the body to the file as it arrives instead of holding all of it in memory first.

Go:

```go
file, err := os.Create("example.png")
if err != nil {
	log.Fatal(err)
}
defer file.Close()

if _, err := io.Copy(file, resp.Body); err != nil {
	log.Fatal(err)
}
```

## Which track when

### Drive a browser yourself when

- The page is on `localhost` or a private network. A hosted API cannot reach those addresses.
- You already run a browser for tests, so the screenshot is one more line in code you have.
- You need to script many steps before the capture: log in, fill a form, click through a flow.
- Your language has an official library: Node.js, Python, Java or .NET with Playwright, Node.js with Puppeteer.
- Nothing may leave your machines, including the address you capture.

### Use a screenshot API when

- Your language has no official headless-browser library, such as PHP, Go or Ruby, or you only have a shell.
- You cannot install or update a browser where the code runs: a small container, a serverless function, shared hosting.
- You want the same result from every language and every machine, because the browser is not on any of them.
- You need options that take work to build yourself, such as hiding cookie banners and ads or emulating a device.
- The pages are public and the volume fits a plan with a monthly quota.

More: [getting started](https://curlshot.com/docs/getting-started), examples for [curl](https://curlshot.com/docs/examples/curl), [Node.js](https://curlshot.com/docs/examples/node), [Python](https://curlshot.com/docs/examples/python), [PHP](https://curlshot.com/docs/examples/php), [Go](https://curlshot.com/docs/examples/go) and [Ruby](https://curlshot.com/docs/examples/ruby), [the screenshot API](https://curlshot.com/tools/screenshot-api), and the comparison [headless browser vs screenshot API](https://curlshot.com/blog/screenshot-api-vs-headless-browser).

## Glossary

- **HTTP client:** The part of a language or a tool that sends HTTP requests and reads the answers: `fetch` in Node.js, `net/http` in Go and Ruby, `HttpClient` in Java and C#, and curl in a terminal.
- **Query string:** The part of an address after the `?`, a list of `name=value` pairs joined with `&`. The options of a screenshot request travel there.
- **Percent-encoding:** Writing a character that has a meaning in an address as `%` and two hexadecimal digits, so `https://example.com` inside a query string becomes `https%3A%2F%2Fexample.com`.
- **Header:** A named line that travels with a request or a response, apart from the address and the body, such as `X-Access-Key` on the way in and `Content-Type` on the way back.
- **Bearer token:** A key sent in the standard `Authorization: Bearer <token>` header. Whoever holds ("bears") the token is let in, so it must stay secret.
- **Binary response:** A response whose body is raw bytes, such as a PNG, and not text. It must be written to a file unchanged: no text decoding, no line-ending conversion.
- **Headless browser:** A real browser that runs without a window, driven by code. It loads and draws pages exactly as a visible browser does, which is why it can take screenshots.
- **SDK:** A software development kit: a library that wraps an API in functions for one language. None is needed for any snippet on this page; an HTTP client is enough.

## Questions

### What is the shortest way to take a screenshot of a website from code?

With a headless browser in Node.js or Python it is about six lines: launch a browser, open a page, go to the address, call page.screenshot with a file path, close the browser. With a screenshot API such as CurlShot it is one curl command: the address and the options in the query string, the access key in a header, and --output to save the file.

### How do I take a full-page screenshot in Python?

With Playwright for Python, call page.screenshot(path="example.png", full_page=True) after page.goto. With a screenshot API, send a GET request with the requests package and the parameter full_page set to true, then write response.content to a file opened in "wb" mode.

### Can I take a website screenshot in PHP, Go or Ruby without a headless browser library?

Yes. Playwright and Puppeteer publish no official library for PHP, Go or Ruby, but each of those languages has an HTTP client: the curl extension in PHP and net/http in Go and Ruby. One GET request to a screenshot API such as CurlShot returns the image, and the code writes the bytes to a file.

### Do I need an SDK to use a screenshot API?

No. A screenshot API that answers a GET request with the image needs only an HTTP client, which every language on this page has in its standard library or one package away. The eight versions on this page use curl, fetch, requests, the PHP curl extension, net/http in Go and Ruby, java.net.http.HttpClient and .NET HttpClient.

### Why is my saved screenshot file broken or only a few bytes long?

The request most likely failed and the code saved the error message as if it were an image. A failed request has a status of 400 or higher and a JSON body. Check the status before writing the file, and open the file in binary mode so the bytes are not changed on the way to disk.

### Which errors from a screenshot API should my code retry?

With CurlShot, retry status 429 after the number of seconds in the Retry-After header, and retry 500, 502, 503 and 504 after a short pause that doubles each time. Do not retry 400, 401, 402 or 403: the request, the key or the quota has to change first.

### Is it better to pass the API key in the URL or in a header?

A header is better for code that runs on a server, because addresses are written to logs and histories and a header is not part of the address. CurlShot accepts the key as the X-Access-Key header, as Authorization: Bearer, or as the access_key query parameter for quick tests in a terminal.

## Sources

1. Playwright, "Screenshots (Node.js)": https://playwright.dev/docs/screenshots (checked 11 October 2026)
2. Playwright, "Screenshots (Python)": https://playwright.dev/python/docs/screenshots (checked 11 October 2026)
3. Playwright, "Screenshots (Java)": https://playwright.dev/java/docs/screenshots (checked 11 October 2026)
4. Playwright, "Screenshots (.NET)": https://playwright.dev/dotnet/docs/screenshots (checked 11 October 2026)
5. Puppeteer, "Screenshots guide": https://pptr.dev/guides/screenshots (checked 11 October 2026)
6. Puppeteer, "ScreenshotOptions interface": https://pptr.dev/api/puppeteer.screenshotoptions (checked 11 October 2026)
7. chromedp on GitHub, "chromedp README": https://github.com/chromedp/chromedp (checked 11 October 2026)
8. Selenium, "Working with windows and tabs: TakeScreenshot": https://www.selenium.dev/documentation/webdriver/interactions/windows/ (checked 11 October 2026)
9. php-webdriver on GitHub, "php-webdriver README": https://github.com/php-webdriver/php-webdriver (checked 11 October 2026)
10. Oracle, "HttpResponse.BodyHandlers (Java SE 21)": https://docs.oracle.com/en/java/javase/21/docs/api/java.net.http/java/net/http/HttpResponse.BodyHandlers.html (checked 11 October 2026)
11. Oracle, "HttpRequest (Java SE 21)": https://docs.oracle.com/en/java/javase/21/docs/api/java.net.http/java/net/http/HttpRequest.html (checked 11 October 2026)
12. Oracle, "URLEncoder (Java SE 21)": https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/net/URLEncoder.html (checked 11 October 2026)
13. Microsoft Learn, "HttpClient.GetByteArrayAsync method": https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.getbytearrayasync (checked 11 October 2026)

## Go deeper

- [Getting started with the API](https://curlshot.com/docs/getting-started)
- [Node.js examples](https://curlshot.com/docs/examples/node)
- [Python examples](https://curlshot.com/docs/examples/python)
- [Every error code, and which to retry](https://curlshot.com/docs/errors)
- [The screenshot API](https://curlshot.com/tools/screenshot-api)
- [Turn a URL into an image](https://curlshot.com/tools/url-to-image)
- [From Puppeteer or Playwright to one request](https://curlshot.com/tools/puppeteer-screenshot-api)

## Read next

- [Screenshot API vs headless browser: the real cost](https://curlshot.com/blog/screenshot-api-vs-headless-browser.md): A cost calculator with every assumption editable, the hidden work behind headless Chrome, and an honest checklist for build or buy.
- [Full-page screenshot on every device and browser](https://curlshot.com/blog/full-page-screenshot-every-device.md): Exact keys and menu names to screenshot a whole web page in Chrome, Firefox, Edge, Safari, iPhone, iPad and Android, checked against each vendor.
- [How to screenshot a list of URLs in bulk](https://curlshot.com/blog/bulk-screenshots-list-of-urls.md): Screenshot hundreds or thousands of URLs: a queue simulator that says how long it takes, a short Node.js script, and what fails at scale.
