Filtering by Publish Date in your NextJS MDX Blog

Filtering by Publish Date in your NextJS MDX Blog

When writing for this site, I often find myself needing to be able to keep an article or lesson in draft, but also publish something to production.

Today, this NextJS MDX blog doesn't support the concept of a "draft" post, so let's introduce one.

First, in my content directory where I hold all my MDX files (for me, it's /src/content/articles), I'm going to add a new file called draft-article.mdx.

src/content/articles/draft-article.mdx
---
author: Ty Barho
date: 
title: A Draft Article
description: 
---

This is only a draft.

Filtering Articles by Publish Status with NextJS & MDX

Currently, I have functions for fetching my articles in /src/lib/getContent.js:

src/lib/getContent.js
import glob from 'fast-glob'
import * as path from 'path'
import fs from 'fs'
import matter from 'gray-matter'

export async function getAllContent(srcDir) {
  let slugs = (
    await glob(['*.mdx', '*/index.mdx'], {
      cwd: path.join(process.cwd(), `src/content/${srcDir}`),
    })
  ).map((n) => n.replace(/\.mdx$/, ''))

  const cb = (slug) => getContentItem(slug, srcDir)

  let items = await Promise.all(slugs.map(cb))

  return items.sort((a, z) => new Date(z.data.date) - new Date(a.data.date))
}

export async function getContentItem(slug, srcDir) {
  const filePath = path.join(process.cwd(), `src/content/${srcDir}/${slug}.mdx`)
  const source = fs.readFileSync(filePath)

  const { content, data } = matter(source)

  return {
    content,
    data,
    slug,
  }
}

First, let's update the getAllContent function to filter out any articles that don't have a date property set. Like so:

src/lib/getContent.js
export async function getAllContent(srcDir) {
  let slugs = (
    await glob(['*.mdx', '*/index.mdx'], {
      cwd: path.join(process.cwd(), `src/content/${srcDir}`),
    })
  ).map((n) => n.replace(/\.mdx$/, ''))

  const cb = (slug) => getContentItem(slug, srcDir)

  let items = await Promise.all(slugs.map(cb))

  return items
          .filter((item) => item.data.date)
          .sort((a, z) => new Date(z.data.date) - new Date(a.data.date))
}

Now, let's check and see that our test article is not showing up in our /articles page.

As you can see, the draft article is not showing up in the list of articles.

No Draft Article

Great! 🦄

Now, we need to make sure that article isn't accessible via the /articles/[slug] page by visiting http://localhost:3000/articles/draft-article.

Draft Article 404

Sure enough, we get a 404.

But wait... why?

We haven't written any special code to prevent this article from showing up, so why isn't it showing up?

NextJS getStaticPaths & fallback: false

The reason is because of how NextJS handles dynamic routes.

When you visit a page like /articles/[slug], NextJS will generate a static page for each slug in the getStaticPaths function.

Here's ours...

src/pages/articles/[slug].js
export async function getStaticPaths() {
  const articles = await getAllContent('articles');
  const paths = articles.map(a => `/articles/${a.slug}`);
  
  return {
    paths,
    fallback: false,
  };
}

As you can see, we are using the getAllContent method we just updated to find our lists of paths.

By using fallback: false, we are also telling NextJS that we don't want to generate any pages that don't exist in the paths array.

Here is how fallback: false works...

Yet another thing NextJS handles for us nearly automatically!

Fixing our NextJS Sitemap to Exclude Drafts

Now, we need to make sure that our sitemap doesn't include any draft articles.

In next-sitemap.config.js we will add a helper function to grab the front-matter of each article and filter out any articles that don't have a date property.

next-sitemap.config.js
// ... other imports
const matter = require('gray-matter')

function getFileData(sourceDir, filePath) {
  filePath = path.join(process.cwd(), `./src/content/${sourceDir}/${filePath}`)
  const source = fs.readFileSync(filePath)

  return matter(source).data
}

// ...

module.exports = {
  siteUrl: 'https://www.tybarho.com',
  sitemapSize: 7000,
  exclude: [],
  generateRobotsTxt: true,
  additionalPaths: async (config) => {
    let result = []

    const articles = fs.readdirSync(path.resolve('./src/content/articles'))
    const lessons = fs.readdirSync(path.resolve('./src/content/lessons'))

    for (const article of articles) {
      const data = getFileData('articles', article)

      if (data.date) {
        result.push(
          await config.transform(
            config,
            `/articles/${article.replace('.mdx', '')}`
          )
        )
      }
    }

    for (const lesson of lessons) {
      const data = getFileData('lessons', lesson)

      if (data.date) {
        result.push(
          await config.transform(
            config,
            `/lessons/${lesson.replace('.mdx', '')}`
          )
        )
      }
    }

    return result
  },
  robotsTxtOptions: {
    policies: [
      {
        userAgent: '*',
        allow: '/',
      },
    ],
  },
}

Be sure and run yarn postbuild to regenerate the sitemap and, check /public/sitemap-0.xml ensure that the draft article is not included.

Note: You may need to run yarn build first

And that's that! We are now filtering out all of our draft articles! 😎🚀

If you enjoyed this article, please consider following me on Twitter

Subscribe to the Newsletter

Subscribe for exclusive tips, strategies, and resources to launch, grow, & build your SaaS team.

Share this article on: