How this website was made

Clean HTML templating in Haskell and tricks for webdev on the cheap.

This website is an experience in making a personal website & blog that mixes minimalism, speed, cheap hosting, and exceptionally readable HTML generation with functional programming (Haskell).

In this post, I’ll overview and reflect on some key techniques I came across to implement this site, and will also discuss some alternative approaches for situations that are more demanding, or call for more standard tools.

All the code for the site is available in this GitHub repository, but hopefully many of these techniques will be useful to anyone who wants to make static websites on the cheap, whether you want to use a functional programming style or not.

The Requirements

For the reader it’s a website that:

For the developer (me) it’s a website that:

The outcome is a website made with a recipe that mixes old and new; specifically it:

Functional Templating

Towards HTML Templating that actually looks like HTML

Many webdev development frameworks allow you to generate HTML using HTML templating. This works by users writing the HTML they can, and leaving special placeholders to later be filled-in according to variables; they also wrappers around HTML that cause HTML to be rendered according to if-else logic or per iterations of a loop.

However, mixing HTML with programmatic logic (i.e., variables, if-else logic and loops) often leads to templates that lack readability or even resemblance to the HTML it generates. Examples include:

React JSX (JavaScript)

function TodoList() {
  const todos = ['finish doc', 'submit pr', 'nag dan to review'];
  return (
    <ul>
      {todos.map((message) => <Item key={message} message={message} />)}
    </ul>
  );
}

function Item(props) {
  return <li>{props.message}</li>;
}

Hugo Templating (Go)

{{ if isset .Params "title" }}
 <title>{{ .Params.title }}</title>
{{ else }}
 <title>{{ .Site.title }}</title>
{{ end }}

An alternative I’m interested is: functionally generating HTML and laying out those functions according to the visual logic of the HTML it represents.

Before looking at a Haskell implementation in this website, let’s motivate this idea with the below example using Python’s htbuilder package.

from htbuilder import div, li, ul

dom1 = div(id="container")(
    ul(_class="greetings")(
        li("hello"),
        li("hi"),
        li("whattup"),
    )
)

dom2 = div(id="container")(
    ul(_class="greetings")(
        li(greeting)
        for greeting in [
            "hello",
            "hi",
            "whattup",
        ]
    )
)

Printing either of dom1 or dom2 produces the following HTML (both indented on-save using the popular formatters: prettier for HTML and black for python).

<div id="container">
  <ul class="greetings">
    <li>hello</li>
    <li>hi</li>
    <li>whattup</li>
  </ul>
</div>

Importantly, the source Python and resulting HTML have matching visual logic, i.e., you can mentally write out the HTML as your eyes follow the nesting of dom1 or dom2’s definitions. Furthermore, htbuilder works great when paired with fastapi for the server, and htmx for interactivity and dynamically generating content.

In this website, I used Haskell’s Lucid package (see 2 for tutorial info). The indentation of Haskell’s do-blocks mirrors the resulting HTML nesting, and seamlessly composes with other HTML returning functions. Below is how the nav-bar for this site is written.

navBar :: HTML
navBar = do
  nav_ [class_ "navbar"] $ do
    div_ [class_ "container"] $ do
      ul_ [class_ "navbar-list"] $ do
        li_ [id_ "sandwich-li", class_ "navbar-item"] $ sandwichButton
        navLink "/" "home"
        navLink "/about" "about"
        navLink "/publications" "publications"
        navLink "/posts" "posts"
        navLink "/tags" "tags"
        navLink "/upcoming" "upcoming"
        navLink "/contact" "contact"
        div_ [class_ "expander"] ""
        li_ [id_ "theme-li", class_ "navbar-item"] $ do
          themeButton
          revertToOSThemeButton

navLink :: Text -> Text -> HTML
navLink path text = do
  li_ [class_ "navbar-item nav-toggle"] $ do
    a_ [class_ "navbar-link", href_ path] $ toHtml text

sandwichButton :: HTML
...

themeButton :: HTML
...

revertToOSThemeButton :: HTML
...

Note, the repeated navLink elements could have been generated using a forM loop, but I don’t think that would improve readability in this case.

So why Haskell (i.e., strongly typed functional)?

With the right languages and tools, programming should feel like putting Lego blocks together, and when the pieces click, you’re usually confident it works. For programming, it’s: “if it compiles, it works” - so Python falls short there.

However, a compiler isn’t enough on its own. This is because modern webdev frameworks are often plagued with unclear events and lifecycle systems (not to mention CSS). In other words, a typescript-based framework may compile, but still leave you feeling like an idiot after you click something on the screen and nothing happens, or nothing centres on the page for the 100th time.

The first time I didn’t feel that kind of dread doing web development was programming with elm plus elm-ui for replacing CSS. However, Elm shines best when making single-page-apps with lots of state and interactivity. In other words, not this site. So the most practical solution to me was to use very minimal, compartmentalised JavaScript and CSS.

Minimising Frameworks

The biblical art of fleeing from sin

I once heard an interesting piece of advice for avoiding junk food: “flee from sin”. The idea was, if you feel the temptation of junk food, go on a walk, far away from that food; at a certain point, the temptation subsides, and it becomes safe to return.

So it goes with frameworks, which when close by tempt you to tangle up yet more frameworks. Hence, I wanted to investigate minimal method of implementing the following features (each has a section in this post).3

Comments

When it came to having comments on my website, the following questions naturally arose:

My head starting spinning straight away, would I need a server? Should I host a separate comment server (e.g., commentario)? What would that server cost? Would it be better to pay for a managed service like Disqus? This all felt like I was wildly veering away from my goals with this experiment.

