Hiding Unpublished Drafts with MDX on Gatsby

I'm just getting started on a blog, and I wanted to use Gatsby for it.

In reading the docs for Gatsby, I came across MDX. MDX lets you write posts in Markdown while also letting you import and use JSX. Once it's set up, I get to write my markdown, add some flair with components when needed, import other MDX files, and toss in some Javascript if I need to set any variables or do any maps.

You may be thinking that sounds amazing. And you would be right. The hype definitely feels warranted. @johno and @silvenon did some awesome work.

My little hiccup

Something I was having a bit of trouble with, though, was how to hide a draft on my live site while I was still working on it. The Gatsby plugin is awesome in that it will just find your mdx files in src/pages and create a page out of each of them for you, with only minimal setup. But with that magic, we limit our configuration options. There's not any great way to prevent it from skipping over the src/pages posts that you've marked as a draft with the frontmatter.

Note: The probably better way to solve this problem would be to move the posts outside of src/pages so that the MDX plugin does not build anything automatically and then follow this guide to create the blog posts. With this setup, you would just filter out any posts that have published: false out of your createPages query, and you'd be good to go.

But maybe you still want to use the default src/pages magic but don't want to move the files or to worry about GraphQL or to jump into your gatsby-node file. Maybe you just like all of your drafts being in the same directory that they'll be built in, thanksverymuch.

Well, as my high school algebra teacher said pretty much every day, there's more than one way to get to the mall. Indeed there is, Sister Phillips. If we don't follow the guide above, we can let the plugin just render the draft page. And then instead of showing the full post content, we can just render some custom 'this post isn't finished yet' messaging. Et voilà! Now the world won't find all our erotic fan fiction that we know the world isn't quite ready for.

Note: The below solution, while it works, is less important than hiding any links to unpublished drafts on our blog index page. If we're really trying to hide any drafts, better to hide the post entirely than show a 'not found' message. If we filter out the unpublished pages from our allMdx (allMdx(filter: {frontmatter: {published: {eq: true}}}) instead of just allMdx ) query, then it will be suuuuuper difficult for anyone to guess a post slug and find our racy novellas. BUT! Nothing wrong with a fallback in case someone happens upon the slug or we forget to filter out the drafts.

Install and configure MDX to point to a default layout

First, we'll install the gatsby plugin and its requirements:

yarn add gatsby-plugin-mdx @mdx-js/mdx @mdx-js/react

Next, we add the MDX plugin to our gatsby-config and tell it to wrap each post in a layout component. The layout component is where we can inject the styles that every blog post shares so that we don't have to remember to import all of our components in the top of each separate .mdx file.

// gatsby-config.js
module.exports = {
...,
plugins: [
...,
{
resolve: `gatsby-plugin-mdx`,
options: {
defaultLayouts: {
default: require.resolve("./src/templates/post-layout.js"),
}
}
}
]
}

Build out the MDXProvider template

This template is where we'll use the MDXProvider, which can be used to customize our MDX posts. In the documentation, it describes how we can use custom components when rendering. The docs lists every component that MDX could render from markdown, so you can customize any of your headers, paragraphs, tables, thematic breaks, block quotes, etc, through the components prop on the MDXProvider.

import React from "react"
import { MDXProvider } from "@mdx-js/react"
import { Link } from "gatsby"
import { CustomH1, CustomParagraph } from "../components/ui"
const shortcodes = {
Link,
h1: CustomH1,
p: CustomParagraph,
...
}
export default ({ children }) => (
<MDXProvider components={shortcodes}>{children}</MDXProvider>
)

This setup would allow us to use Gatsby links anywhere in our MDX files without importing the Link into each file. It also maps all h1 and p tags built from our markdown to some custom components.

But as the above stands, we'd still be posting our outlines and drafts for all to find. Again, they'll be pretty hidden once we filter out the unpublished pages from our allMdx query for our blog index page. But assuming someone slips past our defenses and find a post we don't want the world to see yet, this file is the place to prevent the content from being rendered.

We'll put the published status in the frontmatter of each blog post, so we will have access to the published boolean on pageContext.frontmatter.published. And since we don't really want to hide the posts from ourselves locally while we're working on writing it, we'll add a check for the node environment and only hide the post text if it's on production.

<!-- src/pages/posts/hidden.md -->
---
title: "hello, world"
isPublished: false
date: "02-23-2020"
---
# I hope the world can't find this post just yet
And here is the rest of the content...
// src/templates/post-layout.js
import React from "react"
import { MDXProvider } from "@mdx-js/react"
import { Link } from "gatsby"
import { CustomHeader } from "../components/ui"
const shortcodes = { Link, h1: CustomHeader }
const NotFinishedMessage = () => <p>This post is still a work in progress. Send me a note to tell me to hurry up and finish it!</p>;
const isProduction = process.env.NODE_ENV === 'production'
export default ({ children, pageContext }) => (
<MDXProvider components={shortcodes}>
{isProduction && !pageContext.frontmatter.published ? (
<NotFinishedMessage />
) : (
children
)}
</MDXProvider>
)

And there we have it. If somehow someone makes it to an unfinished post in production, they'll see a message to keep their sneaky eyes outta here. Otherwise, like when we're developing, the full content will be shown.

Again, this extra guard is pretty small potatoes, since somewhere on the order of definitely zero people are typing random urls on my site to find unfinished posts. But now that you know you've got access to pageContext.frontmatter, you could do something else in here. Instead of hide your drafts, you could format the published date or add the blog title or a list of tags here so that they work auto-magically for every blog post.

Thanks for reading along!

©2020 Matthew Knudsen