Skip to content

FHL: fluent v9 and headless docsites upgrade - #35

Draft
mainframev wants to merge 52 commits into
masterfrom
feat/docsite-fumadocs
Draft

mainframev wants to merge 52 commits into
masterfrom
feat/docsite-fumadocs

Conversation

@mainframev

Copy link
Copy Markdown
Owner

No description provided.

…0-2 partial)

Adds a React Router + Vite + Fumadocs application that renders documentation from
the existing Storybook story modules, with no duplication of example code.

The site imports story modules directly and reads meta/parameters at runtime, applying
the same two build rules Storybook applies: `.md` as a string module, and the existing
`@fluentui/babel-preset-storybook-full-source` plugin for standalone per-example source.
Both description locations (sibling `.md` files and inline `parameters`) therefore resolve
identically in both hosts.

Published additively at /docs/react and /docs/headless. No existing address changes and
no redirects; the Storybook sites are untouched.

- widen the export-to-sandbox addon's public API (`scaffold`, `openCodeSandbox`,
  `openStackblitz`, `getDependencies`, `Data`) so the export works outside Storybook,
  and take an injected `targetDocument` instead of the ambient one (repo rule #3)
- bump root vite 6.4.2 -> 7.3.6; fumadocs-mdx's generated content index uses the
  `import.meta.glob` `base` option, which is Vite 7+ only despite its declared peer range.
  The web-components and chart-web-components Storybooks are verified on Vite 7.
- build-time docgen manifest, with slot-shorthand and native-props detection ported as
  pure functions so the rendered page and machine-readable output cannot disagree
- Tailwind preflight scoped away from example previews, and Griffel styles injected
  during prerender so examples are styled before hydration
- record authored example order at build time; ES module namespace objects sort their
  keys, which had promoted the wrong example to primary
- guard that fails the build, naming the module, on unsupported story capabilities
- accessibility gate (axe-core via Playwright) over representative pages

Planning artifacts for this change live in openspec/changes/migrate-docsite-to-fumadocs.
Adds 53 headless component pages and the one-shot generator that produced them.
The generator recurses, so grouped entry points (Concepts/Positioning, Tags/Tag)
are picked up rather than skipped, and it never overwrites an existing page.

- alias the headless library's subpath exports so Rollup can resolve them
- load the headless design tokens in the /docs/headless route only. Storybook
  imports them globally from its preview; without them the CSS Module rules
  applied but every token reference fell back, so border radii, gaps and padding
  were wrong. Accordion and Card now match Storybook on all compared computed
  properties.
- suppress MODULE_LEVEL_DIRECTIVE/SOURCEMAP_ERROR warnings. Fluent sources carry
  'use client', which Rollup warns about once per module and, with sourcemaps
  disabled, cannot locate — producing 608 lines of noise that masked a genuine
  resolve failure. Every other warning code still surfaces.
- return a passthrough sourcemap from the story-order plugin instead of null

Build: 58 routes, 14.8s cold (6 routes was 11.8s), so prerender scaling is
startup-dominated rather than per-page.
…e rule

`app/lib/source.ts` was never committed. The repo-wide .gitignore has a bare `lib`
rule for build output, which silently excluded the app's `app/lib/` source directory.
The original worktree still built because the file existed locally as an untracked
file; a fresh worktree exposed the gap.

Moved to `app/source.ts` rather than adding a .gitignore negation, so the file cannot
be swallowed again by a rule that is correct for the rest of the monorepo.

Also declare the Nx build dependencies the Vite config needs at config-load time
(the babel preset and the sandbox addon) plus the component packages the app imports,
so the site builds from a clean checkout instead of relying on previously built output.
…ok does

Generates Vite aliases from the workspace TypeScript path mappings, the Vite
equivalent of the TsconfigPathsPlugin that Storybook already uses.

Previously the site resolved Fluent packages through node, so it only built where
`lib/` output happened to exist from earlier builds. A clean checkout failed with
"Failed to resolve entry for package @fluentui/react-components", and a stale `lib/`
could silently win over the source being edited.

This also removes the need to hand-maintain one alias per `*-stories` package, which
would not have scaled across the remaining trees.

Verified from a clean state (no dist, no .source, no docgen output, nx cache reset):
`nx run public-docsite-v9-new:build` succeeds in 67s, prerendering 58 routes across
10 dependency tasks. type-check, 23 unit tests and 4 accessibility checks all pass.
Extends the page generator to every component package that ships stories, producing
91 v9 pages. 149 routes now build and prerender clean.

Excluded, per the proposal's non-goals: the migration shims (they embed v8/v0
playgrounds that would pull those libraries into the bundle), the headless preview
(its own tree), and workbench-internal packages.

