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:
- Pandoc
- Mustache
./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