Adding generators to your Gatsby site with plop
Tags: react, gatsby, plop, beginners
Published: 2019-01-27 11:02:39

---
title: Adding generators to your Gatsby site with plop
published: true
description: 
tags: react, gatsby, plop, beginners
cover_image: https://thepracticaldev.s3.amazonaws.com/i/pmu90hsx0l5hvp9bm6uu.jpg
---

In this post, I’m going to integrate [plop](https://github.com/amwmedia/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:

```markdown
------
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](https://github.com/amwmedia/plop) briefly to get the idea about their API.

Although I use [gatsby-starter-typescript-plus](https://github.com/resir014/gatsby-starter-typescript-plus) to create my site, here I am referring to another starter, [gatsby-starter](https://github.com/fabien0102/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.

```md
  ├── 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
```
<figcaption>plop generator folder structure in the starter’s README</figcaption>

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.

```js
// 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`.

```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](https://github.com/amwmedia/plop#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”.

```js
// 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. 

```js
// 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](https://github.com/SBoudrias/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](https://github.com/SBoudrias/Inquirer.js/#question). 

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](https://thepracticaldev.s3.amazonaws.com/i/atxu2f611ifnlq3jo5h3.png)
<figcaption>Running my “track generator” in the CLI (kindly ignore the typo 🙈)</figcaption>

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](https://github.com/amwmedia/plop#interface-actionconfig) object; or we could have a [dynamic actions array](https://github.com/amwmedia/plop#using-a-dynamic-actions-array) as “a function that takes the answers data as a parameter and returns the actions array”.

The [gatsby-starter generator](https://github.com/fabien0102/gatsby-starter/blob/master/generators/blog-post-generator.js#L32) 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.

```js
// 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](https://github.com/amwmedia/plop#case-modifiers).


## 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.

```md
---
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](https://thepracticaldev.s3.amazonaws.com/i/t19brqmu0x7f555b65l9.png)
<figcaption>Success message after running my track generator in the command line</figcaption>

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.

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

&gt; 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](https://daringfireball.net/projects/markdown/syntax#blockquote), is escaped into HTML `&gt;`. I made several attempts to fix it, such as checking the docs for hints, running [.replace()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace) and [.unscape()](https://lodash.com/docs/#unescape), all to no avail. 

I found the solution in [this issue](https://github.com/amwmedia/plop/issues/142), 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`. 

```js
// 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](https://github.com/amwmedia/plop#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!