Two resolution fixes were needed once the full corpus was in play:

- alias the three deprecated packages (react-alert, react-infobutton,
  react-virtualizer). They have no tsconfig path entry, but
  `@fluentui/react-components/unstable` still re-exports them, so any page importing
  the unstable barrel failed to resolve.
- add `tabster` and `keyborg` to ssr.noExternal. Both are CommonJS; left external,
  Node's ESM loader rejects their named imports during prerendering.

Spot-checked Button, Menu, DataGrid, Dialog, Tree and Field: 200s, examples render
(8-28 per page), no error boundaries, no console errors.

Known gap recorded as task 4.9: the Fumadocs content index is generated with eager
globs, so all examples land in one 10M chunk instead of being split per page.
Component pages now read `meta.decorators` from the imported story module and apply
them around each example, instead of leaving examples unwrapped.

Taking the decorators from the module keeps them on the same source of truth as the
examples themselves (design D1). The alternative was re-declaring each layout wrapper
on its docs page, which would silently drift from Storybook whenever a decorator changed.

Verified against the computed styles: Field constrains to 400px, Avatar renders flex
with a 5px gap, Label uses space-evenly. Covers the 10 v9 decorator pages plus the
headless SwatchPicker.
Every page previously modulepreloaded a single 10M chunk, because the Fumadocs content
index was generated with eager globs — so opening one component page downloaded the
examples for all 149 pages.

Collections are now async, so only frontmatter stays eager and each page's compiled body
is a separate chunk. Rendering that body suspends, so the server entry uses `prerender`
from react-dom/static, which waits for suspended boundaries instead of emitting fallbacks.
Prerendered HTML therefore still contains full content and Griffel styles, verified with
JavaScript disabled.

  largest chunk   10M  -> 476K
  chunks           22  -> 464
  per-page js/css  ~10M -> ~0.7M

Enabling this surfaced a content bug that had been present since the first build: sibling
`*Description.md` imports were compiled into MDX components by fumadocs-mdx instead of read
as strings, so component pages rendered `function MDXContent(props = {}) {...}` where their
prose should be. It went unnoticed because the one hand-written page took its visible
description from frontmatter. The raw-string plugin now redirects those imports to a
base64-encoded virtual module id, which the MDX file filter cannot match.

Verified: 149 routes, no hydration errors, examples render, no-JS styling intact,
type-check clean, 23 unit and 4 accessibility tests passing.
… defects

Adds a `test-content` target that sweeps all prerendered pages for symptoms rather
than relying on spot-checks: compiled-source leaks, error boundaries, missing API
data, application-error placeholders and unrendered `[object Object]`.

Generated pages make "verify one page, assume the rest" unsafe. The previous commit
fixed descriptions that had rendered as JavaScript source across 144 pages; that
survived as long as it did because only the one hand-written page was being checked.

Running it immediately found a second defect on 13 pages, recorded as task 4.12:
slot names leak onto the rendered element as DOM attributes (`icon="[object Object]"`
on Avatar, `root=`/`avatar=` on headless AvatarGroup). The slot content still renders
correctly as a child, so slots resolve but are not stripped from root props, and it
persists after hydration. Needs a Storybook comparison to establish whether the
source-aliasing introduced it before anything is changed.
Rendering AvatarGroup in both hosts shows Storybook emits the identical attributes
(role, id, root, avatar, class, aria-label), so the slot names appearing as
`[object Object]` attributes are upstream Fluent behaviour that this site reproduces
faithfully — not a defect introduced by resolving packages to source.

The audit now only flags `[object Object]` in page text, where it does indicate an
unresolved expression. Flagging the attribute form would leave the gate permanently
red across 13 pages, which is how a check stops being read.

All 148 prerendered pages now pass the audit.
Adds a one-shot codemod converting Storybook MDX docs to Fumadocs pages, and the 54
pages it produced. The site now builds 203 routes.

The codemod strips the `<Meta>` marker and docs-block imports, derives the frontmatter
title and file path from the meta title, and removes the leading heading that would
otherwise duplicate the frontmatter title as a second h1.

