#+TITLE: Makefile as static site builder #+DESCRIPTION: A few Makefile features tutorial #+KEYWORDS: blog static #+AUTHOR: Yann Esposito #+EMAIL: yann@esposito.host #+DATE: [2021-05-09 Sun] #+LANG: en #+OPTIONS: auto-id:t #+STARTUP: showeverything After many different tools, I recently switched to a simple Makefile to generate my static website. In previous article [[https://her.esy.fun/posts/0017-static-blog-builder/index.html][Static Blog Builder]] I give a starter pack. In this post I provide more detail about my specific Makefile. A Makefile is constitued of rules. The first rule of your Makefile will be the default rule. The first rule of my Makefile is called =all=. A rule as the following format: #+begin_src makefile target: file1 file2 cmd --input file1 file2 \ --output target #+end_src if =target= does not exists, then =make= will look at its dependencies. If any of its dependency need to be updated, it will run all the rules in the correct order to rebuild them, and finally run the script to build =target=. A file need to be updated if one of its dependency need to be updated or is newer. The ususal case of =make= is about building a single binary out of many source files. But for a static website, we need to generate a lot of files from a lot of files. So we construct the rules like this: #+begin_src makefile all: site # build a list of files that will need to be build DST_FILES := .... ALL += $(DST_FILES) # another list of files DST_FILES_2 := .... ALL += $(DST_FILES_2) site: $(ALL) #+end_src In my =Makefile= I have many similar block with the same pattern. 1. I retrieve a list of source files 2. I construct the list of destination files (change the directory, the extension) 3. I declare a rule to construct these destination files 4. I add the destination files to the =ALL= variable. So I have a block for: - raw assets I just want copied - images I would like to compress for the web - =html= I would like to generate from org mode files - =gmi= I would like to generate from org mode files - =xml= files I use as cache to build different index files - =index.html= file containing a list of my posts - =rss.xml= file containing a list of my posts - =gemini-atom.xml= file containing a list of my posts So to go further, let's take a look at a simplified raw assets copy block: #+begin_src makefile SRC_ASSETS := $(shell find src -type f) DST_ASSETS := $(patsubst src/%,_site/%,$(SRC_ASSETS)) _site/% : src/% @mkdir -p "$(dir $@)" cp "$<" "$@" ALL += $(DST_ASSETS) #+end_src OK, this looks terrible. But mainly: 1. ~SRC_ASSETS~ will contains the result of the command ~find~. 2. We replace all =src/= prefix of all those files by the =_site/= prefix. 3. We create a rule, if you are asked to build =_site/= look at =src/= and - create the directory to put =_site/= in - copy the file About the line ~@mkdir -p "$(dir $@)"~: - the =@= at the start of the command simply means that we make this execution silent. - The =$@= is replaced by the target string. - And =$(dir $@)= will generate the dirname of =$@=. For the line with ~cp~ you just need to know that =$<= will represent the first dependency. Once you have this pattern in mind. Adding new block become a bit natural. You will also like to use some variables for repetitive names. ** Prelude :PROPERTIES: :CUSTOM_ID: prelude :END: #+begin_src makefile all: site SRC_DIR ?= src DST_DIR ?= _site CACHE_DIR ?= .cache # we don't want to publish files in drafts NO_DRAFT := -not -path '$(SRC_DIR)/drafts/*' # we don't copy source files NO_SRC_FILE := ! -name '*.org' #+end_src ** CSS :PROPERTIES: :CUSTOM_ID: css :END: #+begin_src makefile # CSS SRC_CSS_FILES := $(shell find $(SRC_DIR) -type f -name '*.css') DST_CSS_FILES := $(patsubst $(SRC_DIR)/%,$(DST_DIR)/%,$(SRC_RAW_FILES)) ALL += $(DST_CSS_FILES) $(DST_DIR)/%.css : $(SRC_DIR)/%.css @mkdir -p "$(dir $@)" minify "$<" > "$@" css: $(DST_CSS_FILES) #+end_src This is very similar to the block for raw assets. The difference is just that instead of using =cp= we use the =minify= command. And also I use global constants (=SRC_DIR= and =DST_DIR=). ** ORG -> HTML :PROPERTIES: :CUSTOM_ID: org----html :END: Now this one is more complex but is still follow the same pattern. #+begin_src makefile # ORG -> HTML EXT ?= .org SRC_PANDOC_FILES ?= $(shell find $(SRC_DIR) -type f -name "*$(EXT)" $(NO_DRAFT)) DST_PANDOC_FILES ?= $(patsubst %$(EXT),%.html, \ $(patsubst $(SRC_DIR)/%,$(DST_DIR)/%, \ $(SRC_PANDOC_FILES))) PANDOC_TEMPLATE ?= templates/post.html MK_HTML := engine/mk-html.sh PANDOC := $(MK_HTML) $(PANDOC_CSS) $(PANDOC_TEMPLATE) $(DST_DIR)/%.html: $(SRC_DIR)/%.org $(PANDOC_TEMPLATE) $(MK_HTML) @mkdir -p "$(dir $@)" $(PANDOC) "$<" "$@.tmp" minify --mime text/html "$@.tmp" > "$@" @rm "$@.tmp" ALL += $(DST_PANDOC_FILES) html: $(DST_PANDOC_FILES) #+end_src So to construct =DST_PANDOC_FILES= this time we also need to change the extension of the file from =org= to =html=. We need to provide a template that will be passed to pandoc. And of course, as if we change the template file we would like to regenerate all HTML files we put the template as a dependency. But importantly *not* at the first place. Because we use =$<= that will be the first dependency. I also have a short script instead of directly using =pandoc=. Because I would like to handle the =toc= depending on the metadatas in the file. The =mk-html.sh= is quite straightforward: #+begin_src bash #!/usr/bin/env bash set -eu # put me at the top level of my project (like Makefile) cd "$(git rev-parse --show-toplevel)" || exit 1 template="$1" orgfile="$2" htmlfile="$3" # check if there is the #+OPTIONS: toc:t tocoption="" if grep -ie '^#+options:' "$orgfile" | grep 'toc:t'>/dev/null; then tocoption="--toc" fi set -x pandoc $tocoption \ --template="$template" \ --mathml \ --from org \ --to html5 \ --standalone \ $orgfile \ --output "$htmlfile" #+end_src Once generated I also minify the html file. And, that's it. But the important part is that now, if I change my script or the template or the file, it will generate the dependencies.