clubmate.fi

A good[ish] website

Web development blog, loads of UI and JavaScript topics

Render draft posts locally in Gatsby

Filed under: JavaScript— Tagged with: gatsby

This post looks how to render posts from your drafts directory in an MDX Gatsby setup. Locally only, so you don’t leak unfinished posts.

I’ve got this habit of using my drafts directory contents/drafts as a kind of s scratch pad. I jot down ideas, create post stems, and throw in some interesting links as I do research. I’ve got ten or so posts cooking at all times.

But this is slightly janky, if I want to preview the post I have to put it into the posts directory, so it’s rendered by Gatsby. And I got to thinking, that what if I render the drafts too? Making sure not to deploy them or any of the related routes.

Only on development

You can get the environment from your built-in Gatsby environmental variables. You get them like so:

const isDevelopment = process.env.NODE_ENV === 'development'

Note that line, we need it later on.

You can read more about the environmental variables from Gatsby docs.

Tell gatsby-source-filesystem to pull in the drafts

You’ve got this in your gatsby-config.js for sure:

module.exports = {
  plugins: [
    {
      resolve: 'gatsby-source-filesystem',
      options: {
        name: 'posts',
        path: path.resolve('./contents/posts')
      }
    }
  ]
}

gatsby-source-filesystem can be called more than once, and we can add another one for the drafts, but only on the development environment. Set up a little ternary condition:

const isDevelopment = process.env.NODE_ENV === 'development'

module.exports = {
  plugins: [
    {
      resolve: 'gatsby-source-filesystem',
      options: {
        name: 'posts',
        path: path.resolve('./contents/posts')
      }
    },
    ...(isDevelopment
      ? [
          {
            resolve: 'gatsby-source-filesystem',
            options: {
              name: 'drafts',
              path: path.resolve('./contents/drafts')
            }
          }
        ]
      : [])
  ]
}

Now we’ve told Gatsby that drafts exist.

Query the data

Writing the GraphQL queries for the drafts data isn’t that surprising, but there are few things to consider if you’re running MDX.

Data handling in non-NXD setup

If you’re a non-MDX user, then the code example in gatsby-filesystem docs should work (below). To differentiate between the "posts" and the "drafts" you filter in allFile. The instance name (posts|drafts) live in sourceInstanceName:

{
  allFile(filter: { sourceInstanceName: { eq: "drafts" } }) {
    edges {
      node {
        foo
        bar
      }
    }
  }
}

You can check that with GraphiQL query explorer tool at http://localhost:8000/___graphql:

Dig into the data with GraphiQL

If you don’t run MDX, then you should be good with the data.

Note that, if you’re using MDX in a form of gatsby-plugin-mdx, you can’t sub-select in the parent of a node, and you don’t have filter.sourceInstanceName, but you can add something similar yourself while you’re creating the pages.

Next, let's make it filterable.

Make the data filterable by source name

You can add arbitrary data under the fields object in the GraphQL data, by using the createNodeField helper inside the onCreate hook. You might have something like this your gatsby-node.js file:

exports.onCreateNode = ({ node, getNode, actions }) => {
  const { createNodeField } = actions

  if (node.internal.type === 'Mdx') {
    createNodeField({
      name: 'slug',
      node,
      value: createFilePath({
        node,
        getNode,
        trailingSlash: false
      })
    })
  }
}

The above code creates the slug field for the posts. But we want to add an extra field: filter.fields.sourceName. We can do that with the same createNodeField helper:

exports.onCreateNode = ({ node, getNode, actions }) => {
  const { createNodeField } = actions

  if (node.internal.type === 'Mdx') {
    const parent = getNode(node.parent)    const isDraft = parent.sourceInstanceName === 'drafts'

    if (parent.internal.type === 'File') {      createNodeField({        name: 'sourceName',        node,        value: parent.sourceInstanceName      })    }
    const slug = createFilePath({
      node,
      getNode,
      basePath: 'posts',
      trailingSlash: false
    })

    // Check if it’s a draft or a post.
    const entryPath = path.join(isDraft ? 'draft' : '', slug)
    // Make the slug field.
    createNodeField({ name: 'slug', node, value: entryPath })
  }
}

The highlighted bits explained:

  • First, get the parent, using the getNode helper coming in from the onCreate hook.
  • Then, make the sourceName field, after this you should have it available in filter.fields.sourceName, check from GraphiQL.
  • Finally, generate the correct path for the entry, I’d want the draft posts to be available from the path /draft/draft-name.

Data handling with MDX

This is pretty much the full query that I use, it’s filtered by sourceName:

query($skip: Int, $limit: Int = 10, $sourceName: String = "drafts") {
  allMdx(
    # Show only the drafts
    filter: { fields: { sourceName: { eq: $sourceName } } }    # Sort the drafts by date
    sort: { fields: [frontmatter___date], order: DESC }
    # Skip and limit are for pagination
    skip: $skip
    limit: $limit
  ) {
    edges {
      node {
        id
        frontmatter {
          title
          date(formatString: "DD MMMM, YYYY")
        }
        fields {
          slug
        }
        excerpt
      }
    }
  }
}

Now, put that into a template.

Render a list of drafts

Whenever rendering content, it needs to be at least two places:

  • /draft/<draft-name>: the individual post page, in my case it’s handled by the same template that renders posts. So that’s already done.
  • /drafts: a list of all the items, which is the archive page.

Let’s do the archive page now.

If you put anything in src/pages it becomes a route. But you also want to make sure that the drafts route is not rendered anywhere else but in the local environment. We can import the 404 page, and then export the 404 page if we’re not on development, otherwise we’ll just render the drafts template:

import React from 'react'
import NotFound from './404'

const Drafts = props => {
  return <span>Draft stuff..</span>
}

export default process.env.NODE_ENV !== 'development' ? NotFound : Drafts
This way the 404 page has the HTTP status code of 200, and not 404. This might be an issue in some cases.

Here’s more expanded example, rendering a link, title, and an excerpt:

// src/pages/drafts.js
import React from 'react'
import NotFound from './404'

const Drafts = props => {
  const { edges } = props.data.allMdx

  return (
    <div>
      {edges.map(draft => {
        <Link to={draft.node.fields.slug}>
          <h2>{draft.node.frontmatter.title}</h2>
        </Link>
        <p>{draft.node.frontmatter.excerpt}</p>
      })}
    </div>
  )
}

export default process.env.NODE_ENV !== 'development' ? NotFound : Drafts

Add it to your nav

This site’s nav when running it locally, has the drafts menu item up there:

A screenshot of a UI where one item is ”Drafts“

Below is an un-styled example using the Gatsby’s Link navigation component:

import { Link } from 'gatsby'

return (
  <nav>
    {process.env.NODE_ENV === 'development' && (
      <Link activeStyle={{ borderBottom: '2px solid black' }} to="/drafts">
        Drafts
      </Link>
    )}
    {/* More nav items etc... */}
  </nav>
)

The activeStyle prop is a feature of reach-router (which Gatsby uses under the hood). It knows the current path, and it has the to prop, so it can do a little check if the nav item is the current page.

Test it up

Build the project, then serve the project:

$ gatsby build
$ gatsby serve

Test that the single draft /draft/my-post-draft route and the /drafts aren’t accessible.

Also check your projects public directory that there aren’t any component---src-draft-*.js templates there, and that the public/drafts/index.html is empty.

Conclusions

To sum this post up:

  • Tell gatsby-source-filesystem that drafts exist.
  • Add the name field to the filter, so the MDX data can be filtered.
  • Query the data related to the drafts with GraphQL.
  • Make the drafts template to render the drafts archive page.
  • Add drafts to the menu.

Thanks for reading. Hope this was helpful.

Comments would go here, but the commenting system isn’t ready yet, sorry. Tweet me @hiljaa if you want to make a correction etc.

  • © 2021 Antti Hiljá
  • About
  • Follow me in Twatter → @hiljaa
  • All rights reserved yadda yadda.
  • I can put just about anything here, no one reads the footer anyways.
  • console.log('Smash the patriarchy!')
  • I love u!