Two rewrites make the moved pages work:

- relative imports (helper modules, images) become `@repo/*` alias imports, so pages keep
  importing the original files rather than copies that would drift
- Storybook id links (`/docs/a-b-c--docs`) become real paths, using a map built from both
  conceptual pages and component pages. 71 unresolved targets dropped to 7, and those
  remaining are external icon docs, deliberately excluded shims, or stale links in the
  original content.

64 pages are excluded: the per-component v8/v0 migration guides render legacy components
and pull `@fluentui/react` and `@fluentui/react-northstar` into the bundle. That happens
transitively through story modules, so inspecting page text does not reveal it — it was
isolated by bisecting the content tree. Excluded by path for the same reason as the
migration shim packages, and still available on the Storybook docsite.

All 202 prerendered pages pass the content audit; type-check, 23 unit tests and 4
accessibility checks pass.
…t output

Adds a `test-links` target that resolves page links against the routes the site actually
serves and asset links against files on disk, checking the built output rather than source.

Links are rewritten by a codemod across generated pages, so a mistake there is systematic
rather than a typo — and this caught one immediately: the rewritten paths included the
router basename, so every migrated page linked to `/docs/docs/react/...`. Content links
are resolved against the basename and must not repeat it.

Links into the deliberately excluded v8/v0 migration subtree now point at the Storybook
docsite, which still hosts that content, instead of dangling.

20,759 links across 202 pages resolve.
…chitecture

Pages were flat at the root and in alphabetical order, losing the grouping Storybook
presents. Two separate things were missing.

Position: a page's path now comes from its story `meta.title`, not its directory, because
the title is what carries the hierarchy (`Components/Badge/CounterBadge`,
`Compat Components/Calendar`, `Utilities/...`). Deriving paths from directories flattened
every component to the root. `Components/Button/Button` collapses to `components/button`,
matching how Storybook presents it.

Order: `scripts/generate-nav.mjs` transcribes `options.storySort.order` — which exists only
as a hand-maintained array in the Storybook preview config and is not derivable from titles
or file names — into `meta.json` files, using Fumadocs' `...` rest so unlisted pages still
appear rather than disappearing.

Also fixes slug generation, which mangled spaces and acronyms: `Compat Components` produced
a directory with a space in it, and `APIs` became `ap-is`.

Sidebar now reads Concepts, Theme, Components, ... with components nested under their group.
202 routes; content audit and 11,297 internal links pass; accessibility page paths updated
to match the new structure.
The site is served as static files, so the `api/search` route from the Fumadocs setup
guide cannot exist. The dialog is configured as a static client that fetches a prebuilt
index instead.

The index is built by reading the content directory directly rather than through the
Fumadocs loader, which is built on `import.meta.glob` and only resolves inside Vite —
so it cannot be driven from a plain Node build step. Fenced code, JSX and import
statements are stripped before indexing, otherwise every component page matches on
framework noise rather than its prose.

Verified in a browser: 200 pages indexed, searching returns results, and the only
search-related network request is for the static index itself.

The index is 3.5MB and fetched lazily when the dialog opens. Worth revisiting if that
proves heavy in practice.
Adds navigation links to the charts Storybook, the v8/v0 migration guides and the
component workbench itself, all marked external. That content is deliberately not
migrated (proposal Non-goals), so linking out keeps it reachable rather than simply
absent from the site.

Layout tabs for switching between the two documentation trees are attempted here but do
not render: passing `tabs` to `DocsLayout` type-checks yet produces no markup, most
likely because Fumadocs derives tabs from a single page tree rather than accepting
arbitrary cross-tree urls, and each tree is its own loader. Left open as task 6.2 —
readers can still move between trees via the outbound links.
Generates an llms.txt summary and a plain-text rendering for each of the 201 pages,
derived from the same content the site renders rather than from a built artifact.

Example source is produced by the same babel plugin that feeds the source panels, so the
code published here is byte-identical to what a reader sees on the page. The previous
generator could not offer that: it drove a headless browser over a built Storybook and
round-tripped MDX through HTML and Turndown, losing components and frontmatter.

Prop types come from the shared docgen manifest, so the abbreviated slot forms match the
rendered table — Button's `icon` publishes as `Slot<"span">` rather than the expanded
`WithSlotShorthandValue<...>` the old extractor emitted. That divergence between the two
published surfaces was one of the motivations for the manifest.

