How to keep documentation screenshots up to date
Text in a docs repo gets reviewed and fixed with the code. Screenshots don't, so they fall behind every release. This guide covers the four ways teams deal with that, and how to make the screenshot in your docs come straight from your tests.
Screenshot drift
A screenshot is a copy of your UI on the day someone took it. Then a button gets renamed, a menu moves, or the colours change, and the docs start describing one screen while showing another. Call it screenshot drift. Nothing fails and nothing warns. The page looks fine to everyone except the reader trying to follow it.
Mintlify's media guide puts it plainly: "Every screenshot you publish is a commitment to update it when the UI changes." Most teams make that commitment without noticing, and nobody in particular owns it.
Drift can be worse than a missing image. A reader whose screen doesn't match the page starts to doubt the text around it too.
Docs as code skips the pictures
Docs as code, also called documentation as code, means you keep docs in Git, change them in pull requests, and build them in CI. It works well for text. A wrong sentence shows up in a diff, a reviewer catches it, and the fix ships with the code that made it wrong.
A PNG doesn't fit that loop. Git stores it, but no diff tells you it's out of date, and no build step makes a new one. The image keeps showing the UI as it was when someone committed it.
The new AI docs tools have the same gap. Mintlify's agent, GitBook Agent, Doc Holiday and Dosu all draft text updates from code changes. None of them, as far as their own pages say, captures a new screenshot. Mintlify's agent can check whether a screenshot still matches the page, but a person still has to retake it. Checked October 2026 against each vendor's own pages.
For living documentation, the images have to come from the same pipeline that ships the UI.
Four ways to keep screenshots current
1. Re-capture by hand
Someone notices a stale image, opens the app, sets up the right state, takes the shot, crops it and uploads it again. For a handful of images that change twice a year, this is fine, and it needs no tools. It breaks down once you ship every week. Nobody knows which images a release made wrong, so the cleanup waits for a reader to complain.
2. Commit the PNGs to the repo
Keeping images next to the Markdown puts them in pull requests and history, and some teams add a script that regenerates them. James Adam describes a neat version in self-updating screenshots, using headless Chrome from a Rails task. It works. You also own the script, the browser setup and the commits it produces, and every refresh adds binary files to your history. If the images live in an app repo but the docs live somewhere else, you still need a way to copy them across.
3. A help-centre tool that logs into your app
A few products record a path through your running app and re-take the screenshots for you. We checked all three on their own sites in October 2026.
- LaunchBrightly turns recorded workflows into automations that capture behind your login, audit images when the product changes, and push approved updates to every help article that uses them.
- Ferndesk is a help centre whose agent navigates your product, captures annotated screenshots and updates them when the UI changes.
- Promptless Capture logs in with a test account you store as environment variables. It re-captures when a pull request or commit touches the UI the docs reference, and has a crop and annotate editor for review.
Support and docs writers can run them without writing code. They add arrows, highlights and styling, and they sync into a help centre. In return, you hand a vendor credentials for a live environment. The recorded flows become a second test suite to keep working, and the images usually live inside the vendor's product.
Each has its own comparison page: LaunchBrightly, Ferndesk and Promptless.
4. Capture from your own tests in CI
If your app has end-to-end tests, they already put its screens into known states. Let one of them save a screenshot, publish it from CI, and embed it by a URL that always points at the newest build. That is what Screenery does. Capture is deterministic, you review the test like any other code, and no credential leaves your CI. The URL works anywhere an image URL works.
This has costs of its own. You need a test suite and a CI job, and a developer writes or edits the test for each screenshot. Screenery has no editor for arrows or callouts, so style the page in the test before the capture, or put callouts in the docs page around the image.
| Approach | Who runs it | What triggers a new image | Where the image lives |
|---|---|---|---|
| By hand | Whoever notices | A complaint | Wherever it was uploaded |
| PNGs in the repo | Developers | A commit, or your own script | Your repository |
| Help-centre tool | Support or docs team | A release or UI change, seen from outside | Usually the vendor's product |
| Tests in CI | Developers | Every push on the default branch | A stable URL, embeddable anywhere |
Step by step with Screenery
This follows the getting-started path: GitHub Actions, the Screenery GitHub App, and a public project called acme/dashboard.
1. Save a screenshot from a test
Write the file under test-results/. Its path, minus the extension, becomes its name, so pick it like a route: lowercase, area first, variant last.
import { test, expect } from '@playwright/test';
test('billing settings', async ({ page }) => {
await page.goto('/settings/billing');
await expect(page.getByRole('heading', { name: 'Billing' })).toBeVisible();
await page.screenshot({ path: 'test-results/settings/billing.png' });
});
Wait for the content you want before the shot, not for a fixed time. For sign-in, fixed data and fonts, see Screenshot automation. If you already keep Playwright goldens, Screenery can publish those files instead; see Playwright goldens and Argos folders.
2. Push from CI
Add one step after your tests:
name: screenshots
on:
push:
branches: [main]
pull_request:
jobs:
screenshots:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
# … install dependencies and run your Playwright tests …
- uses: screenery/push@v1
with:
project: acme/dashboard
path: test-results
id-token: write lets the job prove to Screenery which repository it is, so there is no secret to store. pull-requests: write lets the job post a preview comment on pull requests. Every input is on the GitHub Action reference.
3. Embed the @latest URL
The push log prints this snippet for every public screenshot. Paste it once:

