Screenery

Screenery / Guides

Self-updating screenshots in Mintlify docs

Mintlify keeps your text in Git and rebuilds it on every change. The screenshots stay as they were the day someone took them. This guide puts an image URL in your MDX that your own tests refresh on every merge, inside Mintlify's Frame component, in light and dark.

The upkeep problem

Mintlify's own media guide says it directly: "Every screenshot you publish is a commitment to update it when the UI changes."

Its advice for living with that is sound. Crop tightly, skip screenshots of pages that change often, keep the source files, and note what each image shows so you can find the stale ones. For a redesign, it suggests treating the screenshot updates as a docs sprint. All of that makes recapturing cheaper. A person still has to notice, recapture and commit.

If your product has end-to-end tests, you can skip that step. A test reaches the screen, saves a screenshot, and CI publishes it at a URL your MDX points to. When the UI changes and the change merges, the next build replaces the image.

How Mintlify shows images

From Images and embeds and Frames in Mintlify's docs, checked October 2026:

An absolute URL works wherever a path does. Mintlify's own examples load images from a CDN. So nothing changes on the Mintlify side, and nothing needs installing there. You swap a path for a URL. Docusaurus and Starlight can build that URL from a short reference with a docs plugin, but Mintlify does not run remark plugins, so here you paste the full URL.

The trade-off is that Mintlify doesn't optimize an image at a URL. Keep each capture to the size the page shows. Set the test's viewport, or screenshot one element instead of the whole page.

1. Capture the screenshots in your app's tests

The screenshots come from the repository that holds your app, whether your Mintlify content lives in that repository or its own. Take a light and a dark version of each screen:

// tests/docs.spec.ts
import { test, expect } from '@playwright/test';

for (const theme of ['light', 'dark'] as const) {
  test(`billing settings (${theme})`, async ({ page }) => {
    await page.emulateMedia({ colorScheme: theme });
    await page.goto('/settings/billing');
    await expect(page.getByRole('heading', { name: 'Billing' })).toBeVisible();
    const suffix = theme === 'dark' ? '.dark' : '';
    await page.screenshot({
      path: `screenshots/settings/billing${suffix}.png`,
      animations: 'disabled',
    });
  });
}

billing.png is published as settings/billing. billing.dark.png is not a second screenshot. It is the dark version of the same one, served when a page asks for ?theme=dark. Push both in the same push and save both in the same format. If your app has its own theme switch instead of following the system setting, set what the switch sets before the page loads. See Dark mode and Light, dark and other themes.

Sign-in, fixed data, fonts and viewports are covered in Screenshot automation. The aim is a test whose picture changes only when the UI does.

2. Publish from CI

This assumes a public Screenery project, acme/dashboard, created with the Screenery GitHub App on the app's repository. Getting started covers that. Add this workflow to the app's repository, not the docs one:

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@v6
      - uses: actions/setup-node@v6
        with:
          node-version: lts/*
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test
      - uses: screenery/push@v1
        with:
          project: acme/dashboard
          path: screenshots

id-token: write lets the job prove to Screenery which repository it runs for, 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 URL in MDX

The simplest form is a Markdown image:

![Billing settings](https://cdn.screenery.dev/acme/dashboard/settings/billing@latest.png)

Inside a Frame, with a caption:

<Frame caption="Billing settings, from the latest build">
  <img
    src="https://cdn.screenery.dev/acme/dashboard/settings/billing@latest.png"
    alt="Billing settings with the current plan and payment method"
  />
</Frame>

With a light and a dark variant, using Mintlify's dark-mode classes:

<Frame caption="Billing settings">
  <img
    className="block dark:hidden"
    src="https://cdn.screenery.dev/acme/dashboard/settings/billing@latest.png"
    alt="Billing settings with the current plan and payment method"
  />
  <img
    className="hidden dark:block"
    src="https://cdn.screenery.dev/acme/dashboard/settings/billing@latest.png?theme=dark"
    alt="Billing settings with the current plan and payment method"
  />
</Frame>

Both images use the same @latest URL, and the dark one adds ?theme=dark. A missing theme is never a 404. If a screen has no dark capture yet, the dark URL serves the light image, so you can write every embed this way from the start.

The push log prints a ready-made Markdown snippet for every public screenshot, so you rarely type a URL by hand. The extension is the format you saved. Screenery doesn't convert formats, so a PNG stays .png.

Replacing images you already have

  1. Search your MDX for /images/ and list the screenshots of your product.
  2. Write one test per screen that reaches the right state and saves the file under a stable name.
  3. Merge, then swap each path for its @latest URL.
  4. Delete the old PNGs from the docs repository once the new embeds show up.

Keep repository images for things that rarely change, such as logos and diagrams. The URLs are for screens of a UI that keeps moving.

What happens after a merge

The full rules are in How @latest moves. To fail a pull request when a screenshot changes by accident, see Visual regression testing in CI.

Mintlify's agent

Mintlify's agent docs say it can look at the images in your docs and check whether a screenshot still matches the content. They don't say it takes a new screenshot. So the agent can tell you an image is out of date, and a person retakes it.

With Screenery URLs in your MDX, the retaking happens in CI instead. The agent checks the words and the images, and your tests replace the images. Checked October 2026 against Mintlify's docs.

FAQ

Can Mintlify show an image from an external URL?
Yes. Put the full URL in Markdown image syntax or in an img tag's src. Mintlify's own examples load images from a CDN. Root-relative paths are for images stored in the docs repository, and those files must be under 20 MB.
How do I show a different screenshot in Mintlify's dark mode?
Use two img tags. Give the light one className="block dark:hidden" and the dark one className="hidden dark:block", which is Mintlify's documented pattern. With Screenery, both use the same @latest URL and the dark one adds ?theme=dark. If no dark screenshot was captured, that URL serves the light image instead of a 404.
Do I have to redeploy my Mintlify docs when a screenshot changes?
No. The MDX keeps the same URL, and Screenery changes the image behind it when your default branch builds. A browser may keep the old image for up to five minutes.
Will an open pull request change the screenshots in my Mintlify docs?
No. A pull request publishes to its own channel, such as @pr-12. 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.
Does Mintlify's agent retake screenshots?
Mintlify's agent docs say it can look at the images in your docs and check whether a screenshot still matches the content. They don't say it takes a new screenshot. Screenery does the retaking, from your tests, so the two can work side by side.

Related guides

Mintlify details come from Mintlify's own docs, checked October 2026. Follow the links for their current behaviour.