Automate Documentation Screenshots So They Never Go Stale

2026-04-14 | Tags: [screenshot-api, documentation, automation, developer-tools]

If you maintain product documentation, you know the pain: screenshots go stale the moment your UI changes. A button moves, a color shifts, a new feature appears — and suddenly your docs show something that doesn't match reality.

Most teams handle this manually. Someone takes new screenshots, resizes them, uploads them. It's tedious, error-prone, and always deprioritized. Here's how to automate it entirely with a screenshot API.

The Problem with Manual Screenshots

Documentation screenshots break silently. Nobody gets an alert when a UI change makes a docs image outdated. The feedback loop is:

  1. UI changes in a release
  2. Docs screenshots become stale
  3. A user notices (weeks later)
  4. Someone manually retakes screenshots
  5. Repeat

The fix is to make screenshot capture part of your build or deploy process.

Capturing Screenshots on Deploy

Here's a Python script that captures screenshots of key pages and saves them to your docs directory:

import requests
import os
from pathlib import Path

API_BASE = "https://hermesforge.dev/api"
DOCS_IMG_DIR = Path("docs/images/screenshots")
DOCS_IMG_DIR.mkdir(parents=True, exist_ok=True)

# Define the pages you need screenshots of
PAGES = {
    "dashboard": {
        "url": "https://app.example.com/dashboard",
        "width": 1280,
        "height": 800,
        "delay": 2000,  # wait for charts to render
    },
    "settings": {
        "url": "https://app.example.com/settings",
        "width": 1280,
        "height": 900,
    },
    "login": {
        "url": "https://app.example.com/login",
        "width": 800,
        "height": 600,
    },
    "mobile-nav": {
        "url": "https://app.example.com/dashboard",
        "width": 375,
        "height": 812,
        "delay": 1000,
    },
}

def capture_page(name, config):
    """Capture a single page screenshot."""
    params = {
        "url": config["url"],
        "width": config.get("width", 1280),
        "height": config.get("height", 800),
        "format": "webp",
        "block_ads": "true",
    }
    if "delay" in config:
        params["delay"] = config["delay"]

    resp = requests.get(f"{API_BASE}/screenshot", params=params, timeout=30)
    if resp.status_code == 200:
        output = DOCS_IMG_DIR / f"{name}.webp"
        output.write_bytes(resp.content)
        size_kb = len(resp.content) / 1024
        print(f"  {name}: {size_kb:.1f}KB -> {output}")
        return True
    else:
        print(f"  {name}: FAILED ({resp.status_code})")
        return False

def main():
    print(f"Capturing {len(PAGES)} documentation screenshots...")
    results = {name: capture_page(name, cfg) for name, cfg in PAGES.items()}

    succeeded = sum(results.values())
    failed = len(results) - succeeded
    print(f"\nDone: {succeeded} captured, {failed} failed")

    if failed > 0:
        exit(1)

if __name__ == "__main__":
    main()

Integrating with Your Build System

In a Makefile

docs-screenshots:
    python scripts/capture_docs_screenshots.py

docs-build: docs-screenshots
    mkdocs build

deploy: docs-build
    rsync -az site/ server:/var/www/docs/

In a CI Pipeline (GitHub Actions)

name: Update Docs Screenshots
on:
  push:
    branches: [main]
    paths: ['src/**']  # only when source code changes

jobs:
  update-screenshots:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Capture screenshots
        run: python scripts/capture_docs_screenshots.py

      - name: Check for changes
        id: diff
        run: |
          git diff --quiet docs/images/screenshots/ || echo "changed=true" >> $GITHUB_OUTPUT

      - name: Commit updated screenshots
        if: steps.diff.outputs.changed == 'true'
        run: |
          git config user.name "docs-bot"
          git config user.email "docs-bot@example.com"
          git add docs/images/screenshots/
          git commit -m "docs: update screenshots [automated]"
          git push

This workflow runs whenever source code changes, captures fresh screenshots, and commits them only if they've actually changed.

Handling Dynamic Content

Real apps have dynamic content — user names, dates, notification counts. You don't want your docs screenshots showing "Welcome, test-user-47" or yesterday's date.

Option 1: Custom JavaScript Injection

Use the js parameter to normalize dynamic content before the screenshot:

params = {
    "url": "https://app.example.com/dashboard",
    "width": 1280,
    "height": 800,
    "js": """
        // Replace dynamic user name
        document.querySelectorAll('.user-name').forEach(el => {
            el.textContent = 'Jane Developer';
        });
        // Normalize dates
        document.querySelectorAll('.date-display').forEach(el => {
            el.textContent = 'March 15, 2026';
        });
        // Clear notification badges
        document.querySelectorAll('.notification-badge').forEach(el => {
            el.remove();
        });
    """,
    "delay": 1000,
    "format": "webp",
}

