A good[ish] website
Web development blog, loads of UI and JavaScript topics
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 a 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 move it into the content/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.
You can get the environment from the Gatsby environmental variables:
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.
You’ve probably got something like this in your gatsby-config.js
:
module.exports = {
plugins: [
{
resolve: 'gatsby-source-filesystem',
options: {
name: 'posts',
path: path.resolve('./contents/posts')
}
}
]
}
gatsby-source-filesystem
can be called more than once, so 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.
Writing the GraphQL queries for the drafts data isn’t that surprising, but there are few things to consider if you’re running MDX.
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
:
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.
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:
getNode
helper coming in from the onCreate
hook.sourceName
field, after this you should have it available in filter.fields.sourceName
, check from GraphiQL./draft/draft-name
.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.
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
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
This site’s nav when running it locally, has the drafts menu item up there:
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.
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.
To sum this post up:
gatsby-source-filesystem
that drafts exist.name
field to the filter, so the MDX data can be filtered.Thanks for reading. Hope this was helpful.
Comments would go here, but the commenting system isn’t ready yet, sorry.