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.
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 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.
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:
{
"content": "<h1>Hello world!</h1>",
"data": {
"title": "Hello",
"slug": "home"
}
}
^ What is returned from gray-matter, data here being the parsed front-matter content.
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.
npm i -D husky
npx husky install
(you will notice it created a .husky directory).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)npm i lint-staged -D
.lintstagedrc
file{
"**/*.{md,mdx}": "node updateFrontmatter"
}
(updateFrontmatter is the filename of your script)
"lint:staged": "lint-staged"
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.