Asif Rahman

Static site build script

This little shell script compiles a folder of markdown files into HTML files using Pandoc.

First it preprocesses markdown files as Mustache templates. This lets you use variables in your markdown files that are defined in a metadata.yaml file or the frontmatter. The script then uses Pandoc to convert the markdown files to standalone HTML files.

Usage

Save the source to a file named ./dev and make the script executable:

chmod +x ./dev

On MacOS run the setup command to install the required dependencies:

./dev setup

To build the site run:

./dev build

To force a full rebuild run:

./dev build -F

To build a specific file run:

./dev build content/notes.md

If you have caddy and npx installed, you can run a local server and watch for changes:

./dev run

Source

#!/bin/bash
# Usage:
#   chmod +x dev
#   ./dev [COMMAND]

set -e

# Files modified in the last 30 minutes will be rebuilt
MMIN=30

ERROR='\033[0;31m'
SUCCESS='\033[0;32m'
CODE='\033[0;36m'
NC='\033[0m' # No Color

cmd_helps=()

defhelp() {
  local command="${1?}"
  local text="${2?}"
  local help_str
  help_str="$(printf '   %-24s %s' "$command" "$text")"
  cmd_helps+=("$help_str")
}

# Print out help information
cmd_help() {
  echo "Script for performing dev tasks."
  echo
  echo "Usage: ./dev [COMMAND]"
  echo "Replace [COMMAND] with a word from the list below."
  echo
  echo "COMMAND list:"
  for str in "${cmd_helps[@]}"; do
    echo -e "$str"
  done
}
defhelp help 'View all help.'

# ------------------------------------------------------------------------------
# Repo
# ------------------------------------------------------------------------------

cmd_clean() {
    echo "Cleaning up..."
    rm -f public/*.html
    rm -f public/*.xml
}
defhelp clean 'Clean up.'

cmd_setup() {
    echo "Setting up..."
    # check if jq is installed
    if ! command -v jq &> /dev/null; then
        echo "Installing jq..."
        brew install jq
    fi

    # check if pandoc is installed
    if ! command -v pandoc &> /dev/null; then
        echo "Installing pandoc..."
        brew install pandoc
    fi

    # check if mustache is installed
    if ! command -v mustache &> /dev/null; then
        echo "Installing mustache..."
        go install github.com/cbroglie/mustache/cmd/mustache@latest
    fi
}

# Build a file or all files, optionally force a full rebuild
# Build all files that have changed in the last 30 days
# ./dex build
# Build all files
# ./dex build -F
# Build a specific file
# ./dex build file.md
cmd_build() {
    echo "Building..."

    REBUILD=0

    # Check if given -F flag to force a full rebuild
    # ignore the flag if it is not given
    while getopts "F" opt; do
        case ${opt} in
            F)
                REBUILD=1
                ;;
            \?)
                # ignore unknown flags
                ;;
        esac
    done

    # check if any files in templates/ have changed in the last 30 minutes, if so, force a full rebuild
    if [ $REBUILD -eq 0 ]; then
        if [ $(find templates -type f -mmin -$MMIN | wc -l) -gt 0 ]; then
            echo "Templates have changed, forcing a full rebuild..."
            REBUILD=1
        fi
    fi

    # Markdown extension (e.g. md, markdown, mdown).
    MEXT="md"

    # if rebuild=0 and a file name is given, build that file
    if [ $REBUILD -eq 0 ] && [ $# -eq 1 ]; then
        FILES="$1"
    fi

    # Only check for files if FILES is not set
    if [ -z "$FILES" ]; then
        # get all markdown files that have changed in the last 30 minutes if not forcing a full rebuild
        # otherwise, get all markdown files
        if [ $REBUILD -eq 0 ]; then
            echo "Incremental build..."
            FILES=$(find content -type f -name "*.$MEXT" -mmin -$MMIN)
        else
            echo "Full build..."
            FILES=$(find content -type f -name "*.$MEXT")
        fi
    fi

    # if there are no files, exit
    if [ -z "$FILES" ]; then
        echo "${ERROR}No files to process!${NC}"
        exit 0
    fi

    # Location of the root directory with this Makefile, templates/, content/, public/
    ROOT=$(pwd)

    echo "Files to process:"
    echo "---"
    echo "$FILES"
    echo "---"

    # build each file
    for file in $FILES; do
        echo "Building: $file"
        # get the file name without the extension
        FILENAME=$(basename -- "$file")
        FILENAME="${FILENAME%.*}"

        # gather yaml front matter from the file if it exists using sed and awk
        frontmatter=$(awk '
        # Start capturing when we find the opening --- line
        /^---$/ { if (capture) exit; capture=1; next }
        # Print lines only if capture is active
        capture { print }
        ' < "$file")

        # strip leading and trailing --- from the frontmatter and add it back
        # we do this as a sanity check in case the file does not have frontmatter properly formatted
        frontmatter=$(echo "$frontmatter" | sed 's/^---//' | sed 's/---$//')
        frontmatter=$(echo $'\n'"---"$'\n'"$frontmatter"$'\n'"---")

        # collect all context data in one place
        context=$(cat content/metadata.yaml <(echo "$frontmatter"))

        # preprocess the markdown file with mustache, use the frontmatter and metadata.yaml as context
        # cat the frontmatter and metadata.yaml firstthen pipe the markdown file to mustache
        inputtext=$(cat <(echo "$context") | mustache "$file")

        pandoc -r markdown+simple_tables+table_captions+yaml_metadata_block+auto_identifiers+header_attributes+fenced_code_blocks+fenced_code_attributes+tex_math_dollars \
            -w html \
            --tab-stop=2 \
            --toc \
            --mathjax \
            --metadata-file content/metadata.yaml \
            -V builddate="$(date +"%a, %d %b %Y %H:%M:%S %z")" \
            -V year="$(date +"%Y")" \
            --template=./templates/bear.html \
            -o ./public/"$FILENAME".html \
            <(echo "$inputtext")

    done

    # print a success message
    echo -e "${SUCCESS}Build complete!${NC}"

}
defhelp build 'Build the site.'

cmd_run() {
    echo "Starting server..."
    npx nodemon --watch 'content/**/*' -e md,html,yaml --exec './dev build' \
        & caddy file-server --listen :8000 --root ./public \
        & wait
}

# --------------------------------------------------------------------------
# Core script logic
# -----------------------------------------------------------------------------

silent() {
    "$@" > /dev/null 2>&1
}

# If no command given
if [ $# -eq 0 ]; then
    echo -e "${ERROR}ERROR: This script requires a command!${NC}"
    cmd_help
    exit 1
fi
cmd="$1"
shift
if silent type "cmd_$cmd"; then
    "cmd_$cmd" "$@"
    exit $?
else
    echo -e "${ERROR}ERROR: Unknown command!${NC}"
    echo "Type './dev help' for available commands."
    exit 1
fi