Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d9f8880
Storybook: Declare an explicit ID on every story and doc
Mustafabharmal Aug 27, 2026
3aa6769
Storybook: Extract the stories globs into their own module
Mustafabharmal Aug 27, 2026
8d3d307
Storybook: Test that every story and doc declares an ID
Mustafabharmal Aug 27, 2026
3188a2d
Storybook: Fail CI when a published story URL disappears
Mustafabharmal Aug 27, 2026
a52a39d
Storybook: Document the stable story URL convention
Mustafabharmal Aug 27, 2026
c3574e4
Storybook: Note the ID shared between the two Icon story files
Mustafabharmal Aug 28, 2026
e6fbe17
Add new story IDs for searchable chip select and select controls
Mustafabharmal Aug 28, 2026
27c0701
Add new story IDs for grid dashboard and lanes size limits
Mustafabharmal Aug 28, 2026
ba4c9ab
Merge remote-tracking branch 'upstream/trunk' into add/storybook-stab…
Mustafabharmal Sep 22, 2026
d576cca
Add new story IDs for various design system components and playground…
Mustafabharmal Sep 22, 2026
7b0c452
Merge branch 'trunk' into add/storybook-stable-story-ids
Mustafabharmal Sep 22, 2026
a68888d
Merge branch 'trunk' into add/storybook-stable-story-ids
Mustafabharmal Oct 6, 2026
592ef72
Storybook: Import the Vitest globals in the story ID test
Mustafabharmal Oct 6, 2026
59e3f33
Storybook: Read story metas with Storybook's CSF parser
Mustafabharmal Oct 6, 2026
c768a63
Storybook: Reuse the shared stories globs in main.ts
Mustafabharmal Oct 6, 2026
e722df5
Storybook: Restore the note on the ID shared by the Icon story files
Mustafabharmal Oct 6, 2026
e7a37f6
Storybook: Declare an ID on the Design System contributing doc
Mustafabharmal Oct 6, 2026
7c7af36
Storybook: Refresh the story ID snapshot after merging trunk
Mustafabharmal Oct 6, 2026
bce4061
Merge branch 'trunk' into add/storybook-stable-story-ids
Mustafabharmal Oct 7, 2026
5a7affe
Storybook: Find story files with globSync in the story ID test
Mustafabharmal Oct 7, 2026
9400009
Storybook: Document the stable story URL convention
Mustafabharmal Oct 7, 2026
8424800
Storybook: Refresh the story ID snapshot after merging trunk
Mustafabharmal Oct 7, 2026
7427d66
Storybook: Check MDX doc IDs with Storybook's MDX analyzer
Mustafabharmal Oct 7, 2026
82efa75
Merge branch 'trunk' into add/storybook-stable-story-ids
Mustafabharmal Oct 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/storybook-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,20 @@ jobs:
- name: Setup Node.js and install dependencies
uses: ./.github/setup-node

# Every story URL is built from a declared ID. Regenerating the
# committed list of them fails when one is gone, so losing a
# published URL has to be deliberate. The story index is read from
# source, so this runs before the build rather than after it.
- name: Check no story URL disappeared
run: npm run storybook:story-id-snapshot

- name: Check story ID snapshot is up to date
run: |
if ! git diff --exit-code -- storybook/story-ids.txt; then
echo "::error::The Storybook story ID snapshot is out of date. Review the diff above to confirm the change is expected, then run 'npm run storybook:story-id-snapshot' locally and commit the updated storybook/story-ids.txt."
exit 1
fi

