Author’s Note: I’m currently in the process of migrating old blog posts to this new system. That may mean some links, syntax highlighting, and other details are broken or missing temporarily. Sorry for the inconvenience!

In the last post, we added a little bit of content to the top of our blog posts:

---
version: 0.1.0
---

A very small addition, but this allowed our deploy script to automatically detect that it should copy in some of the initial <html> junk, since we didn’t want to have to copy-and-paste it at the beginning of each post. But it didn’t eliminate all the work, because we still have various <meta> tags whose content changes from article to article.

Today, I want to expand that header at the top of our post, and build ourselves a more dynamic template from scratch.

Front matter

The term “front matter” comes from (as so many blog things do) the world of book publishing. Crack open your nearest dusty tome, and you’re liable to find a title page, and a foreword, and a preface, and all of that lovely stuff. The term has been repurposed by some tools called “static site generators” to describe the data that goes in our post before the actual content.

The --- format is one pretty common way of doing that with software that processes Markdown. The key: value format between the lines is usually an example of YAML code (YAML stands for Yet Another Markup Language, or YAML Ain’t Markup Language, depending on who you ask). But we’re already focusing on HTML, CSS, Markdown, shell scripts…I really don’t want to add another language to the mix just yet. So we’ll just support the key: value syntax without worrying about whether or not it matches that language just yet. Here’s what we’ll use for our front matter on this post:

---
version: 0.2.0
title: Building Custom Templates
description: Parsing our own front matter for fun and profit!
url: https://cheerskevin.com/building-custom-templates.html
---

This provides enough data that we should be able to fill out our <meta> tags, and also bumps the minor version number, so we can make sure we don’t assume the data will be there on older posts.

A specification for our template

We’re going to write a bash script that should be able to take a filename (our blog post), and return back the full HTML, including all of our fancy templated goodness. Let’s imagine we have a very simple Markdown blog post: example-post.md

---
version: 0.2.0
title: Example Post
description: This is an example
url: https://cheerskevin.com/example-post.html
---

# Example Header
This is an example post

We should expect our script to generate the following output:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="twitter:card" content="summary">
<meta name="twitter:site" content="@cheerskevin">
<meta property="og:type" content="article">
<meta name="twitter:title" content="Example Post">
<meta property="og:title" content="Example Post">
<meta name="twitter:description" content="This is an example">
<meta name="description" content="This is an example">
<meta property="og:description" content="This is an example">
<meta property="og:url" content="https://cheerskevin.com/example-post.html">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Example Post</title>
<style>
html {
  background: #666;
  overflow-x: hidden;
}
body {
  background: #fff;
  margin: 10px auto;
  max-width: 960px;
  overflow-x: hidden;
  padding: 5px 10px;
  width: 90%;
}
</style>
</head>
<body>
<h1>Example Header</h1>

<p>This is an example post</p>
</body></html>

Some of the content needs to be automatically inserted (<!DOCTYPE html>), some of it is generated from our front matter (<meta name="description" content="This is an example" />), and some of it just the conversion of our content from Markdown to HTML (<p>This is an example post</p>).

Now let’s go about writing a bash script to do just that.

Getting the variables

Bash variables start with a $ sign. We’re going to need a few of those - we’ll need to extract out data from the front matter and assign it to variables, so we can inject it back in to the final template. So let’s start by creating a script capable of extracting the values from our front matter:

#!/bin/bash
FILENAME=$1
FRONT_MATTER=$(cat $FILENAME | sed -n '/^---/,/^---/p')

VERSION=$(echo "$FRONT_MATTER" | grep version: | sed 's/.*: //')
TITLE=$(echo "$FRONT_MATTER" | grep title: | sed 's/.*: //')
DESCRIPTION=$(echo "$FRONT_MATTER" | grep description: | sed 's/.*: //')
URL=$(echo "$FRONT_MATTER" | grep url: | sed 's/.*: //')

echo "Filename is $FILENAME"
echo "Front matter is $FRONT_MATTER"
echo "Version is $VERSION"
echo "Title is $TITLE"
echo "Description is $DESCRIPTION"
echo "Url is $URL"

We start by extracting just the parts between the --- lines, and then individually pattern-match for the items we know we’re going to need. Run this against our example post, and we get the following output:

Filename is example-post.md
Front matter is ---
version: 0.2.0
title: Example Post
description: This is an example
url: https://cheerskevin.com/example-post.html
---
Version is 0.2.0
Title is Example Post
Description is This is an example
Url is https://cheerskevin.com/example-post.html

Plug it in, plug it in

Believe it or not…we’re pretty much done. All that remains is to echo out our template, using $VARIABLES where the injected content should go:

#!/bin/bash
FILENAME=$1
FRONT_MATTER=$(cat $FILENAME | sed -n '/^---/,/^---/p')

VERSION=$(echo "$FRONT_MATTER" | grep version: | sed 's/.*: //')
TITLE=$(echo "$FRONT_MATTER" | grep title: | sed 's/.*: //')
DESCRIPTION=$(echo "$FRONT_MATTER" | grep description: | sed 's/.*: //')
URL=$(echo "$FRONT_MATTER" | grep url: | sed 's/.*: //')