Option 2: Dedicated Docs Environment

Point your screenshot script at a staging environment with seeded data:

PAGES = {
    "dashboard": {
        "url": os.getenv("DOCS_APP_URL", "https://docs-staging.example.com") + "/dashboard",
        # ...
    },
}

Comparing Old and New Screenshots

Sometimes you want to know what changed, not just that something changed. Add a comparison step:

import hashlib

def file_hash(path):
    return hashlib.sha256(path.read_bytes()).hexdigest()

def capture_with_diff_detection(name, config):
    output = DOCS_IMG_DIR / f"{name}.webp"
    old_hash = file_hash(output) if output.exists() else None

    success = capture_page(name, config)
    if not success:
        return "failed"

    new_hash = file_hash(output)
    if old_hash is None:
        return "new"
    elif old_hash != new_hash:
        return "changed"
    else:
        return "unchanged"

Optimizing Image Size

Documentation sites serve a lot of images. WebP format (supported by the screenshot API via format=webp) typically produces files 30-50% smaller than PNG with no visible quality loss.

For a docs site with 20 screenshots: - PNG: ~4MB total - WebP: ~2MB total

That's 2MB less for your readers to download on every page load.

Format Typical Size (1280x800) Browser Support Best For
PNG 200-400KB Universal Pixel-perfect accuracy
WebP 100-200KB 97%+ browsers General documentation
JPEG 80-150KB Universal Photos, not UI screenshots

When This Approach Works Best

This pattern is ideal when:

It's less useful when: - Your UI rarely changes - You need annotated screenshots (arrows, callouts) — though you can layer those on with a tool like Pillow - Your app requires authentication that can't be bypassed in a staging environment

Full Script with All Features

Here's the complete version combining everything above:

#!/usr/bin/env python3
"""Capture documentation screenshots automatically."""

import requests
import hashlib
import json
import sys
from pathlib import Path
from datetime import datetime

API_BASE = "https://hermesforge.dev/api"
DOCS_IMG_DIR = Path("docs/images/screenshots")
MANIFEST_FILE = DOCS_IMG_DIR / "manifest.json"

def load_config(config_file="docs-screenshots.json"):
    return json.loads(Path(config_file).read_text())

def file_hash(path):
    if not path.exists():
        return None
    return hashlib.sha256(path.read_bytes()).hexdigest()

def capture(name, config):
    params = {
        "url": config["url"],
        "width": config.get("width", 1280),
        "height": config.get("height", 800),
        "format": config.get("format", "webp"),
        "block_ads": "true",
    }
    for opt in ("delay", "js", "scale"):
        if opt in config:
            params[opt] = config[opt]

    output = DOCS_IMG_DIR / f"{name}.{params['format']}"
    old_hash = file_hash(output)

    try:
        resp = requests.get(f"{API_BASE}/screenshot",
                          params=params, timeout=30)
        if resp.status_code != 200:
            return {"status": "failed", "code": resp.status_code}

        output.write_bytes(resp.content)
        new_hash = file_hash(output)

        status = "new" if old_hash is None else (
            "changed" if old_hash != new_hash else "unchanged"
        )
        return {
            "status": status,
            "size_kb": round(len(resp.content) / 1024, 1),
            "path": str(output),
        }
    except requests.Timeout:
        return {"status": "timeout"}

def main():
    DOCS_IMG_DIR.mkdir(parents=True, exist_ok=True)
    pages = load_config()

    print(f"Capturing {len(pages)} screenshots...")
    results = {}
    for name, config in pages.items():
        result = capture(name, config)
        results[name] = result
        icon = {"new": "+", "changed": "~", "unchanged": "=",
                "failed": "!", "timeout": "T"}
        print(f"  [{icon.get(result['status'], '?')}] {name}: {result['status']}")

    # Write manifest for tracking
    manifest = {
        "captured_at": datetime.utcnow().isoformat() + "Z",
        "results": results,
    }
    MANIFEST_FILE.write_text(json.dumps(manifest, indent=2))

    changed = sum(1 for r in results.values() if r["status"] == "changed")
    failed = sum(1 for r in results.values()
                 if r["status"] in ("failed", "timeout"))

    print(f"\nSummary: {changed} changed, {failed} failed, "
          f"{len(results) - changed - failed} unchanged/new")

    sys.exit(1 if failed > 0 else 0)

if __name__ == "__main__":
    main()

Save your page definitions in docs-screenshots.json:

{
    "dashboard": {
        "url": "https://app.example.com/dashboard",
        "width": 1280,
        "height": 800,
        "delay": 2000
    },
    "settings": {
        "url": "https://app.example.com/settings",
        "width": 1280,
        "height": 900
    }
}

Then run python capture_docs_screenshots.py in your CI pipeline and never manually take a docs screenshot again.


Built with the Screenshot API — capture any webpage as an image with a single HTTP call.