Shortcodes vs MDX

swyx - Mar 26 '21 - - Dev Community

There are two prevalent solutions for injecting dynamic content into markdown: Shortcodes and MDX. I think most people should use shortcodes, but there are also valid cases for picking MDX. Here's my breakdown.

There's some confusion between these content formats (Gatsby's docs for shortcodes literally just make them synonyms for MDX) so I figure it is worth setting some definitons upfront.

Defining Shortcodes

The earliest instance I can find of Shortcodes is in WordPress. The whole goal is that you can continue writing in standard plain text, but insert special components just by using a special syntax that wouldn't show up in normal writing:

  • Wordpress: [gallery id="123" size="medium"]
  • Dev.to: { % twitter 834439977220112384 % } (remove spaces)
  • Elder.js: {{shortcode foo=""}} optional inner content {{/shortcode}}

These are mostly used for inserting anything from Tweet embeds to YouTube videos to GitHub gists to image galleries. But there's really no limit to what you can define for your shortcodes, to cover anything from simple Tip and Alert displays to interactive Quiz components!

Shortcodes are the plaintext analog of Web Components - where a <custom-element> might extend HTML, shortcodes extend plaintext (typically Markdown). You could write your shortcodes in React or Vue or Web Components - it doesn't matter, because they are inserted after the fact.

Defining MDX

MDX, introduced in 2018, inverts this model of content vs code. It renders your markdown as a React component (or Svelte component, with MDsveX), so it is very natural to add more React components inline:

import Video from '../components/video'

# My blog post

Here's a video:
<Video width={300} src="video.mp4" />
Enter fullscreen mode Exit fullscreen mode

MDsveX goes a little further, offering layouts, frontmatter, syntax highlighting, styling, and state. But MDX is by far more popular because of React. In my 2020 survey of the React ecosystem, all blogging documentation tools now offer MDX support by default.

Comparing the Two

Superficially, both Shortcodes and MDX do the same job, which is why it feels a little silly to write up a blogpost like this. But they do have important differences, that I myself did not appreciate until Nick Reese converted me when I was arguing for MDsveX in Elder.js.

  • Portability and Future-proofing
    • MDX requires you to use React and a bundler plugin, tying you in to that ecosystem. Great if you stay within the lines of what they imagine, problematic if you want something slightly different or need to move off React (you now have to go through and convert all your content)
    • Shortcodes are framework and platform agnostic. This is how I can blog on Dev.to and render on my own site (the inverted POSSE pattern), and have both render correctly in their native environments.
    • Though shortcodes still require a build chain to process them (including injecting scripts if needed), the minimal viable shortcode processor is no more complex than String.replace. Ultimately, shortcodes are more likely to show graceful degradation: I have seen 20 year old blogs with shortcodes that are no longer active, but are still readable because they just fall back to plain text.
  • Scope
    • Shortcodes are limited to their immediate responsibility area - starting and ending with the brackets that designate them.
    • MDX has a broader scope than Shortcodes in that it transforms the entire file - meaning that you can (and often should) supply your own versions of markdown components. This is handy for, for example, adding classes and preload handling to <a> tags, or adding hash links and id's to <h2> headers like I do on my blog.
  • Customizability

    • Shortcodes require you to predefine all the components you are going to use up front. If you'd like to add a new type of component, you'll have to jump out of your writing mode, go add some code to your components folder, and then jump back in to keep writing.
    • With MDX, you can compose components as freely as you do JSX:
    import GroupThing from '../components/GroupThing'
    import ItemThing from '../components/ItemThing'
    
    # My blog post
    
    Here's a thingy:
    <GroupThing foo="bar">
        <ItemThing baz={1} />
        <ItemThing baz={2} />
        <ItemThing baz={3} />
    </GroupThing>
    
    ## You can even define stateful components inline!
    
    export const MyCounter = () => {
      const [counter, setCounter] = React.useState(0);
      return (
        <button onClick={() => setCounter((c) => c + 1)}>
          Increment {counter}
        </button>
      );
    };
    
    <MyCounter />
    
    Yes this is still MDX even though it looks like a React/JSX file!
    

    Since you are already using a JS build tool to compile MDX, it is easy to inject further JS as needed and have the bundler properly resolve and code split things for you.

  • WYSIWYG

    • This is a minor point, but the fact that everything in markdown corresponds to a visible rendered element is a nice correspondence. MDX breaks this by having import and export statements that compile away to nothing. In practice this is no big deal but it slightly rankles me.

Conclusion

I think most developer bloggers spring for MDX because they enjoy using JSX, but they end up using the same 3-4 components on every single post or document they write. In those scenarios, they are accepting the downsides of MDX, without really benefiting from the customizability.

In fact, I think you can go pretty far with shortcodes. Does it really matter if you have 30 components that you pull in via shortcodes? Not really, amount of components isn't really a motivating factor. I think this covers most bloggers. Most bloggers should use shortcodes.

However, there are still valid usecases of MDX.

For a design system or frontend component library, one might argue that MDX allows you to display the exact components that you are documenting:

import Button from './components/Button'

# Button

This is our default Button!

<Button />

This is our secondary Button!

<Button type="ghost" />
Enter fullscreen mode Exit fullscreen mode

However for backend code, or for complex-enough frontend code that you'd like to run integration tests on, you may wish to transclude from a source file, and that may use shortcodes. Storybook's Component Story Format also provides a nice convention that keeps your documentation agnostic of MDX.

Because MDX compiles to a React component, you could build tooling that can typecheck MDX (as far as I know this doesn't yet exist, hence this point is all the way down here). The same is doable for shortcodes, but since there is very little restriction on how shortcodes are processed, it is far less likely that successful shared tooling will arise.

Finally, there's the question of customization. If you need to compose components inline as you write, then MDX is unquestionably the right choice. This is why Hashicorp went with MDX, and you can listen in on my conversation with Jeff Escalante for more on this.

My final "quote" on this, if you will, is this:

  • MDX works best as a more concise way to write React *pages*. MDX optimizes for flexibility - great for complex docs!
  • Shortcodes are best for including custom components in a future-proof way. Shortcodes optimize for longevity - great for blogs!

Appendix: Structured Content

Perhaps a third "alternative" to Shortcodes vs MDX is structured content - discrete "blocks" of content rendered inside of a CMS, like you see in Notion, Sanity.io or in WordPress Gutenberg. I don't have a lot of personal experience with this, but my sense is that it locks you in to these systems, in exchange for WYSIWYG and no-code editing. In a way, structured content is what would happen if your writing is entirely made up of data inside shortcodes.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .