Making your own documentation template with MDX

As with many things in development, every framework is great until you need it to do something custom. Storybook, unfortunately, is not immune to this, especially if you’re working in a non-React environment.

The documentation contains some simple examples for reorganising the default docs page for each component’s stories file. Since we’re working in a Vue Storybook environment and we don’t have access out-of-the-box to JSX, the first example isn’t much use to us. Fortunately, there’s a second example that’s more pertinent.

Enter MDX

You can also use MDX to generate the documentation template. This is useful in non-React projects where JSX-handling is not configured.

Storybook Documentation

That’s exactly what we want! How do we implement that?

1import { Meta, Title, Primary, Controls, Stories } from '@storybook/blocks';
2
3<Meta isTemplate />
4
5<Title />
6
7# Default implementation
8
9<Primary />
10
11## Inputs
12
13The component accepts the following inputs (props):
14
15<Controls />
16
17---
18
19## Additional variations
20
21Listed below are additional variations of the component.
22
23<Stories />

Unfortunately, that’s your lot. Great if all you need are a few formatting tweaks (and don’t mind losing some functionality from the standard Docs template), less great if you need to add new sections or do other things.

Our Challenge

In our attempts to wrangle the documentation template, we had two main goals:

  1. Maintain the exact same template as used by default.
  2. Generate a new block powered by information from the stories file that’s shown inside the template.

If you look at the JSX code that generates the default documentation template, you’ll notice a few things that our MDX “replacement” is missing:

export const DocsPage: FC = () => {
const resolvedOf = useOf('meta', ['meta']);
const { stories } = resolvedOf.csfFile;
const isSingleStory = Object.keys(stories).length === 1;
return (
<>
<Title />
<Subtitle />
<Description of="meta" />
{isSingleStory ? <Description of="story" /> : null}
<Primary />
<Controls />
{isSingleStory ? null : <Stories />}
</>
);
};

By using the MDX template, in the event of a single story in our stories file, we would lose the additional <Description> block as well as rendering a moot <Stories> block that’s made redundant by the <Primary> block that’s shown earlier.

Fortunately, more recent versions of MDX are ideologically closer to JSX than standard Markdown and with a few quirks, we can write our own JSX blocks without needing to wire up JSX support for our project. Although bear in mind that you will be writing React regardless of your preferred framework… but hey, it’s all JavaScript.

The ‘export’ caveat

Very quickly in MDX, we hit a wall: you cannot declare variables in MDX. No constants, no lets, no functions, no components. All will result in an error:

There is, however, a loophole. MDX treats import and export statements differently and allows variables to be created in the document scope when using either of these keywords. Which means that this is valid code:

export const foo = "bar";
{ foo }

Rendering a block conditionally

Notice from our JSX basis file that we need to (1) work out if our stories file has multiple stories; and (2) render some blocks conditionally based on that fact.

Here’s what we could do:

export const checkIsSingleStory = () => {
const resolvedOf = useOf('meta', ['meta']);
const { stories } = resolvedOf.csfFile;
return Object.keys(stories).length === 1;
}
export const StoryDescription = () => {
const isSingleStory = checkIsSingleStory();
return (isSingleStory ? <Description of="story" /> : null)
}
export const DocumentationStories = () => {
const isSingleStory = checkIsSingleStory();
return (isSingleStory ? null : <Stories />)
}
<StoryDescription />
<DocumentationStories />

Since we can declare JSX functions inside MDX with export, it allows us to reuse the code from Storybook’s default DocsPage to check if the stories file has multiple stories and react accordingly.

Note that the functions are returning JSX code inside ( ) parentheses.

Creating a brand new block

Okay, that’s the default template reconstructed, now onto our new block.

Our component library can be configured via a global JS configuration file for some components, and we wanted to allow components to provide example source code for setting this up.

If this code is detected, we want our docs page to generate a <Source> block with a short introduction. Here’s how we might do that:

1import { Meta, Title, Primary, Controls, Stories, Subtitle, Description, useOf, DocsContext, Story, Source } from '@storybook/blocks';
2
3export const EvoMarkConfig = () => {
4 const resolvedOf = useOf('meta', ['meta']);
5 const { evo } = resolvedOf.csfFile.meta.parameters?.docs ?? {};
6
7 return evo?.config && (<>
8 <h3>evoMark Global Config</h3>
9 <p>You can set props globally for this component by using a <code>evomark.config.js</code> configuration file in your project root.</p>
10 <p>An example of this is shown below:</p>
11 <Source language="js" code={`export default = ${evo.config}`} format="dedent" />
12 </>)
13}
14
15<EvoMarkConfig />

Now if we go to one of our stories files, we can utilise this new block via the config object:

1// some-component.stories.js
2
3export default {
4 title: "Some Component",
5 parameters: {
6 docs: {
7 evo: {
8 config: `{
9 someComponent: {
10 someProp: "some-value"
11 }
12 }`
13 }
14 }
15 },
16}

Note that evo.config above is a text string inside template quotes.

Setting our documentation template as default

The final step is telling our Storybook to use our new documentation template. For this, we need to load up our .storybook/preview.js file and tweak the exported default config object:

1import DocumentationTemplate from "./DocumentationTemplate.mdx";
2
3export default {
4 parameters: {
5 docs: {
6 page: DocumentationTemplate
7 },
8 }
9}

Conclusion

And there we go, our very own documentation template with new blocks relying on stories data and the ability to reorder and re-format to our heart’s content.

For more information on what blocks are available by default inside your MDX template, see https://storybook.js.org/docs/writing-docs/doc-blocks#available-blocks.

And stay tuned for the next part in this series coming soon.

Get in Touch

Get in touch with us today 01202 790300

monday 09:00 - 17:00
tuesday 09:00 - 17:00
wednesday 09:00 - 17:00
thursday 09:00 - 17:00
friday 09:00 - 16:00
saturday Closed
sunday Closed