loading...
Cover image for Adding generators to your Gatsby site with plop

Adding generators to your Gatsby site with plop

ekafyi profile image Eka Updated on ・7 min read

In this post, I’m going to integrate plop to my playlist site (no online repo yet, sorry!). This is a simple “microblog” type website, which simply lists songs and albums I’m listening to on repeat, with Gatsby using local Markdown files as data source.

Plop, in their own definition, is “a small tool that gives you a simple way to generate code or any other type of flat text files in a consistent way”.

I’d like to use it because it makes it easier for me to create new posts. At the moment, if I want to add a new “track” post, I have to:

  • Create a new file in the src/contents directory with a certain name pattern (eg. 2019-01-20--star-guitar.md)
  • Open the file
  • If I remember all the field names, type the frontmatter (and optional body text); otherwise, copy from existing files.

A “track” post file looks like this:

------
title: 'Star Guitar'
artist: 'The Chemical Brothers'
date: '2019-01-19'
url: https://www.youtube.com/watch?v=0S43IwBF0uM
tags:
  - cover
draft: false
---

Using plop will save time and energy as well as ensure consistency (eg. no error because I accidentally write tag instead of tags).

Step 1: Install plop and prepare the directory

First, I add plop by running yarn add plop in my project directory and install plop globally by running npm install -g plop. I go through the docs on plop’s Github repo briefly to get the idea about their API.

Although I use gatsby-starter-typescript-plus to create my site, here I am referring to another starter, gatsby-starter by fabien0102, which has existing plop generators. So I’m not completely starting from scratch.

I begin by looking at the generators directory content.

  ├── generators                    // generators (`npm run generate`)
  │   ├── blog-post-generator.js    // `blog post` generator
  │   ├── component-generator.js    // `component` generator
  │   ├── page-generator.js         // `page` generator
  │   ├── plopfile.js               // generators entry
  │   ├── templates                 // all templates (handlebar notation)
  │   └── utils.js                  // utils scripts for generators
plop generator folder structure in the starter’s README

For each generator, I should have a generator file (eg. blog-post-generator.js) and a corresponding template file (eg. templates/blog-post-md.template). This starter has a plopfile.js that serves as an index that loads and exports each generator from the aforementioned files; also utils.js that contains helper functions.

Other than setGenerator, I’m not sure how these all work yet, but I’m going to copy and implement these files one by one to my site to see them in practice.

Step 2: Prepare the folder, plopfile, and helper

I create an empty generators folder in my project root. I copy plopfile.js from the reference starter, changing generator name with my own. I’m starting with the “track generator” first.

// generators/plopfile.js
module.exports = plop => {
    plop.load('./track-generator.js')
}

The original utils.js file consists of two helper functions: inputRequired and addWithCustomData. I’m not generating complex components that require sample data, so I’m just going to copy the former into my utils.js.

// generators/utils.js
const inputRequired = name => {
    return value => (/.+/.test(value) ? true : `${name} is required`)
}
module.exports = { inputRequired }

Step 3: Make a generator!

A generator is created with the setGenerator method that takes an optional description and a config object. The config object consists of prompts and actions arrays.

I’m making a generator with the description “track entry”.

// generators/track-generator.js
const { inputRequired } = require('./utils')

module.exports = plop => {
    plop.setGenerator('track entry', {
        prompts: [], // empty for now
        actions: [], // empty for now
    })
}

Step 4: Ask questions (prompts)

The prompts array contains objects that represent questions to ask the user. For example, I want my “track” generator to ask six questions:

  1. Track title
  2. Track artist
  3. URL to the track (on Spotify, Youtube, etc)
  4. Tags
  5. Body
  6. Draft (create post, but don’t publish)

Next, I’m populating prompts with corresponding question objects.

// generators/track-generator.js
// (truncated to `prompts`)
module.exports = plop => {
    plop.setGenerator('track entry', {
        prompts: [
            // question 1
            {
                type: 'input',
                name: 'title',
                message: ' f',
                validate: inputRequired('title')
            },
            // question 2
            {
                type: 'input',
                name: 'artist',
                message: 'Track artist?',
                validate: inputRequired('artist')
            },
            // question 3
            {
                type: 'input',
                name: 'url',
                message: 'Track URL?'
            },
            // question 4
            {
                type: 'input',
                name: 'tags',
                message: 'Tags? (separate with comma)'
            },
            // question 5
            {
                type: 'input',
                name: 'body',
                message: 'Body text?'
            },
            // question 6
            {
                type: 'confirm',
                name: 'draft',
                message: 'Save as draft?',
                default: false
            }
        ], 
    })
}

Plop uses inquirer.js for the question object. Let’s have a closer look at the object keys.

  • type refers to the prompt type. I use input type to get text input for questions 1 to 5, and confirm type to get boolean (true/false) input for question 6. If you want a multiple choice like the (fictional) cover image at the top of this post, use the list type.

  • name is used as variable to store the input. I use the name, eg. title, to store the data to be returned and displayed in the template file.

  • message is the message displayed in the command line. For example, I’m printing the message “Track title?” when asking for the title data.

  • validate is a function that returns either true or an error message. I use the inputRequired function in utils.js, which ensures the question is answered (not blank), for the two required fields, title and artist.

  • default is self-explanatory. I use it for draft, because I want to publish the post by default.

You can read the complete specs in Inquirer.js docs here.

Now I am running the generator by typing plop --plopfile ./generators/plopfile.js in my project directory

Command Line Interface showing questions: Track title, artist, URL, tags, body text, and save as draft

Running my “track generator” in the CLI (kindly ignore the typo 🙈)

It works as intended, but it does not do anything yet. Let’s populate the actions now!

Step 5: Do things (actions)

The actions property can be an array containing ActionConfig object; or we could have a dynamic actions array as “a function that takes the answers data as a parameter and returns the actions array”.

The gatsby-starter generator does the latter: run a function with user input as data. This function does two things: automatically populate the date frontmatter field using new Date() (one fewer thing to type manually!), and parse the tags as YAML array.

Finally, it returns the actions array to add a file using the specified template, file name, in specified directory. Other than changing the path and templateFile, I don’t make other modifications here.

// generators/track-generator.js
// (truncated to `actions`)
module.exports = plop => {
    plop.setGenerator('track entry', {
        actions: data => {
            // Get current date
            data.date = new Date().toISOString().split('T')[0]

            // Parse tags as yaml array
            if (data.tags) {
                data.tags = `tags:\n  - ${data.tags.split(',').join('\n  - ')}`
            }

            // Add the file
            return [
                {
                    type: 'add',
                    path: '../src/content/tracks/{{date}}--{{dashCase title}}.md',
                    templateFile: 'templates/track-md.template'
                }
            ]
        }
    })
}

You might notice dashCase, which is part of plop's helpful built-in-helpers.

Step 6: Make the template

Next, I’m creating a template file called track-md.template in the templates directory. This is a straightforward file that resembles the Markdown file structure.

---
title: {{title}}
artist: {{artist}}
date: "{{date}}"
url: {{url}}
{{tags}}
draft: {{draft}}
---

{{body}}

If you are wondering about the lack of tags: in the frontmatter, that string is returned as part of data.tags object in the actions function above.

I go back to the command line and repeat the same process as before, run plop --plopfile ./generators/plopfile.js and answer the questions. Now, after answering all the questions, I got this message informing that the file has been created in my contents/tracks folder.

Command Line Interface showing file 2019-01-27-on-the-sunshine.md successfully added

Success message after running my track generator in the command line

I open the file 2019-01-27—on-the-sunshine.md and voila, it is populated with the data I input from the command line.

---
title: On the Sunshine
artist: Spiritualized
date: "2019-01-27"
url: https://open.spotify.com/track/6xALY6wGGzQZl36A3ATnFq?si=lUwasuJmQbaWZOQsxg2G2Q
tags:
  - test
draft: false
---

> And in the evening / Take it easy / You can always do tomorrow / What you cannot do today

One minor issue is the > character, which creates blockquote in Markdown, is escaped into HTML >. I made several attempts to fix it, such as checking the docs for hints, running .replace() and .unscape(), all to no avail.

I found the solution in this issue, which turns out to be Handlebar-specific rather than plop or JS one. In order to avoid Handlebars’ HTML escape, we use “triple stash” ({{{body}}}) instead of double. I also use it for the url field so the special characters do not get encoded. I reran the code and it works perfectly.

Bonus: Make a shortcut

Typing plop --plopfile ./generators/plopfile.js every time is tedious and hard to remember; let’s make a shortcut in package.json.

// package.json
{
    "scripts": {
        "generate": "plop --plopfile ./generators/plopfile.js",
        "generate-build": "plop --plopfile ./generators/plopfile.js && gatsby build"
    }
}

I can run the generator by typing yarn generate. If I’m writing a short post that I want to publish immediately (without eg. checking or editing), I can run yarn generate-build, which will run Gatsby build command after I input all the data.

Wishlist

Other things I want to try in the future with plop:

  • Automate creating a component (eg. component file, corresponding style file, Storybook .stories.js file, readme, test files, etc). It already exists in this starter site, but I’m not able to explore it now.
  • Use Custom Action Function to save image file from an external URL into local path to use in a Markdown frontmatter. Just an idea; not sure if it’s possible, btw.

Thank you for reading, until next time!

Discussion

pic
Editor guide