Next.js "New Post" Node Script


September 13, 2021

Reading time
6 min read

I use this tool to help start new posts for Next.js, but you could do something similar for Gatsby, Eleventy, Hugo, etc... I mostly wanted a quick way to genearte a new post so that I can reduce the amount of time to start writting content!

Quick 1 Minute Video Tour

Before we unpack how it's made, here is a quick 1 minute tour video of how it's setup and what it looks like when running.

Package.json and Starting Script

I added a custom npm script to my package.json file called new-post that runs a local Node program.

  "name": "",
  "scripts": {
    "new-post": "node ./lib/new-post"
  "devDependencies": {
    "inquirer": "^8.1.2",
    "inquirer-datepicker-prompt": "^0.4.2",
    "open": "^8.2.1",
    "slug": "^5.1.0"

In order to run the custom npm script, you can execute the script npm run new-post from the command line.

npm run new-post

Development Dependencies

The above package.json snippet shows the special dev dependencies that I use in my Node program.

  • slug - Slugifies strings, even when they contain Unicode. Make strings URL-safe.
  • inquirer - A collection of common interactive command line user interfaces.
  • inquirer-datepicker-prompt - Datepicker plugin for Inquirer.js
  • open - Open stuff like URLs, files, executables. Cross-platform.

You can install these dependencies from the command line using either npm or yarn.

npm install -D open slug inquirer inquirer-datepicker-prompt
yarn add -D open slug inquirer inquirer-datepicker-prompt

Breaking Down the JavaScript

High Level Overview

From a high level, the Node scripts does the following.

  1. Requires the dependencies (listed in the previous section)
  2. Prompts the user for specifics about the new post
  3. Takes the answers and...
    • Generates a new mdx file with frontmatter
    • Creates an associated image folder
    • Open the browser to preview the post
const fs = require("fs");
const slugify = require("slug");
const inquirer = require("inquirer");
const open = require("open");

// Register datepicker plugin for inquirer
inquirer.registerPrompt("datetime", require("inquirer-datepicker-prompt"));

    { type: "input", name: "title", message: "Title" },
    // ... other propts for desc, slug, date, categories, draft, etc ...
  .then(answers => {
    const { slug, date, title, desc, categories, draft, published } = answers;
    // ... take answers and create post, images folder, and open browser ...

NOTE: You can download the full script from GitHub;

Inquirer Prompt Types

The inquirer module has many default Prompt Types available to use such as list, rawlist, expand, checkbox, confirm, input, number, password, and editor. In addition to these you can leverage external Plugins.

Input Type

I use the input prompt type to request for the Title of the blog post along with Slug and Description. For the Slug field, I grab the value entered in for title and run it through slugify to provide the default value for that field. The user can change the value if they want, or take the default option.

{ type: "input", name: "title", message: "Title" },
  type: "input",
  name: "slug",
  default: answers => slugify(answers.title.toLowerCase()),
  message: "Slug"
    type: "input",
    name: "desc",
    message: "Description"

Confirm Type

For my blog posts I have the idea of a draft version that is only available in development and a published version that is listed on my blog and included in the RSS feed. I use the confirm type to ask about these fields and can provided a default answer.

{ type: "confirm", name: "draft", default: true, message: "Is Draft?" },
{ type: "confirm", name: "published", default: false, message: "Is Published?" }

Select Type

In order to capture the categories of the blog post, I use the checkbox prompt types and provides an array of choices. This prompt supports a validate function where I can enforce at least one category has been selected from the list.

  type: "checkbox",
  message: "Select categories",
  name: "categories",
  choices: [
    { name: "JavaScript" },
    { name: "React" },
    { name: "CSS" },
    { name: "Comic" },
    { name: "Neovim" },
    { name: "Vim" },
    { name: "Command Line" }
  validate: answer =>
    answer.length ? true : "You must choose at least one category"

DateTime Type

One last field that I capture is when I want the blog post to be published. Technically I control when the item is published since this is a static site, but this is the date that is displayed on the post and it controls the sorting of blog posts. The inquirer doesn't natively have a Date prompt type, but I was able to use the inquirer-datepicker-prompt external plugin to capture the published date.

  type: "datetime",
  name: "date",
  message: "When would you like the post to be published?",
  format: ["yyyy", "/", "mm", "/", "dd"]

Abort if Post Already Exists

Once the information from the user has been gathered, I wanted to make sure that I wasn't going to overwrite an existing blog post at the same location, so that is the first thing I do. If I find a post with the same slug, then I abort the script and let the user know.

if (fs.existsSync(`/posts/${slug}.mdx`)) {
  throw "That post already exists!";

Write New Post & Create Images Folder

If the blog post is indeed unique (it doesn't already exist), then I go ahead and gather up the frontmatter for the post and write the file in the apropriate location. In addition, I create a folder in the public folder in case I want to add any images for that post.

  `posts/${ slug }.mdx`,
slug: "${slug}"
date: "${date.toISOString()}"
title: "${title}"
description: "${desc}"
categories: ${JSON.stringify(categories)}
draft: ${draft}
published: ${published}

## Introduction

fs.mkdirSync(`public/images/${slug}`, { recursive: true });

Launch the Browser to New Post

The last thing that the script does is to open a new browser tab to where the new post is located. I use the open node module to do this is a cross-platform sort of way.


Comment and Share

NOTE: You can download the full Node script described in the above post from GitHub;

If you found this post helpful, please consider sharing on Twitter.

Also, do you have a method to create new posts on your blog? Do you capture other fields other than the ones that I mentioned? If so, I'd love to hear about it! Feel free to join the discussion on Twitter.

Web Mentions

Tweet about this post and have it show up here!