"New Post" Script for My Gatsby Site
As a technical writer, we sometimes don’t feel like writing even though our ideas are fresh in our minds. It seems like we don’t have the urge to go through the motions of putting down our thoughts to paper. For me the cause of this boiled down to Yak Shaving, where the small task involved in setting up a new post every time blocked me from actually getting started.
Yak Shaving for those not aware of the lingo is a term for the seemingly endless series of small tasks that have to be completed before the next step in a project can move forward. The term itself applied to Computer Science was coined by Dr. Carlin Vieri of MIT. Here is an email from a colleague of Dr. Vieri which really explains the term with a concrete example.
I stumbled upon a post by Elijah Manor that shows his process of getting started with writing in Next.js and immediately knew I had to do something similar for my Gatsby blog. This has always been a pain point for me when I want to start a new post, as I have to copy templates from a previous post and spend some time tweaking it and changing the front matter before I start writing. Sometimes the thought of going through with that alone makes me stop and not even bother.
Script Breakdown
There are four main parts to the script broken down as follows:
- Create command line questions.
- Parse input and get the answers.
- Create the files/directories and populate template.
- Output a message to the console on complete and open a browser to the new page.
👉 Part 1 (Command line questions):
Using Inquirer.js, there is a prompt
directive that takes an array of questions which have types. Using these questions, the package creates a command line questionnaire that shows the respective questions and collects the answers in an object. The question types can be input
, number
, confirm
, list
, rawlist
, expand
, checkbox
, password
, and editor
.
These cover most use cases and will suffice to write scripts for most tasks you will run into in practice.
For my use case, I also needed a type for datetime
, which is not provided by Inquirer. Checking the documentation, I did see a plugin that extends the base types and provides a new type for datetime
. Using Inquirer Date Prompt I was able to add the datetime
type for prompting questions asking for date and time.
const answers = await inquirer.prompt([
{
type: 'input',
name: 'title',
message: 'Title',
},
{
type: 'input',
name: 'slug',
default: (answers) => slugify(answers.title.toLowerCase()),
message: 'Slug',
},
{
type: 'checkbox',
name: 'category',
message: 'Select a category',
choices: [
{ name: 'AWS' },
{ name: 'CDK' },
{ name: 'TypeScript' },
{ name: 'React' },
{ name: 'DevOps' },
],
validate: (answer) => (answer.length ? true : 'You must choose a category'),
},
{
type: 'datetime',
name: 'date',
message: 'When would this post be published?',
format: [
'yyyy',
'-',
'mm',
'-',
'dd',
' ',
'hh',
':',
'MM',
':',
'ss',
' ',
'TT',
' ',
'Z',
],
validate: (answer) =>
answer.length ? true : 'You must enter a valid date and time',
},
{
type: 'datetime',
name: 'modified',
message: 'When was this post last modified?',
format: [
'yyyy',
'-',
'mm',
'-',
'dd',
' ',
'hh',
':',
'MM',
':',
'ss',
' ',
'TT',
' ',
'Z',
],
},
{
type: 'input',
name: 'thumbnail',
message: "Enter path to this post's thumbnail",
default: (answers) => `./images/${answers.slug}/${answers.slug}.png`,
},
{
type: 'input',
name: 'desc',
message: 'A short summary of the post: ',
},
{
type: 'confirm',
name: 'published',
default: false,
message: 'Publish?',
},
]);
The name
property matches the front matter names I need when starting a new post. All other options passed are self explanatory. Default
properties take a function that is passed an object that has all the previous answers. This creates a powerful system where you can use previous answers to make decisions for the next set of questions.
👉 Part 2 (Parse input and get answers):
Since we are running the script inside an async
block, we await
the results of the prompt
function call. This gives us the answers from the command line questions in an object. The object is spread to get the values we need.
const { title, slug, category, date, modified, thumbnail, desc, published } =
answers;
👉 Part 3 (Create files/directories and populate template):
I use template literals to interpolate a string that contains the path to where I want to store the new post. If using this for your own projects, the paths will be different based on where you have Gatsby configured to check for new posts.
When populating the template, don’t forget to convert dates to ISO strings otherwise it will output the dates in a human readable format which will not be compatible with Gatsby’s node creation process.
I also create a directory to store images for this post and have a post image in the front matter that is used for the image OG
tag on the site.
const fileName = `${rootDir}/src/posts/blog/${slug}.md`;
if (fs.existsSync(fileName)) {
throw 'That post already exists!';
}
fs.writeFileSync(
fileName,
`---
title: '${title}'
category: '${category}'
date: '${date.toISOString()}'
modified: '${modified.toISOString()}'
thumbnail: '${thumbnail}'
desc: |
${desc}
published: ${published}
---
* <Start writing ...>
`
);
fs.mkdirSync(`${rootDir}/src/posts/blog/images/${slug}`, {
recursive: true,
});
👉 Part 4 (Output message and open browser):
I noticed that opening the page immediately gave an error as it seems the Gatsby post is not fully done creating when the open command comes through. So adding a slight delay of one second before opening the page fixes the issue for me. Maybe there is another way to fix this but for now this hack gets the job done.
fs.mkdirSync(`${rootDir}/src/posts/blog/images/${slug}`, {
recursive: true,
});
// Wait for 1 second(s) to give Gatsby time to create page before we open it.
// Opening the pages immediately gives an error.
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log(`\n${slug} was created!`);
open(`http://localhost:8000/blog/${slug}`);
The entire script can be found on Github. Please feel free to copy and modify it for your own needs.