- name: Build Storybook
env:
NODE_OPTIONS: --max-old-space-size=8192
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@
"storybook:e2e:build": "npm run --workspace @wordpress/storybook-playwright storybook:build",
"storybook:manifest-snapshot": "npm run --workspace @wordpress/storybook manifest-snapshot",
"test": "npm run lint && npm run test:unit",
"storybook:story-id-snapshot": "npm run --workspace @wordpress/storybook story-id-snapshot",
"test:create-block": "npm run --workspace @wordpress/validation-tools test-create-block --",
"test:e2e": "npm exec --no --workspace @wordpress/e2e-tests-playwright -- wp-scripts test-e2e --config playwright.config.ts",
"test:e2e:debug": "npm exec --no --workspace @wordpress/e2e-tests-playwright -- wp-scripts test-e2e --config playwright.config.ts --ui",
Expand Down
2 changes: 2 additions & 0 deletions packages/icons/src/icon/stories/index.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import * as icons from '../../';

const meta = {
component: Icon,
// Shares its `id` and `title` with `storybook/stories/icons/library.story.tsx`
// so both files merge into one entry. Change them together.
id: 'icons-icon',
title: 'Design System/Icons/Icon',
parameters: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const Verifier: React.FC< VerifierProps > = () => null;
*/
const meta: Meta< typeof Verifier > = {
title: 'Design System/Theme/Theme Provider/Brand Color Fallbacks',
id: 'design-system-theme-theme-provider-brand-color-fallbacks',
component: Verifier,
argTypes: {
adminThemeColor: {
Expand Down
1 change: 1 addition & 0 deletions packages/theme/src/color-ramps/stories/index.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const ColorGen = ( props: {

const meta: Meta< typeof ColorGen > = {
title: 'Design System/Theme/Theme Provider/Color Scales',
id: 'design-system-theme-theme-provider-color-scales',
component: ColorGen,
argTypes: {
background: {
Expand Down
1 change: 1 addition & 0 deletions packages/theme/src/stories/index.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const wpdsColorTokens = wpdsTokens.filter( ( name ) =>

const meta: Meta< typeof ThemeProvider > = {
title: 'Design System/Theme/Theme Provider',
id: 'design-system-theme-theme-provider',
component: ThemeProvider,
args: {
isRoot: true,
Expand Down
2 changes: 1 addition & 1 deletion packages/theme/src/stories/introduction.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Meta, Markdown } from '@storybook/addon-docs/blocks';
import Readme from '../../README.md?raw';

<Meta title="Design System/Theme/Introduction" />
<Meta title="Design System/Theme/Introduction" id="design-system-theme-introduction" />

<Markdown>{Readme}</Markdown>
2 changes: 1 addition & 1 deletion packages/theme/src/stories/tokens.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Meta, Markdown } from '@storybook/addon-docs/blocks';
import Tokens from '../../docs/tokens.md?raw';

<Meta title="Design System/Tokens/Introduction" />
<Meta title="Design System/Tokens/Introduction" id="design-system-tokens-introduction" />

<Markdown>{Tokens}</Markdown>
2 changes: 1 addition & 1 deletion packages/theme/src/stories/typography.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Meta } from '@storybook/addon-docs/blocks';
import { TypographyTokenPreview } from './typography-preview';
import '@wordpress/theme/design-tokens.css';

<Meta title="Design System/Tokens/Typography" />
<Meta title="Design System/Tokens/Typography" id="design-system-tokens-typography" />

# Typography tokens

Expand Down
28 changes: 28 additions & 0 deletions storybook/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,31 @@ When a snapshot changes due to code changes or Storybook upgrades, a pull reques
npm run storybook:build
npm run storybook:manifest-snapshot
```

## Stable story URLs

A story's URL is built from its `id`. Without one, Storybook derives the `id` from the `title`, so moving a story to another sidebar folder changes its URL and breaks existing links. Every story and doc therefore declares an `id`, and the `title` only sets its place in the sidebar:

```ts
const meta = {
title: 'Components/Containers/Card', // Sidebar position, free to change.
id: 'components-card', // URL, never changes.
};
```

In MDX, set it on the `Meta` block: `<Meta title="Design System/Introduction" id="design-system-introduction" />`. An MDX doc attached to a CSF file with `of={ … }` inherits that file's `id` and needs none of its own.

For a new story, use the `id` Storybook would derive from the `title`: lowercase, with each run of other characters replaced by a hyphen (`Components/Containers/Card` becomes `components-containers-card`). Never change the `id` of an existing story.

Two checks enforce this:

- `storybook/test/story-ids.test.ts` fails when a story or doc does not declare an `id`.
- `storybook/story-ids.txt` lists every published story and doc URL. CI regenerates it and fails when a URL in the committed list has disappeared.

To update the list, run the following from the top-level Gutenberg directory and commit the result. No Storybook build is needed.

```bash
npm run storybook:story-id-snapshot
```

New URLs are added without complaint. If a URL disappears because a story moved, restore its original `id` instead. If the removal is intended, for example when a component's stories are deleted, commit the regenerated list in the same pull request so the removed URLs are visible in the diff. Documentation for a deprecated component should usually be marked deprecated rather than deleted, so its URL keeps working.
31 changes: 2 additions & 29 deletions storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { StorybookConfig } from '@storybook/react-vite';
import dsTokenFallbacks from '@wordpress/theme/postcss-plugins/postcss-ds-token-fallbacks';
import dsTokenFallbacksJs from '@wordpress/theme/vite-plugins/vite-ds-token-fallbacks';
import { statusIndexer } from './status-indexer.js';
import { storyGlobs } from './story-globs.js';

/**
* @see https://storybook.js.org/docs/faq#how-do-i-fix-module-resolution-in-special-environments
Expand All @@ -19,39 +20,11 @@ function getAbsolutePath( packageName: string ) {

const { NODE_ENV = 'development' } = process.env;

const stories = [
'./stories/playground/**/*.story.@(jsx|tsx)',
'./stories/**/*.mdx',
'./stories/design-system/**/*.story.@(ts|tsx|mts|cts)',
'../packages/block-editor/src/**/stories/*.story.@(jsx|tsx|mdx)',
'../packages/editor/src/**/stories/*.story.@(jsx|tsx|mdx)',
'../packages/global-styles-ui/src/**/stories/*.story.@(jsx|tsx|mdx)',
'../packages/components/src/**/stories/*.story.@(jsx|tsx)',
'../packages/components/src/**/stories/*.mdx',
'../packages/icons/src/**/stories/*.story.@(tsx|mdx)',
'./stories/icons/**/*.story.@(ts|tsx|mts|cts)',
'../packages/dataviews/src/**/stories/*.story.@(tsx|mdx)',
'../packages/fields/src/**/stories/*.story.@(tsx|mdx)',
'../packages/image-cropper/src/**/stories/*.story.@(tsx|mdx)',
'../packages/media-editor/src/**/stories/*.story.@(tsx|mdx)',
'../packages/media-fields/src/**/stories/*.story.@(tsx|mdx)',
'../packages/theme/src/**/stories/*.mdx',
'../packages/theme/src/**/stories/*.story.@(tsx|mdx)',
'../packages/grid/src/**/stories/*.story.@(ts|tsx|mts|cts)',
'../packages/widget-primitives/src/**/stories/*.mdx',
'../packages/widget-primitives/src/**/stories/*.story.@(ts|tsx|mts|cts)',
'../packages/widget-dashboard/src/**/stories/*.mdx',
'../packages/widget-dashboard/src/**/stories/*.story.@(ts|tsx|mts|cts)',
'../packages/ui/src/**/stories/*.mdx',
'../packages/ui/src/**/stories/*.story.@(ts|tsx|mts|cts)',
'../packages/admin-ui/src/**/stories/*.story.@(ts|tsx|mts|cts)',
];

const config: StorybookConfig = {
core: {
disableTelemetry: true,
},
stories,
stories: storyGlobs,
// Tags stories with their `componentStatus` so the sidebar can show it.
experimental_indexers: ( existing = [] ) => [ statusIndexer, ...existing ],
staticDirs: [ './static' ],
Expand Down
1 change: 1 addition & 0 deletions storybook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
},
"scripts": {
"manifest-snapshot": "node ./scripts/generate-manifest-snapshot.mjs ./build/manifests/components.json",
"story-id-snapshot": "node ./scripts/generate-story-id-snapshot.mjs",
"postmanifest-snapshot": "prettier --write components-manifest.yml",
"storybook:build": "NODE_OPTIONS=--max-old-space-size=8192 storybook build -c . -o ./build && node ./scripts/sanitize-components-manifest.mjs ./build/manifests/components.json && node ./scripts/inject-legacy-docgen.mjs ./build/manifests/components.json",
"storybook:dev": "storybook dev -c . -p 50240",
Expand Down
87 changes: 87 additions & 0 deletions storybook/scripts/generate-story-id-snapshot.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/usr/bin/env node
/**
* Generates a snapshot of every Storybook story and doc URL.
*
* A story's URL is its ID, and every published link, bookmark, handbook page
* and deprecation notice points at one. The snapshot records the full set so
* that losing one has to be a deliberate act rather than a side effect of a
* rename nobody looked at twice.
*
* The snapshot is regenerated in CI, which fails if an ID it used to contain
* is gone. Removing a page on purpose still works: delete it and commit the
* regenerated snapshot in the same change, where the loss is visible in the
* diff. New IDs are written without complaint.
*
* The story index is built from source, so this needs no Storybook build.
*
* Usage: node generate-story-id-snapshot.mjs
*/
import { readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { buildIndex } from 'storybook/internal/core-server';

const CONFIG_DIR = path.join( import.meta.dirname, '..' );
const SNAPSHOT_PATH = path.join( CONFIG_DIR, 'story-ids.txt' );

const HEADER =
'# Auto-generated snapshot of every Storybook story and doc URL.\n' +
'# Do not edit by hand. Regenerate with `npm run storybook:story-id-snapshot`\n' +
'# and commit the result. See `storybook/README.md` for details.\n';
Comment thread
ciampo marked this conversation as resolved.

/**
* Reads the IDs out of the committed snapshot, ignoring its header.
*
* @return {Promise<string[]>} The IDs, or an empty list if there is no
* snapshot yet.
*/
async function readSnapshot() {
try {
const raw = await readFile( SNAPSHOT_PATH, 'utf8' );
return raw
.split( '\n' )
.map( ( line ) => line.trim() )
.filter( ( line ) => line && ! line.startsWith( '#' ) );
} catch ( error ) {
if (
/** @type {NodeJS.ErrnoException} */ ( error ).code !== 'ENOENT'
) {
throw error;
}
return [];
}
}

const { entries } = await buildIndex( { configDir: CONFIG_DIR } );
const current = Object.keys( entries ).sort();

if ( current.length === 0 ) {
console.error(
'No stories were indexed. The Storybook configuration may have changed.'
);
process.exit( 1 );
}

const committed = await readSnapshot();
const currentIds = new Set( current );
const committedIds = new Set( committed );
const removed = committed.filter( ( id ) => ! currentIds.has( id ) );
const added = current.filter( ( id ) => ! committedIds.has( id ) );

await writeFile( SNAPSHOT_PATH, HEADER + current.join( '\n' ) + '\n' );

if ( added.length > 0 ) {
console.log( `Added ${ added.length } story ID(s) to the snapshot.` );
}

if ( removed.length > 0 ) {
console.error(
`${ removed.length } story URL(s) no longer exist:\n\n` +
removed.map( ( id ) => ` - ${ id }` ).join( '\n' ) +
`\n\nEach of these is a published URL that will start 404ing. If a story moved\n` +
`in the sidebar, give it back its original \`id\` so the URL follows it.\n` +
`Documentation for a deprecated component is usually better marked deprecated\n` +
`than deleted. If the removal is intended, commit the regenerated\n` +
`storybook/story-ids.txt so the lost URLs show up in the diff.\n`
);
process.exitCode = 1;
}
2 changes: 1 addition & 1 deletion storybook/stories/design-system/contributing.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Meta, Markdown } from '@storybook/addon-docs/blocks';
import Contributing from './contributing.md?raw';

<Meta title="Design System/Contributing" />
<Meta title="Design System/Contributing" id="design-system-contributing" />

{ /* The content lives in contributing.md so people and agents can read the same text outside Storybook. */ }

Expand Down
2 changes: 1 addition & 1 deletion storybook/stories/design-system/introduction.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Meta } from '@storybook/addon-docs/blocks';

<Meta title="Design System/Introduction" />
<Meta title="Design System/Introduction" id="design-system-introduction" />

# WordPress Design System

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Meta } from '@storybook/addon-docs/blocks';

<Meta title="Design System/Patterns/Destructive Actions" />
<Meta title="Design System/Patterns/Destructive Actions" id="design-system-patterns-destructive-actions" />

# Destructive Actions

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Meta } from '@storybook/addon-docs/blocks';

<Meta title="Design System/Patterns/Error Messages" />
<Meta title="Design System/Patterns/Error Messages" id="design-system-patterns-error-messages" />

# Error Messages

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const LayoutCardSaveExample = () => {

const meta: Meta< typeof LayoutCardSaveExample > = {
title: 'Design System/Patterns/Save & Submit',
id: 'design-system-patterns-save-submit',
component: LayoutCardSaveExample,
parameters: {
controls: { disable: true },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ const siteLanguageOptions = [

const meta: Meta< ExampleApplicationArgs > = {
title: 'Design System/Theme/Theme Provider/Example Application',
id: 'design-system-theme-theme-provider-example-application',
component: ThemeProvider,
parameters: {
controls: { expanded: true },
Expand Down
1 change: 1 addition & 0 deletions storybook/stories/design-system/tokens/motion.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ function MotionDemo() {

const meta: Meta< typeof MotionDemo > = {
title: 'Design System/Tokens/Motion',
id: 'design-system-tokens-motion',
component: MotionDemo,
parameters: {
controls: { hideNoControlsWarning: true },
Expand Down
2 changes: 2 additions & 0 deletions storybook/stories/icons/library.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ function nameToSlug( name: string ): string {

const meta: Meta = {
component: Icon,
// Shares its `id` and `title` with `packages/icons/src/icon/stories/index.story.tsx`
// so both files merge into one entry. Change them together.
id: 'icons-icon',
title: 'Design System/Icons/Icon',
Comment on lines +53 to 56

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem like a good pattern. Normally if we wanted to split the library story into its own file, we would just put the code (e.g. just the render function) in a separate file and import it in the main story file. Having two duplicated meta objects like this is not recommended.

tags: [ '!autodocs' ],
Expand Down
2 changes: 1 addition & 1 deletion storybook/stories/introduction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Markdown, Meta } from '@storybook/addon-docs/blocks';
import { JumpToIcon } from '@storybook/icons';
import { InlineIcon } from './docs/inline-icon';

<Meta title="Introduction" name="page" />
<Meta title="Introduction" id="introduction" name="page" />

# Introduction

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { WithWpCompatOverlaySlot } from './with-wp-compat-overlay-slot';

export default {
title: 'Playground/Debug fixtures/Draggable cross-document fallback',
id: 'playground-debug-fixtures-draggable-cross-document-fallback',
component: Draggable,
decorators: [ WithWpCompatOverlaySlot ],
};
Expand Down
1 change: 1 addition & 0 deletions storybook/stories/playground/index.story.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ registerCoreBlocks();

export default {
title: 'Playground/Block Editor',
id: 'playground-block-editor',
parameters: {
sourceLink: 'storybook/stories/playground',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Popover } from '@wordpress/ui';

export default {
title: 'Playground/Debug fixtures/Popover with SlotFill',
id: 'playground-debug-fixtures-popover-with-slotfill',
component: Popover.Root,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ function UiPopoverFixture() {
// of scope.
export default {
title: 'Playground/Debug fixtures/WP Compat Overlay Slot',
id: 'playground-debug-fixtures-wp-compat-overlay-slot',
decorators: [ WithWpCompatOverlaySlot ],
};

Expand Down
Loading
Loading