Quickly organizing your NextJS MDX blog with Node scripts

Quickly organizing your NextJS MDX blog with Node scripts

This is my 11th article in this NextJS MDX. Already, I have been surprised to find how difficult a time I am having hunting down articles for links

The main reason it's so hard is because in VSCode, I have no concept of the order in which I wrote something.

My blog is sorted by publishDate in the frontmatter, but I don't have a way to sort my VSCode files by that. I got irritated enough to do something about it. Here's what I did...

Step 1: Simple ordering for my files

This is personal blog, so I didn't need super fancy sorting or filtering, or anything like that. I was pretty sure I could just add a numeric integer at the front of each file to get the level of organization I was looking for.

My original file structure looked like this:

/src
  /content
    /articles
      adding-fancy-og-images-nextjs-mdx-blog.mdx
      adding-next-sitemap-to-your-nextjs-site.mdx
      draft-article.mdx
      eas-build-ios-app-crashing-debug-tips.mdx
      easy-callout-content-with-rehype-plugins.mdx
      enriched-bible-verse-react-component.mdx
      filtering-by-publish-date-nextjs-mdx-blog.mdx
      filtering-by-tags-nextjs-mdx-blog.mdx
      importing-testing-nextjs-code-playground.mdx
      improving-mdx-code-editor-theme-highlight-line-numbers.mdx
      simple-bible-gateway-verse-component.mdx
      tailwind-plus-sandpack-playground-component.mdx

Pretty difficult to tell which order I wrote things in, or otherwise orient myself to my own content.

Plus, there's always that draft-article.mdx file that I have in there for quickly copying and pasting frontmatter to a new article. Lame.

Here's how I decided to change it:

/src
  /content
    /articles
      0001-simple-bible-gateway-verse-component.mdx
      0002-adding-next-sitemap-to-your-nextjs-site.mdx
      0003-eas-build-ios-app-crashing-debug-tips.mdx
      0004-adding-fancy-og-images-nextjs-mdx-blog.mdx
      0005-filtering-by-publish-date-nextjs-mdx-blog.mdx
      0006-filtering-by-tags-nextjs-mdx-blog.mdx
      0007-improving-mdx-code-editor-theme-highlight-line-numbers.mdx
      0008-tailwind-plus-sandpack-playground-component.mdx
      0009-importing-testing-nextjs-code-playground.mdx
      0010-easy-callout-content-with-rehype-plugins.mdx

Much cleaner, and now I at least have some idea of the order in which I wrote things.

Note
I did go through and rename these manually, based on publish date. There were only 10 articles though, so it only took about 5 minutes.

If I had many more than that, I probably would whip together a on-off "rename" script.

Step 2: Fix my broken links and page renders

Once I renamed everything, the site worked, but there was a major hitch... All of my content URLs changed.

Because of how I was pulling data in getStaticProps, urls that used to be:

http://localhost:3000/articles/adding-next-sitemap-to-your-nextjs-site

were now:

http://localhost:3000/articles/0002-adding-next-sitemap-to-your-nextjs-site

Here is how I fixed it.

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

async function getFileSlugs(srcDir) {
  // ...
}

export async function getAllTags(srcDir) {
  let fileSlugs = await getFileSlugs(srcDir)

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

  // ...
}

export async function getAllContent(srcDir) {
  let fileSlugs = await getFileSlugs(srcDir)
  
  const cb = (fileSlug) => getContentItem(fileSlug, srcDir)

  //...
}

export async function getContentBySlug(slug, srcDir) {
  const fileSlugs = await getFileSlugs(srcDir)
  const foundSlug = fileSlugs.find((fileSlug) => fileSlug.replace(/^[0-9]+\-/, '') === slug);

  if (!foundSlug) {
    return null;
  }

  return getContentItem(foundSlug, srcDir);
}

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

  const { content, data } = matter(source)

  const slug = fileSlug.replace(/^[0-9]+\-/, '')

  return {
    content,
    data,
    fileSlug,
    slug,
  }
}

This is a little involved, so let's break it down.

First, I renamed every instance of slug to fileSlug, since it now represented a string like

0045-my-article-name

Next, I created a new slug variable in the getContentItem function, which is just the fileSlug with the leading numbers removed.

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

  const { content, data } = matter(source)

  const slug = fileSlug.replace(/^[0-9]+\-/, '')

  return {
    content,
    data,
    fileSlug,
    slug,
  }
}

This got my links working again, however, NextJS could not find any of the pages, because the slug was now different than the file name.

Now, I just needed to get NextJS getStaticProps reading the right file again. So I created a new method called getContentBySlug that takes a slug and srcDir and returns the content item.

