I’m increasingly convinced that for developer-first tools, a really good docs experience is a durable, non-trivial advantage.
Part of this thesis is that Really Good Docs Experiences, in addition to having great information architecture and strong writing/prose, should be thought of less as ancillary content repositories that can be farmed out to whatever your helpdesk software is and more as important web-apps in their own right.
Every now and then, when a docs site does something cool I try to figure out how they do it. Take shadcn/ui, for instance — how are all of these previews being generated based on Typescript? It’d be one thing if the components were HTML — you can just throw them into an iFrame — but clearly there’s something more complicated going on here.
Blessedly, these docs are open source, so I can find out exactly how. Let’s take that example Notifications
card for example:
That page is being rendered in MDX; there’s a generic ComponentPreview
component rendering it:
<ComponentPreview
name="card-demo"
description="A card showing notifications settings."
/>
It looks like that ComponentPreview component is just proxying out to some big __registry__
:
import { Icons } from "@/components/icons";
// ...
const Preview = React.useMemo(() => {
const Component = Index[config.style][name]?.component;
if (!Component) {
return (
<p className="text-sm text-muted-foreground">
Component{" "}
<code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm">
{name}
</code>{" "}
not found in registry.
</p>
);
}
return <Component />;
}, [name, config.style]);
The component in the registry is right here, and that’s the exact source code that we see. But how is the registry aware of its constituents?
Because it’s autogenerated! There’s a scripts/build-registry.ts file. This build file is gnarly (understandably so), but at a high level it spits out this massive export file:
"card-demo": {
name: "card-demo",
type: "components:example",
registryDependencies: ["card","button","switch"],
component: React.lazy(() => import("@/registry/default/example/card-demo")),
source: "",
files: ["registry/default/example/card-demo.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
And it finds the things to populate in that registry via a manually-enumerated list of potential components:
{
name: "card-demo",
type: "components:example",
registryDependencies: ["card", "button", "switch"],
files: ["example/card-demo.tsx"],
},
So, in sum, walking our way back to the final artifact:
shadcn
has a big, manually-created list of example files (and associated dependencies/metadata.)- A build-time script analyzes that list and autogenerates both an augmented code snippet for that example file and a big index that allows the site to import that snippet live.
- A previewer component pulls in that autogenned snippet and its raw-source equivalent.
- That previewer component is declared by an
mdx
file.
Nothing magical; nothing complex. But certainly bespoke.