Hackernoon logoHow I Automate my Blog Writing with Python - Part 1: Post Starters by@waylon-walker

How I Automate my Blog Writing with Python - Part 1: Post Starters

Waylon Walker Hacker Noon profile picture

@waylon-walkerWaylon Walker

One thing I dread about writing blog posts is getting started. I need to cd into my blog directory, start neovim in a markdown file with a clever name, copy some frontmatter boilerplate, update the post date, add tags, a description, and a cover.

To-do List for starting a post :

  • frontmatter template
  • Title
  • slug
  • tags
  • date
  • cover
  • description
  • create markdown file
  • open in neovim

Lets Automate this 

This ain't no proper cli 

As with many thing running behind the scenes on this site, I am the one and only user, I have limited time, so this is going to be a bit hot and fast. Let's create a file called new-post.

start the script new-post

!#python
# new-post
👆 Works on my machine

If this were something that had more users than me I would probably use something like click, but for this I want to get it done quick and avoid any need to manage dependencies. Be careful if you were to share something with a #!python as it requires the end user to have the right version of python ready to go.

Title 

The title can't really be automated this is the core idea coming out of my 🧠, but it will be captured through the cli and put into proper position. For this I'm going super simple and just pulling it out of sys.argv

set the title

import sys

title = sys.argv[1].title()
! sys.argv is a list of each argument passed into the script split by spaces.

slug 

The slug is what I am calling the route and can simply come out of the title automatically, if I want to shorten it later by hand that will be simple enough to do manually. All that needs to be done is to lowercase and replace a few characters with -.

set the slug

slug = title.lower)(.replace(" ", "-".replace()"_", -"")""))

tags 

For tags I decided I wanted the parser to be as simple as possible and didnt want to dance around any flags. I am simply just going to look at every argument passed into the command and see if any of them contain one of my common tags.

parse the tags

args = ''.join(sys.argv[1:])
tags = []

if 'py' in args:
    tags.append('python')

if 'web' in args:
    tags.append('webdev')

if 'blog' in args:
    tags.append('blog')

if 'data' in args:
    tags.append('data')

🤷‍♂️ antipattern?? The above section does an initialize then modify. I generally try to avoid this pattern with something like a list comprehension, but didn't see an obvious solution so I just went with it.

Frontmatter Template 

Now we have enough information going to assemble the frontmatter I use for my posts. I am going to just insert the values I need into an f-string. Since python 3.6 was released f-strings are my go to templating tool.

create the markdown

import datetime

frontmatter = f"""---
templateKey: blog-post
tags: {tags}
title: {title}
date: {datetime.date.today().strftime('%Y-%m-%dT%H:%M:%S')}
status: draft
description: ''
cover: "/static/{slug}.png"

---

"""

create markdown file 

Now its time to get down to business and make the post. First I want to throw an error if the post already exists, I definitely don't want to blow away an existing post if a certain slug is already taken. I am a big fan of custom error messages and I am going to go ahead and make one here, even though this is just a quick script.

custom error

class PostExistsError(FileExistsError):
    pass

I am a pathlib superfan. It's going to make setting up these paths super simple. Note I am going to anchor my directory down with the __file__ variable. I do this all the time to get paths relative to the module that is currently running.

setup paths

directory = pathlib.Path(__file__).parent
path = pathlib.Path(f"{directory}/src/pages/blog/{slug}.md")

if path.exists():
    raise PostExistsError(f"Post Already exists at {path}")
file is a string that represents the path to the running module

Finally just write the file. Here we open the file with a context manager so that we don't have to worry about closing it when we are done. Note that we open it with the w+ flag for write and creation.

write the file

with open(path, "w+") as f:
    f.write(frontmatter)

git add 

I am not quite ready to pull the trigger on doing an auto commit, but this may happen in a future version. For now I want this file easily picked up by vims :GFiles since I have that is one of my most used hot keys. To do this the file at least needs added. I'm sure there is a better way to do this with a Git library, but I am used to the command line so I am going to just run a subprocess.

I am using the subprocess.Popen command since its what I am used to, note that it will run the task in the background so be sure that you wait on it. The Popen is great if you have several task that are not dependent on each other.

git add

gadd = subprocess.Popen(
    f'cd {directory} && git add {str(path).replace(str(directory) + "/", "")} ',
    shell=True,
)
gadd.wait()

open in neovim 

Last step of the script is to start writing, I want to be open in my blogs directory (hence the cd), with the file open, to the right line (+11), and in insert mode (+star).

open post in neovim

nvim = subprocess.Popen(
    f'cd {directory} && nvim +12 +star {str(path).replace(str(directory) + "/", "")} ',
    shell=True,
)
nvim.wait()

Alias 

Now I want this script to be available everywhere. I am going to simply add the following entry to shorten the script and eliminate the need to use the full path. I added this to my ~/.alias, for you it may be ~/.bashrc, or ~/.zshrc.

alias np=~/git/waylonwalkerv2/new-post

Starting a new post 

Lets start a new post about automating my posts in python.

np "automating my posts" python

This is my workflow 

Ad hoc scripts like this can be a bit of a hot mess, partly due to the just get it done nature, but also due to the fact that I am just riffing off the top of my head and utilizing docs as least as possible.

While writing the script I would run it after each section or so and print some results to make sure they were looking good. If I ever needed access to a live variable I would pop open ipython and run %run new-post "my-new-post" and inspecting it.

Final Script 

final script

#!python
# new-post

import sys
import datetime
import pathlib
import subprocess


title = sys.argv[1].titlecase()
args = "".join(sys.argv[1:])
tags = []

if "py" in args:
    tags.append("python")

if "web" in args:
    tags.append("webdev")

if "blog" in args:
    tags.append("blog")

if "data" in args:
    tags.append("data")

slug = title.lower().replace(" ", "-").replace("_", "-")
frontmatter = f"""---
templateKey: blog-post
tags: {tags}
title: {title}
date: {datetime.date.today().strftime('%Y-%m-%dT%H:%M:%S')}
status: draft
description: ''
cover: "/static/{slug}.png"

---


"""


class PostExistsError(FileExistsError):
    pass


directory = pathlib.Path(__file__).parent
path = pathlib.Path(f"{directory}/src/pages/blog/{slug}.md")

if path.exists():
    raise PostExistsError(f"Post Already exists at {path}")

with open(path, "w+") as f:
    f.write(frontmatter)

gadd = subprocess.Popen(
    f'cd {directory} && git add {str(path).replace(str(directory) + "/", "")} ',
    shell=True,
)
gadd.wait()

nvim = subprocess.Popen(
    f'cd {directory} && nvim +12 +star {str(path).replace(str(directory) + "/", "")} ',
    shell=True,
)
nvim.wait()

How to Quickly Edit Posts Through Automation

check out the next article in this series where I setup a bash function to quickly edit these posts

read more https://waylonwalker.com/quickly-edit-posts

Previously published at https://waylonwalker.com/automating-my-post-starter

Tags

Join Hacker Noon

Create your free account to unlock your custom reading experience.