/src/lib/getContent.js
export async function getContentBySlug(slug, srcDir) {
  const fileSlugs = await getFileSlugs(srcDir)
  const foundSlug = fileSlugs.find((fileSlug) => fileSlug.replace(/^[0-9]+\-/, '') === slug);

  if (!foundSlug) {
    return null;
  }

  return getContentItem(foundSlug, srcDir);
}

Lastly, I just need to change what method I was using in my /pages/articles/[slug].js file.

/src/pages/articles/[slug].js
import { MDXProvider } from '@mdx-js/react';
import { serialize } from 'next-mdx-remote/serialize';
import rehypePrism from 'rehype-prism-plus'
import rehypeCodeTitles from 'rehype-code-titles'
import rehypeBlockquote from '@/lib/rehype-blockquote-meta';
import { MDXRemote } from 'next-mdx-remote'

import components from '@/components/MDXComponents';
import { ArticleLayout } from '@/components/ArticleLayout';
+ import { getAllContent, getContentBySlug } from '@/lib/getContent';
- import { getAllContent, getContentItem } from '@/lib/getContent';

... other code 

export async function getStaticProps({ params: { slug } }) {
+  const { content, data } = await getContentBySlug(slug, 'articles');
-  const { content, data } = await getContentItem(slug, 'articles');

  const mdxSource = await serialize(content, {
    // Optionally pass remark/rehype plugins
    components,
    mdxOptions: {
      rehypePlugins: [rehypeCodeTitles, rehypePrism, rehypeBlockquote],
    },
    scope: data,
  });

  return {
    props: {
      raw: content,
      meta: { ...data, slug },
      mdxSource,
    },
  };
}

And that's that! All my NextJS articles are working again by slug. 🦄

Step 3: Create a new, orderd MDX article from a NodeJS script

Finally, I wanted to make it easier to create new articles, and get rid of that draft-article.mdx file.

To do this, I created a new Node.js script called add-article.js in my /scripts folder.

It simply takes in a filename as the only argument, and creates the next numbered article with that filename slugified.

/scripts/add-article.js
const fs = require('fs')
const path = require('path')

function getFileNameIndex(fileName) {
  const match = fileName.match(/^[0-9]+\-/)
  if (!match) return 0
  return parseInt(match[0].slice(0, -1), 10)
}

async function addArticle() {
  // grab the title from the command line arguments
  const title = process.argv[2]

  // read the src/content/articles directory and get the last numbered file (e.g. 0035-hello-world.mdx)
  const files = await fs.readdirSync(
    path.join(process.cwd(), 'src/content/articles'),
    {
      withFileTypes: true,
    }
  )

  const lastFile = files
    .filter((file) => file.name.endsWith('.mdx'))
    .sort((a, b) => {
      const aIndex = getFileNameIndex(a.name)
      const bIndex = getFileNameIndex(b.name)
      return bIndex - aIndex
    })[0]

  // get the index of the last file
  const index = getFileNameIndex(lastFile.name)

  // ensure the index is a string with 4 digits
  const indexString = `${index + 1}`.padStart(4, '0')

  // get the current date in the format YYYY-MM-DD
  const date = new Date().toISOString().split('T')[0];

  // create a new file name with the next index
  const newFileName = `${indexString}-${title
    .toLowerCase()
    .replace(/ /g, '-')}.mdx`

  // create the new file
  await fs.writeFileSync(
    path.join(process.cwd(), 'src/content/articles', newFileName),
    `---
author: Ty Barho
date: '${date}'
title: ${title}
description:
tags:
---

`
  );
}

async function run() {
  await addArticle()
}

run()

To run it, I first needed to make it executable with:

$ chmod 755 ./scripts/add-article.js

Then, I could run it with:

$ ./scripts/add-article.js "My New Article"

Lastly, I wanted it to be a little bit more convenient, so I added a yarn script in my package.json, like so:

{
  "name": "tailwindui-template",
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "postbuild": "next-sitemap",
    "article:new": "node ./scripts/add-article.js"
  },
  "browserslist": "defaults, not ie <= 11",
  "dependencies": {
  },
  "devDependencies": {
  }
}

Now, any time I need a new article, I can simply run:

$ yarn article:new "My New Article"

Et voila`! A new article is created, and I can get to writing.

Conclusion

All in all, it was pretty easy to add some basic organization to my NextJS MDX blog, assuming it doesn't grow out of control. My numbering scheme works for up to 9,999 articles. I have a hard time believing I'll ever write that many.

Let's Math
At a cadence of 1 article per week, it would take me 192 years to write 9,989 articles.

I think I'm safe. 😎

As always, happy coding! ✨

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: