A script to create a post

Let's start by outlining what create-post will do:

Step 1: import a bunch of libraries

I already know I'm going to use the type definitions and the checkType function. I'm going to need a template to take a post and render a Markdown file with a Frontmatter header, so I'll add a function for that in the /templates/ directory. JS template literals make this easy:

// /templates/markdown.ts

import { Post } from '../src/types';

export function markdownPostTemplate(post: Post): string {

return `---
title: "${post.title}"
date: "${post.date}"
filename: ${post.filename}
status: ${post.status}
author_uid: ${post.author_uid}
slug: ${post.slug}
guid: ${post.guid}
thumbnail_image: ${post.thumbnail_image || '' }
opengraph_image: ${post.opengraph_image || '' }
tags: ${post.tags || '' }
excerpt: "${post.excerpt || '' }"

---

${post.content || 'Write a blog post here'}
`;
}

I'm going to want to handle show-stopping fatal errors, so I'll write a small function that displays a message and exits the Node process. (This is mostly because I'm lazy and would rather not write these two lines over and over.)

// /src/die.ts
export function die(message: string) {
    console.error(`ERROR: ${message}`);
    process.exit();
}

Now I can import these two functions along with the Post and Blog types, the checkType utility, and the contents of the blog-config.json file.

import { checkType } from './src/checkType';
import { Post, Blog } from './src/types';
import { die } from './src';
import { markdownPostTemplate } from './templates/markdown';

const blog: Blog = require('../blog-config.json');

Next up I'll pull in some NPM libraries: minimist for processing command-line arguments, and user-friendly-date-formatter for managing the hell of date handling. I'll also import Node's fs (file system) library for writing the Markdown files to disk.

Finally I'll use checkType to make sure the data loaded from blog-config.json conforms to the Blog type.

const minimist = require('minimist');
const df = require('user-friendly-date-formatter');
const fs = require('fs');

checkType(blog, 'Blog');
console.log('Blog config loaded and validated.');

Step 2: get title and date parameters

Minimist might be a little heavyweight for what I need, but it sure makes getting arguments easy. I'm including short, one-letter versions of each argument and just grabbing the first one supplied:

let args = minimist(process.argv.slice(2), {
    string: [
        'title', 't', 'date', 'd'
    ],
});

const title = args.title || args.t;
const dateArg = args.date || args.d;

if (!title || title.length === 0) {
    die('No title parameter supplied.');
}

The date parameter is optional. If supplied, the app will try to create a post for the given date. Otherwise, it'll use the current date. The Node Date object can parse date strings, so this works so long as you don't give it something invalid.

let date;
if (dateArg && dateArg.length > 0) {
    date = new Date(dateArg);
    if (isNaN(date.valueOf())) {
        die(`Can't use date: ${dateArg}`);
    }
} else {
    date = new Date();
}

That's all the input we need. On to the scary stuff.

Step 3: create an empty post

My basic idea is to take the title and date provided above and pass it to a single function that will write out an empty post file:

createEmptyPost(title, date);

Before I can do that, I'll need a few things:

First, a function that creates and returns the post slug. To do this, I'll want to:

Uh, so:

function createTitleSlug(title: string): string {
    return title.replace(/\s/gi, '_')
                .replace(/[^a-z0-9_]/gi, '')
                .toLowerCase()
                .slice(0, 50);
}

Next, a function that takes the slug and date, and creates a filename:

function createFileName(titleSlug: string, date: Date): string {
    const formattedDate = df(date, '%YYYY-%MM-%DD_%H-%m-%s-%l');
    return `${formattedDate}_${titleSlug}.md`;
}

Then a function that creates a file path to be used as a permalink. My original plan was to configure this path using the archive_format field of blog-config.json but decided nah, archives are daily, because I say so.

function createArchivePath(blog: Blog, titleSlug: string, date: Date): string {
    const format = '%YYYY/%MM/%DD';
    const dateFragment = df(date, format);
    return `/${blog.root}/${dateFragment}/${titleSlug}/`;
}

Finally, the function that writes the Markdown file using the markdownPostTemplate from above. This function will create the /posts/ directory if it doesn't exist. The "wx" flag passed to writeFileSync should ensure the operation fails if a file with the same name already exists.

function createPostFile(filename: string, post: Post) {
    if (!fs.existsSync('./posts')){
        fs.mkdirSync('./posts');
    }
    const content = markdownPostTemplate(post);
    try {
        fs.writeFileSync(`./posts/${filename}`, content, { flag: 'wx' });
    } catch (err) {
        die(`Could not write Markdown file: ${err.message}`);
    }
}

Now that we have all the helper functions, here's the body of createEmptyPost:

function createEmptyPost(title: string, date: Date) {

    const titleSlug = createTitleSlug(title);
    const filename = createFileName(titleSlug, date)
    const postLink = createArchivePath(blog, titleSlug, date);

    const postDate = df(date, '%YYYY-%MM-%DD %H:%m:%s');

    const post: Post = {
        title: title,
        date: postDate,
        filename: filename,
        author_uid: blog.authors[0].author_uid,
        status: 'publish',
        slug: titleSlug,
        guid: postLink
    };

    createPostFile(filename, post);
    console.log(`Created file for post: ${post.title}`);
}

Okay! So, after compiling all of this with tsc, I can run:

node ./build/create-post.js -t "A script to create a post" -d "2019-12-16 09:00:00"

Did it work?

% ls posts
2019-12-03_22-10-19-000_hello_world.md
2019-12-04_20-00-00-000_lets_start_with_some_types.md
2019-12-04_20-00-00-001_baby_step_okay_lets_write_some_config_stuff.md
2019-12-04_20-00-00-002_adventures_in_js_type_checking.md
2019-12-05_17-26-40-406_some_admin_notes.md
2019-12-10_22-15-55-728_we_have_ignition.md
2019-12-11_22-10-19-020_baby_step_some_environment_notes.md
2019-12-16_9-00-00-000_a_script_to_create_a_post.md # <- here it is!
%

And the contents?

---
title: "A script to create a post"
date: 2019-12-16 9:00:00
filename: 2019-12-16_9-00-00-000_a_script_to_create_a_post.md
status: publish
author_uid: scottandrew
slug: a_script_to_create_a_post
guid: /posts/2019/12/16/a_script_to_create_a_post/
thumbnail_image: 
opengraph_image: 
tags: 
excerpt: ""
---

Write a blog post here

How about a missing title?

% node ./build/create-post.js
Blog config loaded and validated.
ERROR: No title parameter supplied.
%

An invalid date?

% node ./build/create-post.js -t "A new post" -d "2019-12-32"
Blog config loaded and validated.
ERROR: Can't use date: 2019-12-32
%

A non-unique filename?

% node ./build/create-post.js -t "A script to create a post" -d "2019-12-16 09:00:00"
Blog config loaded and validated.
ERROR: Could not write Markdown file: EEXIST: file already exists, open './posts/2019-12-16_9-00-00-000_a_script_to_create_a_post.md'
%

So far so good! But there are problems, which I'll write about next.


Posted in: blogging, code, node, javascript

Previously: Baby step: some environment notes

Next: Is it safe?