Adding an Updated Date to Markdown and MDX Posts
Wednesday, 17 February 2021The Problem
Unlike a CMS grabbing the modified date for your posts in frameworks like Gatsby and Next can come as a bit of a challenge. In Gatsby for instance you could use the gatsby-transformer-gitinfo
plug-in which adds a gitPublishedDate
field to your GraphQL schema, and this will work perfectly for a lot of cases. However, there is a huge caveat in that approach, which is what I experienced when deploying to Vercel. If your .git
directory isn’t deployed alongside your repo, then your git logs can’t be accessed and no timestamps can be pulled. This is true for a few other approaches I stumbled upon, such as angelos.devs and pragmatic pineapples posts.
Finding a Solution
Luckily I did come across a thread from @monicalent which offered some alternate solutions, and influenced the approach I ended up taking (here is the initial tweet from Lee Robinson and the snippet page it links to). Unlike the previous approaches, it adds the updated date when staging your content to commit rather than pulling the dates at build time. This means you don’t need to rely on a .git
directory being deployed, with minimal changes to your existing git workflow.
The Approach
The approach that Lee Robinson and Michael Novotny took is to have an existing meta-object in the MDX which is altered via a script. It’s a great idea but I want to cater to both Markdown and MDX files and not have to rely on an initial object to exist. Therefore, instead of having a meta-object, which would only be usable in MDX documents, we parse and alter the front-matter instead. This way we have a common solution which easily integrates into Next and Gatsby projects.
The Node Script
Taking all of this into account, I wrote the below script to satisfy those needs:
/* eslint-disable import/no-extraneous-dependencies */
const fs = require('fs').promises
const matter = require('gray-matter')
const updateFrontmatter = async () => {
const [, , ...mdFilePaths] = process.argv
mdFilePaths.forEach(async (path) => {
const file = matter.read(path)
const { data: currentFrontmatter } = file
if (currentFrontmatter.published === true) {
const updatedFrontmatter = {
...currentFrontmatter,
updatedOn: new Date().toISOString(),
}
file.data = updatedFrontmatter
const updatedFileContent = matter.stringify(file)
fs.writeFile(path, updatedFileContent)
}
})
}
updateFrontmatter()
Here’s what’s happening:
- We grab the Markdown/MDX file paths
- Loop over all of the file paths
- Read each file with the gray-matter package which parses the front-matter for us.
- Parse the front-matter and split it into an object. Take the rest of the content and dump it in a string.
{
"content": "<h1>Hello world!</h1>",
"data": {
"title": "Hello",
"slug": "home"
}
}
^ What is returned from gray-matter, data here being the parsed front-matter content.
- If the posts published front-matter is true we continue, otherwise we do nothing.
- Immutably update the object with the new date and assign it to the data property.
- Take the newly updated gray-matter object and turn this back into a string
- Overwrite the existing content using
fs.writeFile
Now you will probably have noticed above that we are grabbing the paths using process.argv
. The reason for doing this is we are using Husky and lint-staged to create a git commit hook. What this essentially means is when we make a commit which includes a Markdown or MDX file, we can run a node script and pass the paths as arguments, hence what you saw above.
Setting Up Husky and lint-staged
- Install Husky
npm i -D husky
- Add Husky’s git hooks with
npx husky install
(you will notice it created a .husky directory). - Add your commit hook via
npx husky add .husky/pre-commit "npm run lint:staged"
. This will add a pre-commit file to your.husky
directory and within that file, your npm command should exist (if not you can manually add it) - Install lint-staged
npm i lint-staged -D
- Create a
.lintstagedrc
file - Add your lint-staged config:
{
"**/*.{md,mdx}": "node updateFrontmatter"
}
(updateFrontmatter is the filename of your script)
- Add the script to your package.json
"lint:staged": "lint-staged"
Conclusion
Now, whenever you commit either a Markdown or MDX file Husky will run the node script and update/create the updatedOn frontmatter content. No more worrying about the build step and depending on git logs being available. It just works.
Useful Links
- gray-matter package
- Husky docs - These are worth checking out as V5 is very different to V4
- gatsby-transformer-gitinfo - if your .git directory is deployed alongside your repo check out this plugin