Then I stumbled on Rachel Smith’s blog, which asked: why not just take comments via a form, manually commit them to the website’s repo, and redeploy? Is it really such blasphemy as a user to not get your comment posted instantly, or as a dev to hard-code content? Considering personal blogs barely get any comments, would it not a huge waste of money or servers just for a website to be “proper”?

The idea of gaining so much by just taking a way a bit of functionality blew my mind… We really lack an opposite to the phrase “throwing the baby out with the bathwater”.

Afterwards, I found Ryan HBM’s blog, which found an ingenious way to host comments in Rachel’s style. Simply use the GitHub issues of your repository as the form the submitting comments, and build the website by loading those comments using the GitHub API. The benefit here is GitHub handles the tough work of formatting your comments, images, GIFs, etc., providing moderation, storage, accounts, and more.

My Haskell code for reading GitHub comments can be found here.

Themes

For themes, I found this great blog post by Codemzy, that in 18 lines of JavaScript manages a user’s theme preference via a toggle-able “dark” class in the root <html> as well as the browser’s local storage for persistence.

What’s nice about this is that it’s a totally isolated feature in an isolated JavaScript file theme-switcher.js, which we can orchestrate via a window.onload = function () {...} callback in a main.js included after theme-switcher.js.

Again, yes, it’s a bit blasphemous having this hard-coding, special file ordering and the CSS needing to know about the special “dark” class, etc. But the simplicity is just worth it; the hard-coding and coupling always ends up somewhere, if it’s not in the code, it’s in the framework setup or the architecture itself.

I extended Codemzy’s approach in the following way:

You can view the JavaScript here.

Looks

Does anyone else feel like they suck at CSS?

For the look and feel of my site, I drew a lot from Niklas Fasching’s blog. In particular, it introduced me to Skeleton CSS, a 400 line CSS file that defines nice starting points for spacing, typography, etc. Some nice additions I made to it are:

Markdown Authoring

The implementation involves using Haskell’s Pandoc package to convert Markdown to Lucid HTML. A useful, unintended consequence is that writing raw HTML into markdown files just functions as normal HTML in the corresponding converted HTML. For example, converting:

# Hello

Some text

<svg>...</svg>

- Item 1
- Item 2
- Item 3

Produces:

<h1>Hello</h1>
<p>Some text</>
<svg>...</svg>
<li>
  <ul>Item 1</ul>
  <ul>Item 2</ul>
  <ul>Item 3</ul>
</li>

See LucidUtils.hs.

I haven’t had great success converting .tex files to HTML via MathJax - especially when latex imports and new commands are used. What I’ve found works best is simply pasting my latex into Up Math, which converts it into Markdown or HTML. It even handles things like Tikzcd well.

Deployment and Hosting

For hosting, everything was quite easy through AWS:

I use the following script to automate zipping and uploading my files to AWS.

#!/usr/bin/env bash
set -euo pipefail

export AWS_PROFILE=update-amplify

# Check AWS Profie
aws sts get-caller-identity | cat
Get Amplify App details
aws amplify list-apps | cat

APP_ID=...
BRANCH=master
ZIP_FILE="$(date +%F).zip"

echo "Building..."
cabal run -fforce-recomp blog

echo "Packaging..."
(cd html && zip -r "../$ZIP_FILE" .)

echo "Deploying..."
DEPLOYMENT=$(
  aws amplify create-deployment \
    --app-id "$APP_ID" \
    --branch-name "$BRANCH" \
    --query '{jobId: jobId, uploadUrl: zipUploadUrl}' \
    --output json
)
JOB_ID=$(echo $DEPLOYMENT | jq -r '.jobId')
UPLOAD_URL=$(echo $DEPLOYMENT | jq -r '.uploadUrl')

curl -T $ZIP_FILE "$UPLOAD_URL"

aws amplify start-deployment --app-id $APP_ID --branch-name $BRANCH --job-id "$JOB_ID" | cat

mkdir -p published-versions
mv "$ZIP_FILE" published-versions
echo "Done; package in published-versions/$ZIP_FILE"

You just need to: 1. Install the AWS CLI. 2. Create an IAM user with the AdministratorAccess-Amplify policy (to get keys to use the CLI). 3. Create a matching profile i.e., files like:

~/.aws/config

[profile update-amplify]
region = ap-southeast-2
output = json

~/.aws/credentials

[update-amplify]
aws_access_key_id = ...
aws_secret_access_key = ...

Comments

Comments are a static snapshot of a GitHub Issue. Please leave a comment and after reviewing it, I'll rebuild the site with it.

Footnotes and References


1

This doesn’t include the $15.40 USD per year I pay for my domain through Amazon Registrar but is rather is the $0.57 USD per month I pay for AWS Route 53 to serve the static website I’ve uploaded to AWS Amplify for free.

2

Besides YouTube, tutorials are a bit lacking here and tend to focus on the improvements of Lucid over another package Blaze HTML. Nevertheless, Monday Morning Haskell’s tutorial is a good start. Chris Done’s post goes into other benefits and subtleties around how the Html monad works, as well as the Hackage homepage.

NOTE: I’m using the original lucid package rather than lucid2 because of compatibility issues withlucid-svg.

3

Note, my original inspiration for this was Luke Smith’s rants on modern web bloat. For him, living in the woods, he found that loading a simple cooking recipe site would load 20-30Mb of data, ads and trackers over several minutes.

This eventually led me to HTMX, which allows you to get quite a cool amount of interactivity and dynamic content out of an API server. It works particularly well for me when I need to make simple data entry forms and tables for work; they don’t have to be pretty, but they do have to be user-friendly.

The free ebook Hypermedia Systems is a good place to start with HTML. There are also lots of good talks - especially by its creator Carson Gross, e.g., this presentation on theprimeagen. At the very least, that experiment is a great way to take a step back and conceptualise what the web really is.