Docs / Reference
Docs plugins for Docusaurus and Starlight
Write  in a Markdown or MDX page. When your docs site builds, the plugin replaces it with https://cdn.screenery.dev/acme/dashboard/home@latest.png, the URL that serves the newest screenshot from your default branch. CI keeps pushing, and the page shows the new image without an edit.
Which package
You can always paste the full CDN URL, which is ordinary Markdown. A plugin saves typing. It also checks each reference against the URL rules while the site builds, so a typo shows up in the build log instead of as a broken image on the live page.
- Astro Starlight. Install starlight-screenery. It installs remark-screenery for you and wires it into whichever Markdown processor your site uses.
- Docusaurus, or any pipeline that takes remark plugins. Install remark-screenery.
- MkDocs, Mintlify, GitBook and GitHub READMEs. These don't run remark plugins. Paste the CDN URL. See Self-updating screenshots in Mintlify docs and README screenshots that update themselves.

Docusaurus
npm install remark-screenery
Docusaurus has no site-wide remark option. Add the plugin to each content plugin whose pages use it, such as docs, blog and pages:
// docusaurus.config.js
import remarkScreenery from 'remark-screenery';
const screenery = [remarkScreenery, { project: 'acme/dashboard', dark: 'switch' }];
export default {
// ...
presets: [
[
'classic',
{
docs: { remarkPlugins: [screenery] },
blog: { remarkPlugins: [screenery] },
pages: { remarkPlugins: [screenery] },
theme: {
customCss: ['./src/css/custom.css'],
},
},
],
],
};
dark: 'switch' writes a light and a dark image for each screenshot, and your CSS hides one. Docusaurus sets data-theme="dark" on <html> when its switch is on dark, so these two rules in src/css/custom.css do it:
:root:not([data-theme='dark']) .screenery-dark { display: none; }
[data-theme='dark'] .screenery-light { display: none; }
The package ships the same rules as remark-screenery/themes.css, if you'd rather load a file:
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
// theme: { customCss: ['./src/css/custom.css', require.resolve('remark-screenery/themes.css')] }
Leave out dark and you need no CSS at all.
Starlight
npm install starlight-screenery
Don't use astro add. It would register the package as an Astro integration, and it belongs in Starlight's plugins list:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
import starlightScreenery from 'starlight-screenery';
export default defineConfig({
integrations: [
starlight({
title: 'My docs',
plugins: [starlightScreenery({ project: 'acme/docs', dark: true })],
}),
],
});
Then, in any .md or .mdx page:
   
With dark: true, each screenshot becomes two images. The light one gets Starlight's dark:sl-hidden class and the dark one gets light:sl-hidden, so Starlight's own CSS shows whichever matches its theme switch. The plugin adds no stylesheet.
Which Markdown processor it hooks into
You configure none of this. The plugin looks at your Astro config and picks one:
- Astro 7 with Sätteri, the default. Sätteri does not run remark plugins, so the plugin adds a Sätteri visitor that writes the same output.
- Astro 7 with markdown.processor: unified() from @astrojs/markdown-remark. The plugin adds remark-screenery to that processor.
- Before Astro 7. The plugin adds remark-screenery to markdown.remarkPlugins.
- Any other processor. The build logs a warning that the processor is not supported, and screenery: images stay as written.
The package README reports a run on Starlight 0.42.6 with Astro 7.3.8, with both processors, on .md and .mdx pages. Nobody has tried it on older Starlight releases yet.
Other remark pipelines
In a plain unified pipeline:
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';
import remarkScreenery from 'remark-screenery';
const html = await unified()
.use(remarkParse)
.use(remarkScreenery, { project: 'acme/dashboard' })
.use(remarkRehype)
.use(rehypeStringify)
.process('');
With @mdx-js/mdx:
compile(source, { remarkPlugins: [[remarkScreenery, options]] })
Other tools that accept remark plugins should work the same way. Nextra takes them through mdxOptions.remarkPlugins, for example. Docusaurus and Starlight are the only two anyone has tried.
References
A reference is the CDN path after cdn.screenery.dev/, with @ref and the extension optional:
screenery:{org}/{project}/{asset}[@{ref}][.{ext}]
screenery:{asset}[@{ref}][.{ext}] with the project option set
| Reference | Becomes |
|---|---|
| screenery:acme/dashboard/home | https://cdn.screenery.dev/acme/dashboard/home@latest.png |
| screenery:acme/dashboard/checkout/payment-form | …/acme/dashboard/checkout/payment-form@latest.png |
| screenery:acme/dashboard/home@pr-12 | …/acme/dashboard/home@pr-12.png |
| screenery:acme/dashboard/home@v_a1b2c3d4e5f6 | …/acme/dashboard/home@v_a1b2c3d4e5f6.png, one image that never changes |
| screenery:acme/dashboard/home.webp | …/acme/dashboard/home@latest.webp |
| screenery:home with project: 'acme/dashboard' | …/acme/dashboard/home@latest.png |
- @ref is a channel or a version. A channel is a name such as latest, main, pr-12 or v2.3.1. A version is v_ and 12 hex characters. See How @latest moves.
- The extension must be the format CI stored. Screenery does not convert images, so a .webp URL for a stored PNG answers 501. The plugins accept png, jpg, webp, avif and gif. They reject .jpeg, which is spelled .jpg here, and .webm and .mp4, because an <img> can't play a video.
- With project set, the whole reference is an asset name in that project. screenery:acme/marketing/hero then means an asset called acme/marketing/hero inside your project. To show a screenshot from another project on the same page, paste its CDN URL.
- No query strings or fragments. Ask for dark versions with the dark option instead of ?theme=dark.
- Reference-style images work. [home]: screenery:home becomes the plain URL. A definition never gets the dark version, because several images can share it.
- The title stays.  keeps its title attribute.
Options
Both packages take an options object. A bad option throws when the plugin is created, before any page renders. For Starlight, that is when Astro loads astro.config.mjs.
remark-screenery
| Option | Default | Meaning |
|---|---|---|
| project | none | org/project. References are then asset names in it. |
| channel | latest | The channel used when a reference has no @ref. |
| format | png | The extension used when a reference has none. |
| dark | false | false, 'picture' or 'switch'. See Dark versions. |
| classNames | { light: 'screenery-light', dark: 'screenery-dark' } | The two classes dark: 'switch' puts on its images. Both must be non-empty. |
starlight-screenery
| Option | Default | Meaning |
|---|---|---|
| project | none | org/project. References are then asset names in it, so screenery:projects works. Without it, write screenery:org/project/asset. |
| channel | latest | The channel used when a reference has no @ref. |
| format | png | The extension used when a reference has none. It must be the format CI stored. |
| dark | false | true also writes the ?theme=dark image and shows it when Starlight's theme switch is on dark. |
Starlight's dark is a plain boolean. It always uses Starlight's classes, so there is no classNames option and no 'picture' mode.
Dark versions
If CI pushes home.dark.png beside home.png, the CDN serves the dark one at …/home@latest.png?theme=dark. With no dark version, that URL serves the normal image, so asking for dark is always safe. See Light, dark and other themes.
dark: 'picture' follows the reader's system setting. Use it on a site with no light and dark switch of its own:
<picture> <source media="(prefers-color-scheme: dark)" srcset="https://cdn.screenery.dev/acme/dashboard/home@latest.png?theme=dark"> <img src="https://cdn.screenery.dev/acme/dashboard/home@latest.png" alt="Home"> </picture>
dark: 'switch' follows the site's own switch, which can disagree with the system setting. It writes two images, and your CSS hides one:
<img src="…/home@latest.png" alt="Home" class="screenery-light" loading="lazy"> <img src="…/home@latest.png?theme=dark" alt="Home" class="screenery-dark" loading="lazy">
loading="lazy" keeps the browser from downloading the hidden one. Starlight's dark: true writes this same pair with Starlight's classes.
A version reference such as @v_a1b2c3d4e5f6 always gets one image, because the CDN answers 400 to ?theme= on a version URL.
Both forms come out as Markdown syntax tree nodes that carry their HTML element and attributes, not as raw HTML. MDX drops raw HTML nodes, and these survive it, so .mdx pages get the same output as .md pages.
When a reference is wrong
A reference that doesn't fit the URL grammar stops the build. Names are lowercase only and nothing is decoded, so screenery:acme/dashboard/Home fails. The message names the file, line and column:
remark-screenery: docs/intro.md:12:1: "screenery:acme/dashboard/Home" is not a valid reference: "Home" is not an asset name. …
On Starlight, the package README reports a different outcome. Astro 7.3 logs the error and still finishes the build, and the page ships with an empty body. Read the build log or the astro dev terminal after you add a reference. The line number counts from the end of the frontmatter.
[ERROR] [starlight-docs-loader] Error rendering bad.md: remark-screenery: …/bad.md:1:1: "screenery:Projects" is not a valid reference: "Projects" is not an asset name. …
What the plugins leave alone
- Every other image. Relative paths, other hosts, full Screenery CDN URLs and data: URLs pass through unchanged. So does Screenery: with a capital S.
- Links. [text](screenery:…) stays a link to screenery:….
- JSX and HTML. <img src="screenery:…"> in an MDX page is not rewritten. Use Markdown image syntax or paste the CDN URL.
- Private screenshots. The plugins write bare URLs, which open public screenshots only. An unlisted or private screenshot answers 404 without a credential, and signed URLs expire, so neither belongs in a built docs page. See Embedding for the alternatives.
Requirements
- Node 18 or later. Both packages are ESM only.
- No peer dependencies. remark-screenery has no dependencies at all, so it never pins a unified version that differs from your site's. starlight-screenery depends only on remark-screenery and doesn't declare Astro or Starlight as dependencies. It needs a Starlight version with a plugins option.
- Screenshots pushed from CI. The plugins only write URLs. Something has to publish the images first. Getting started sets that up with screenery/push@v1.
- MIT licensed.