Two formatting fixes the first output made obvious: `|` in union types is escaped, since
it otherwise splits Markdown table rows, and very long generated unions are truncated —
`focusgroup` alone expands to several thousand members and buried the rest of the table.
…d have removed

Removing it predates the decision that the workbench stays published and deprecated
rather than retired, and conflicts with the requirement that it remain functional. Its
output is consumed by the workbench's own copy-as-markdown button and lives at a
different address from the new site's, so the new generator does not replace it.
…layout tabs

Passing `tabs` to DocsLayout on its own rendered nothing. Each tree is served by its own
`loader()`, so a tab cannot be bound to a page tree; Fumadocs needs `urls` to resolve the
active tab in that case, and `tabMode="top"` to place them.

Verified in a browser: both tabs render, clicking switches to /docs/headless, and that
tree's sidebar loads.
Adds an `audit:a11y` sweep covering all prerendered pages, grouped by rule. It is a sweep
rather than a gate — too slow to run per commit — complementing the three-page chrome check
that does gate.

It reports 13 serious/critical rules, all inside the rendered examples rather than the docs
chrome: missing form labels, unnamed progressbars and buttons, scrollable regions without
keyboard access.

Provenance was verified rather than assumed. Headless ProgressBar reports
`aria-progressbar-name` identically in Storybook and on the new site, so these are
pre-existing defects in the examples that the migration surfaces but does not introduce.
Recorded as task 7.0 for the component teams.
Adds a README covering the model that matters most — examples are never duplicated, so
changing one means changing its story — along with the build gates, the one-shot page
generators, the deliberate exclusions, and the handful of non-obvious decisions a
maintainer would otherwise have to rediscover (source resolution, async collections,
the prerender entry, Tailwind scoping).

Also records in AGENTS.md that both sites are published and that examples are edited in
their story rather than in the docs site.

Confirms no existing published address changed: the deploy workflow still copies the four
existing trees unchanged and the new site only adds `_pages/docs`.
…site-wide

No page rendered a props table or API disclosure cards. Two causes compounded: the docgen
manifest still listed only Button, left over from the Phase 1 walking skeleton, and the
page generator never emitted the `docgen` prop — so regenerating content overwrote the one
hand-written page that had it.

Components are now discovered from each package's `library/src/components/<Name>/<Name>.tsx`
rather than a hand-maintained list, which is what went stale. They are parsed one package at
a time: handing react-docgen-typescript all 351 component files at once returns entries with
zero props, which is how the first attempt at this produced 214 empty components rather than
an obvious failure.

214 components documented, 122 pages now carry a props table.

The content audit gains a rule for this: a page documenting a component that exists in the
manifest must render its table. The existing rule only caught a *failed* lookup, and passing
no `docgen` prop rendered nothing at all — leaving no text for an audit to match.
…rsion to the docs

Exported sandboxes pinned `@types/react ^17` against a React `^18` runtime, while both
documentation hosts render React 19. A reader opening an example got a project that
type-checked against a different React than the example was written for.

The scaffold's dev dependencies and both hosts' required dependencies now follow the
version actually rendered.
Assembled the artifact exactly as the workflow does and served it statically: the new
/docs tree renders with examples, props tables, llms.txt and search index, and the
existing /headless Storybook still serves alongside it. Only local dev and preview had
been exercised before.
…ied by parity check

Adds an `audit:parity` check that compares every component Storybook indexes against the
examples the new site publishes, reading Storybook's own index.json rather than scraping.
Sampling pages by hand cannot answer "did the migration lose anything"; a page can render,
pass every other gate, and still be missing examples.

It immediately found 10 story entry points under `apps/public-docsite-v9/src` that the
generator never scanned — focus-management and theme utilities, positioning concepts,
accessibility scenarios. They appear in Storybook's sidebar like any other page and had
been silently dropped, because the generator only looked at component packages.

Nine are now generated. `useUncontrolledFocus` is excluded because its example renders
v8's FocusTrapZone, and entry points whose examples import v8/v0 are now skipped generically
rather than by name.

Parity now: headless 53/53 with every story present; react 99/123, the 24 absent being the
23 migration shims plus that one page, all excluded by design.

210 routes; content audit and 11,960 internal links pass.
… own preview

Migrated MDX pages embedded examples with `FluentCanvas`, the Storybook addon's preview
wrapper. That pulled the addon into the documentation bundle and bypassed this site's
preview chrome, so those examples had no theming, direction or error boundary. They now
use `<StoryPreview>`, and the addon no longer appears in the output at all.

`<FluentStory id="…">` references a story by id, so there is no component to hand
`<StoryPreview>`; it becomes a link to that example's own page and anchor, as do
Storybook's `?path=` deep links.

Doing this exposed three heading defects the codemod had left behind: `<h1 class="sbdocs-title">`
carried over from Storybook, `<Title>` usage surviving after its import was stripped —
undefined at hydration, failing the page with React error microsoft#419 — and mid-document `#`
headings producing a second h1.

The content audit gains rules for both, scoped to the page's own headings so components
that legitimately render an h1 (AccordionHeader, Image's examples, the theme designer) are
not flagged. Narrowing that scope took three attempts; each earlier version flagged example
markup as a page defect.
Adds a controls panel for stories that declare `argTypes`, merging component-level and
story-level definitions the way Storybook does, honouring `table.disable` and
`control: false`, and inferring a control from the declared type where none is given.
Story `args` defaults now apply whether or not controls are shown.

Hand-rolled rather than adopting Fumadocs' `defineStory`, which would mean re-authoring
every example — the duplication this whole approach exists to avoid.

Object-valued options are omitted: a select can only carry a string, and Avatar's `badge`
option rendered as "[object Object]" in the list.

Two behaviours are inherited from how stories are written rather than from this panel: a
story that spreads props before its own attributes cannot be varied, matching Storybook,
and stories declaring `args` without `argTypes` get no controls. Deriving those from the
docgen manifest is recorded as 4.4a.
Most stories declare `args` without `argTypes`, so they rendered a fixed example with no way
to vary it — CounterBadge showed `count: 5` and nothing else. Storybook infers controls from
the same type information the docgen manifest already holds, so it is used when a story
declares none. Authored `argTypes` still win, so an explicit choice to expose or hide a
control is never overridden.

Slots, refs, handlers and unions longer than twelve options are left out; offering every
prop produces a panel nobody can use. CounterBadge now has nine working controls.

This surfaced a bug that predates it: the primary example's call site never received
`decorators`, `metaArgTypes` or `docgen` — only the secondary examples did. The most
prominent example on every page had been rendering without its decorator, and would have
had no controls either. Both call sites now pass the same props.

Two audit rules added: a page whose examples list headings but render none (which is how
TimePicker's hydration failure presents), and object-valued select options, which rendered
as "[object Object]" on Avatar.

TimePicker's page is recorded as task 4.14 — it fails at hydration and shows no examples.
…ts as values

Derived controls read defaults from the docgen manifest, where every value is a string and
a prop with no default is recorded as the literal "undefined". Passing that through handed
the component the *string* "undefined", which is not the same as leaving the prop unset.

TimePicker's `hourCycle` then reached `toLocaleTimeString`, which Node's ICU rejects for a
string value — the value `undefined` is accepted fine — throwing during prerender and taking
the page's content boundary with it. The page listed seven example headings and rendered
none of them.

It now renders 6 examples and 30 prop rows, prerendered rather than falling back to client
rendering.

The stack pointed into react-timepicker-compat and a fix there looked obvious. Reproducing
the throw first showed `hourCycle: undefined` does not throw at all, which is what located
the real cause — otherwise a published component would have been changed to accommodate a
bug introduced here.
… see

The link checker matched `href="(/docs/[^"#?]*)"`. A character class excluding `#` does not
match an href containing one and then stop at it — it fails to match the href at all, so every
anchored link was skipped silently. "All internal links resolve" was true only of the links
being looked at. Widening the pattern surfaced 21 broken links, and the checker now verifies
fragments too, since an anchor pointing at nothing leaves the reader at the top of the page
with no indication anything is wrong.

The defects behind them:

- Component routes were registered as `/docs/<tree>/...` while page routes used `/<tree>/...`.
  The router adds the basename itself, so those links resolved to `/docs/docs/...`.
- A page's path was derived from `meta.title` when generating the page but from the story's
  directory when generating links to it, so `Components/Accordion` was linked at
  `/react/accordion` and generated at `/react/components/accordion`. The anchor rule had two
  further copies. All four now share `scripts/story-route.mjs`.
- `nameToHash` lowercased the export name, producing `motioncustom` where Storybook uses
  `motion-custom`, despite a comment claiming the schemes matched.
- 17 links kept Storybook's `?path=` query form and resolved against the current page.
- Storybook resolved images with `require()` inside JSX. An ES module has no `require`, so
  three pages threw during prerender and fell back to client rendering.
- `Concepts/Developer/Positioning Components` had no page at all; parity had been reporting
  98/123 against a recorded 99/123. It now renders its 16 examples.

12,041 links across 209 pages resolve, paths and fragments alike. The shared path and anchor
derivation is covered by tests because the whole site's URLs depend on it.
…e repository

Twelve react-card stories reference their images as raw.githubusercontent.com URLs on the
default branch rather than importing them. Keeping that is the right call: a sandbox exported
from the page cannot resolve a repository-relative import, so making the paths local would
break the CodeSandbox and StackBlitz buttons, and pinning a commit SHA would freeze the images
and add a maintenance task for no present benefit. All of them resolve today.

The weakness was never the URL, it was that nothing connected it to the file it names. Moving
or renaming one of those assets would break the published image silently, on the Storybook
docsite as well. The content audit now resolves each of those URLs against the working tree,
so the failure happens in the commit that moves the file rather than in production. It needs
no network, and was verified by moving an asset and confirming two pages fail.
`tabMode="top"` renders the switcher into the same grid area as the page itself
(`[grid-area:main]`). Neither element sets `align-self`, so both stretched to fill the row and
the switcher became a 23,936px bar with an opaque background and `z-10`, painted over the
article. All 209 pages showed nothing but chrome.

The content was in the DOM and correct throughout, which is why nothing caught it: links
resolved, content audits passed, axe was satisfied, parity matched. Every gate reads the DOM,
and presence was never the property that mattered.

`yarn test:render` now checks that a page's heading is the element actually hit at its own
coordinates, and that a page-appropriate amount of text is visible. Removing the fix fails all
four pages and names the covering element.

The behaviour is unchanged in fumadocs-ui 16.15.1, so the rule stays until it is fixed upstream.
…e structure

Found by looking at the rendered pages rather than the DOM.

The headless stories' token stylesheet carries global element rules alongside its custom
properties — `body`, `a`, `code`, `button`. Under Storybook those are correct, because the story
is the page. Loaded by the site they styled the site: `body` set its own background and colour
and, being unlayered, outranked the theme in `@layer base`, so headless pages rendered
white-on-black whatever the toggle said. A Vite plugin scopes those rules to the preview
subtree, which is what stands in for Storybook's canvas here, and leaves the properties global.

The source panel highlighted with `github-light` only, so dark mode put dark text on a dark
page. It now emits both palettes and follows the theme.

Alongside those:

- Search results led to `/docs/docs/...`; the index stored `/docs`-prefixed URLs while the
  router already mounts at `/docs`. The link checker cannot see this because it reads rendered
  anchors, not the index.
- `Concepts/Introduction` becomes the `/react` landing page, which had been a placeholder saying
  "Documentation for Fluent UI React v9." twice. The tree had two introductions and the useful
  one was a level down.
- Removed the Component Selector page, whose only story renders `<div>Hello world</div>`.
- The theme toggle shared a footer row with the Charts link instead of stretching the sidebar
  width on its own line, and the Storybook link is gone from the sidebar.
Descriptions are authored as Markdown files and imported as strings, which is correct and
matches Storybook's `asset/source` rule: stories combine them before they become
`parameters.docs.description.component`. Storybook then renders that string as Markdown. This
site printed it verbatim, so headings, lists and fenced code appeared as literal `##`, `-` and
backticks on 127 of 209 pages. The rule mirrored Storybook's input and not its output.

Rendering happens where the string is consumed rather than at build time, because the `.md`
imports have to stay strings for the stories to be able to join them.

Fenced code goes through the site's own highlighter, so a snippet in a description looks like
the same snippet in the source panel. The highlighter moves to its own module and gains the
languages the descriptions use beyond `tsx` — `ts`, `jsx`, `js` and `css`.

`buildMarkdown` is deliberately untouched: Copy as Markdown wants the raw string. Raw HTML is
passed through rather than escaped, which is what Storybook does with these same strings, and
they are files in this repository.

The content audit now fails on Markdown left unrendered in prose. Verified by reverting the
render and confirming it reports the affected pages.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant