カラントの喫茶店

Currant's Kissaten

How this site is built

23/07/2021


I wasn’t sure how I wanted to create this website initially. I have some experience developing web apps, mostly in React, but since I’m going for simplicity here that was obviously out of the question. I knew I wanted some kind of generator though, if only to maintain separation between the content and the HTML. This meant the strictly manual approach wasn’t going to work either. I looked at some popular static site generators but they all seemed like serious overkill, so in the end I settled on rolling my own. What I ended up with was a 96 line bash script. Nice!

That’s misleading of course, because the difficult part of actually generating HTML was delegating to pandoc, but I still think it’s a pretty slick solution. It’s much more manual than using a proper SSG, but it’s automated enough for my purposes.

Now for how it works. I’ll start with the data, each page on this site is generated from a template and an MDSH file. The template is just HTML with some variables:

<!DOCTYPE html>
<html>
    <head>
        <link rel="stylesheet" href="css/awsm_theme_gondola.min.css"/>
        <title>$title</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <header>
            <h1>カラントの喫茶店</h1>
            Currant's <em>Kissaten</em>
        </header>
        <main>
            <article>
                <h2>$title</h2>
                <h3>$date</h3>
                <hr/>
                $content
                <hr/>
                Tags: $tags
            </article>
            $next
            $prev
        </main>
        <footer>
            Ether: 0xF828E83d82D98cf5c491C563c99952Ce96fE8C96 | 
            <a href="crypto.html">Other crypto's</a>
            <br/>
            Built $last_built
        </footer>
    </body>
</html>

(You can also spy that I’m using awsm CSS, at least at the time of writing).

Why, those don’t variables look suspicously like shell variables (・・?
More on that soon!

The MDSH file contains the actual content for the page, it’s basically a markdown file (.md) combined with a script (.sh).

local date=23/07/2021
local title="How this site is built"
local tags="programming"
%%

I wasn't sure how I wanted to create this website initially...

The markdown contains the real content, while the script part is used to set variables. I considered separating them into two separate files, but in the end of the appeal of “one file per page” won out. The two parts of an MDSH file are separated by a line containing only “%%”.

Now how do we apply an MDSH file to a template? The entrypoint here is this function convert_mdsh()

# convert_mdsh: mdsh_file -> template_file -> substituted_text
convert_mdsh() {
  log converting $1
  
  local i script_end a script
  readarray -t a < "$1"
  
  # Find the end of the script section and the start of the
  # markdown section
  for (( i=0; i<${#a[@]}; i++ )); do
      if [[ ${a[$i]} == "%%" ]]; then ((script_end = i + 1)); break; fi
  done
  
  # Isolate the script part
  script="${a[@]:0:((script_end-1))}"
  
  unlines "${a[@]:script_end}" | \
  pandoc -f markdown -t html | \
  xargs -0 printf "$script local content='%s'" | \
  substitute_template "$2" | \
  tidy -utf8 -indent -quiet --tidy-mark no
}

This function takes in two parameters: the MDSH file and the template file. I’ll run through it statement by statement:

  1. log is a helper that just logs its arguments to STDERR. This is useful because we’ll be redirecting the STDOUT of convert_mdsh() to our final HTML file
  2. This for loop finds the “%%” in the MDSH and stores its line number in script_end.
  3. Here we grab the script part of the MDSH for later use.
  4. This is the start of the actual conversion pipeline. unlines is a utility that converts an array, in this case the markdown content, back into a multiline string.
  5. pandoc accepts the markdown and renders it into HTML
  6. A bit of a weird line, this takes the markdown from the last step and embeds it in a string containing the script part of the MDSH we grabbed earlier with an additional declaration local content=<RENDERED HTML>. xargs is needed because printf doesn’t read from STDIN.
  7. Finally we substitute the template using the script we created in the previous step. This is how substitute_template is defined:

    substitute_template() {
     eval "$(cat -)"
     eval "cat <<EOF
    $(<$1)
    EOF
    " 2> /dev/null
    }

    substitute_template first evaluates its input, which has the effect of setting a bunch of variables locally. Most importantly, thanks to that weird printf content is set to the rendered HTML from the markdown. The next line reads the template file (stored in the first argument), “runs” it in a subshell (which has the effect of substituting parameters), then outputs it. The herestring somehow makes quotations work properly, but I’m not sure how or why.

    An interesting consequence of this approach is that we can do a variety of manipulations in the template itself. For example if I decided I wanted tags to begin with an uppercase letter (and I didn’t want to do some CSS trick to accomplish it) I could replace $tags in the template with ${tags^}. I think I’ll stick away from that kind of stuff though, this approach is insane enough already (^^).
  8. The last step is to pass the fully rendered page through HTML tidy. In addition to prettying up the output, tidy will also warn about things like empty tags and missing href’s, which is handy for catching errors.

Armed with the ability to convert MDSH building the site is pretty straightforward:

  1. Clear the build directory, copy in static resources
  2. Convert all the “singleton” (non-blog) pages.
  3. Loop through the blog entries, applying the following rules:
    1. If the entry is the first, as defined by sort -V -r, set the next variable to empty, the prev variable to an <a> to the next entry in the list. Make a copy of this page at index.html in addition to its fixed entry.
    2. If the entry is the last, set the prev variable to empty and the next variable to a link to the next entry in the list.
    3. Otherwise set both prev and next to links as you would expect.
  4. That’s it, we’re done!

I store the site and script in a repository on github and have the build script configured to run on push with an additional pipeline step to deploy the resulting site (to Neocities at the time of writing). All in all I’m pretty happy with what I ended up building, bash is not a language I particularly enjoy but its hard to beat the simplicity for things like this.


Tags: programming
next previous