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.
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.
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.
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.
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.
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.
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.
I think I'm safe. 😎
As always, happy coding! ✨
If you enjoyed this article, please consider following me on Twitter