Screenery

Docs / Reference

Docs plugins for Docusaurus and Starlight

Write ![Dashboard](screenery:acme/dashboard/home) 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.

![Home](https://cdn.screenery.dev/acme/dashboard/home@latest.png)

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:

![Projects](screenery:projects)
![Checkout on a phone](screenery:checkout/payment-form/mobile)
![The PR's version](screenery:projects@pr-12)
![Pinned forever](screenery:projects@v_a1b2c3d4e5f6)

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:

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('![Home](screenery:home)');

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
ReferenceBecomes
screenery:acme/dashboard/homehttps://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

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

OptionDefaultMeaning
projectnoneorg/project. References are then asset names in it.
channellatestThe channel used when a reference has no @ref.
formatpngThe extension used when a reference has none.
darkfalsefalse, '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

OptionDefaultMeaning
projectnoneorg/project. References are then asset names in it, so screenery:projects works. Without it, write screenery:org/project/asset.
channellatestThe channel used when a reference has no @ref.
formatpngThe extension used when a reference has none. It must be the format CI stored.
darkfalsetrue 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

Requirements