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.
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:
- Track title
- Track artist
- URL to the track (on Spotify, Youtube, etc)
- Tags
- Body
- 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, andconfirm
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 thelist
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 theinputRequired
function inutils.js
, which ensures the question is answered (not blank), for the two required fields,title
andartist
.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
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.
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!
Top comments (0)