It is a plain image URL, so the same line works in Markdown or MDX on Docusaurus, Mintlify, GitBook or Starlight, in a GitHub README, and in any help-centre editor that accepts an image URL. On Mintlify, see Self-updating screenshots in Mintlify docs for the Frame component and dark-mode classes. On Docusaurus or Starlight, a docs plugin can write the URL for you from a short screenery: reference. In HTML:
<img src="https://cdn.screenery.dev/acme/dashboard/settings/billing@latest.png"
alt="Billing settings" />
If your docs have a dark theme, capture a billing.dark.png beside the normal file and ask for it with ?theme=dark. See Light, dark and other themes.
4. Know what moves the image
- A push on the default branch moves @latest. Merge a UI change, and the next build replaces the image behind every embed. Screenery looks up a channel URL on each request, so there is nothing to purge. Browsers may keep a copy for up to five minutes.
- A pull request gets its own channel, such as @pr-42, and a comment showing what changed against @latest. Your docs don't change until it merges.
- The first push creates @latest from whatever branch it came from, so your first URLs work before anything merges. That happens once per project.
- A version URL never changes. Use @v_… when a page, such as a changelog entry, must keep showing one exact image.
The full rules are in How @latest moves and Screenshot URLs.
5. Optional: fail the build on an unexpected change
Set diff-baseline on the Action to a committed folder of approved screenshots. The Action compares each screenshot with that folder pixel by pixel first, and a change past your threshold fails the job.
A failing gate pushes nothing, so a regression never reaches your docs.
See The visual-diff gate.
Checklist
- List every screenshot your docs show, and the screen and state each one needs.
- Write one test per screenshot that reaches that state and waits for real content.
- Name files like routes: lowercase, area first, variant last. Renaming one later breaks its URL.
- Add screenery/push@v1 after your tests, with id-token: write.
- Replace each uploaded image in your docs with its @latest URL.
- Capture a .dark file if your docs have a dark theme.
- Add a diff-baseline if a screenshot should never change by accident.
- Delete the old PNGs from the docs repo once the embeds work.
FAQ
- Do I have to use Playwright?
- No. Screenery pushes the image files in a folder: PNG, JPEG, WebP, AVIF, GIF, WebM and MP4. Any test runner or script that writes those files works. The examples use Playwright because most teams already have it.
- Does it only work with GitHub Actions?
- No. GitHub Actions needs no secret, because the Action signs in with the job's OIDC token. Any other CI runs npx screenery push with a publish token in SCREENERY_TOKEN. See Without the GitHub App.
- Will an unmerged pull request change my published docs?
- No. A pull request pushes to its own @pr-N channel. Only a push on the default branch, or a promotion, moves @latest. The one exception is a new project's first push, which creates @latest from whatever branch it came from.
- What happens if I rename a screenshot test?
- The name is part of the URL, and there are no redirects. After the next default-branch push, the old name's @latest URL answers 404. Old version URLs keep working. Treat a screenshot name like a route.
- Can the screenshots be private?
- Yes. Unlisted screenshots need a signed URL and private ones a read token. A paste-once snippet exists only for public screenshots, but an admin can create an embed token to put a private screenshot in a page. See Embedding.
- Can it catch a screenshot that changed by mistake?
- Yes, if you give it a baseline. With diff-baseline set, the Action compares every screenshot with the committed baseline before it publishes. A change past the threshold fails the job, and the Action pushes nothing.
Start
Follow Getting started. Or add the MCP server with claude mcp add screenery -- npx -y screenery mcp and ask your agent to set up self-updating screenshots for the repository; see Screenery for AI agents.
Related guides: Screenshot automation for docs and READMEs, Self-updating screenshots in Mintlify docs, Markdown image syntax, README screenshots that update themselves and Playwright visual regression testing.
Competitor details come from each vendor's own pages, checked October 2026. Products change; follow the links for their current features.