her.esy.fun/src/drafts/0018-makefile-as-static-site-builder-follow-up/index.org

216 lines
6.1 KiB
Org Mode
Raw Normal View History

2021-05-09 15:32:15 +00:00
#+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.
2021-05-17 21:27:37 +00:00
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.
2021-05-09 15:32:15 +00:00
2021-05-17 21:27:37 +00:00
A Makefile is constitued of rules.
2021-05-09 15:32:15 +00:00
The first rule of your Makefile will be the default rule.
2021-05-17 21:27:37 +00:00
The first rule of my Makefile is called =all=.
A rule as the following format:
2021-05-09 15:32:15 +00:00
#+begin_src makefile
2021-05-17 21:27:37 +00:00
target: file1 file2
cmd --input file1 file2 \
--output target
2021-05-09 15:32:15 +00:00
#+end_src
2021-05-17 21:27:37 +00:00
if =target= does not exists, then =make= will look at its
2021-05-09 15:32:15 +00:00
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
2021-05-17 21:27:37 +00:00
=target=.
2021-05-09 15:32:15 +00:00
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/<something>= look at
=src/<something>= and
- create the directory to put =_site/<something>= 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.