echo "
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta name='twitter:card' content='summary'>
<meta name='twitter:site' content='@cheerskevin'>
<meta property='og:type' content='article'>
<meta name='twitter:title' content='$TITLE'>
<meta property='og:title' content='$TITLE'>
<meta name='twitter:description' content='$DESCRIPTION'>
<meta name='description' content='$DESCRIPTION'>
<meta property='og:description' content='$DESCRIPTION'>
<meta property='og:url' content='$URL'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<title>$TITLE</title>
<style>
html {
  background: #666;
  overflow-x: hidden;
}
body {
  background: #fff;
  margin: 10px auto;
  max-width: 960px;
  overflow-x: hidden;
  padding: 5px 10px;
  width: 90%;
}
</style>
</head>
<body>
$(cat $FILENAME | sed '1,/---/d' | markdown)
</body></html>
"

Note that we have the Markdown generation actually embedded in the template now. We just strip out the front matter before processing, so it can inject the result right into our template. Running ./v0.2.0.sh example-post.md generates our HTML nice and clean.

Make new friends, but keep the old

We have a deploy.sh script that gets run whenever a file change is detected, which we modified last time to handle the start of our templates.

#!/bin/bash
case "${FILENAME##*\.}" in
  md)
    if head -n 1 $FILENAME | grep -q '\-\-\-'; then
      cat templates/prefix.html > "${FILENAME%.*}.html"
      cat $FILENAME | sed '1,/---/d' | markdown >> "${FILENAME%.*}.html"
      cat templates/suffix.html >> "${FILENAME%.*}.html"
    else
      markdown $FILENAME > "${FILENAME%.*}.html"
    fi
  ;;
esac
rsync -ru --del . cheerskevin.com:/var/www/cheerskevin.com/

Right now, it distinguishes between Markdown files that start with ---, and those that do not (opting not to paste the prefix.html and suffix.html contents in the posts that don’t contain ---). But now we need to distinguish between different versions (thank goodness we added a v0.1.0 to our last post!). We can extract the version just like we did in our template, and then do a case-analysis to decide how to process it:

#!/bin/bash
case "${FILENAME##*\.}" in
  md)
    if head -n 1 $FILENAME | grep -q '\-\-\-'; then
      VERSION=$(cat $FILENAME | sed -n '/^---/,/^---/p' | grep version: | sed 's/.*: //')
      case "$VERSION" in
        "0.1.0" )
          cat templates/prefix.html > "${FILENAME%.*}.html"
          cat $FILENAME | sed '1,/---/d' | markdown >> "${FILENAME%.*}.html"
          cat templates/suffix.html >> "${FILENAME%.*}.html"
        ;;
        "0.2.0" )
          ./templates/v0.2.0.sh $FILENAME > "${FILENAME%.*}.html"
        ;;
      esac
    else
      # Contains no front matter, process as self-contained Markdown
      markdown $FILENAME > "${FILENAME%.*}.html"
    fi
  ;;
esac
rsync -ru --del . cheerskevin.com:/var/www/cheerskevin.com/

Now we have support for posts that have no front matter, posts that have v0.1.0 front matter, and our new front matter. But wait! We can do even better! Let’s extract out the v0.1.0 handling to its own file under templates/

#!/bin/bash
FILENAME=$1
cat templates/prefix.html
cat $FILENAME | sed '1,/---/d' | markdown
cat templates/suffix.html

Then we can clean up our version logic, by using the $VERSION variable to determine the name of the script to run!

#!/bin/bash
case "${FILENAME##*\.}" in
  md)
    if head -n 1 $FILENAME | grep -q '\-\-\-'; then
      VERSION=$(cat $FILENAME | sed -n '/^---/,/^---/p' | grep version: | sed 's/.*: //')
      ./templates/v$VERSION.sh $FILENAME > "${FILENAME%.*}.html"
    else
      # Contains no front matter, process as self-contained Markdown
      markdown $FILENAME > "${FILENAME%.*}.html"
    fi
  ;;
esac
rsync -ru --del . cheerskevin.com:/var/www/cheerskevin.com/

We’re automatically assuming we should use ./templates/v0.2.0.sh to process this post now, because that’s the version in the front matter. As we make further improvements on this site, we won’t need to go back to the deploy script, we can just create new templates and use a new version in the front matter to match!

Wrapping it up

This post has been a bit of a doozy, but let’s recap what we’ve done:

  • We talked a bit about the post’s front matter. How we can provide the basic details of the post in one location, so they can be used in a bunch of locations.
  • We started considering our new templating system by taking a simple case, and defining what the input should be (our Markdown with front matter), and what the output should be (the HTML with values injected). This is often a very helpful exercise to make sure we don’t overengineer solutions.
  • We learned to extract the contents of our front matter into variables (we can be cleverer about this, but it’ll do for now), and how to reinject them into our templates
  • We found a way to select how to handle the Markdown files based on the version numbers, allowing us to move forward with ease!

I wanted to get this bit out of the way, because we really need to start looking at some of the page stylesheets and working to make the site a bit easier on the eyes. It’s tricky to do that while having to copy and paste our styles and attributes from post to post. Now, when we make improvements, we can bump the version number, and just improve upon our template. So look forward to that in the near future!

←Previous Post | Next Post→