Andrew Shaw

Blog » How the Web Site is built

internet elixir mix

The Drum Shop

This homepage is half in-earnest technical project and half performance art. I wanted to keep things as simple, bare-bones, and extensible as I can, and to that end I opted initially for plain HTML and CSS. It’s surprising how far default browser rendering will get you!

After a few pages worth of copy-pasting I figured something more reusable would be better, and so looked back to the days of simple PHP includes. For the equivalent, Elixir comes with EEx - Embedded Elixir for the templating of arbitrary text documents. Its efficient implementation and swappable backends is what powers Phoenix and Phoenix LiveView, and is more than suitable for my dingy needs.

In this post I’ll run through the overall structure, then break down custom sections like the blog and photo gallery. You can read this as part documentation and part tutorial. I’m not sure if I’ll bother sharing the code, but seriously, writing a static site generator is so easy that you can do pretty much all of this in an evening.

Coding

The Build Script

PHP, which powered my first ever one of these sort of sites, executes at response time, much like CGI. The page content is dynamically generated at runtime. To contrast, static sites are… static, so all assets are generated up front on build/deployment time. Venerable Mix tasks are used to tie the whole thing together with mix homepage.build building out all of the HTML, homepage.photos running the photo ingestion pipeline (see below) and homepage.deploy shelling out to Cloudflare’s wrangler CLI to push a new deployment up to prod.

The Layout

I have one top level template, which defines the root structure of each page, that is, the actual <html> element, the <head> etc. The navbar is generated in this template, from a master list of pages.

Inside the main template is the <%= @inner_content %> include which you may recognise from Phoenix templates:

<h1>Andrew Shaw</h1>
<div id="content">
    <nav>
        <ul>
        <%= for {page, title} <- @pages do %>
            <li class="<%= if page == @current_page, do: "active", else: "" %>">
                <a href="/<%= page %>.html"><%= title %> </a>
                <%= if page == "blog" do %>
                    <img class="updated" src="/images/updated.gif" />
                <% end %>
            </li>
        <% end %>
        </ul>
    <div>
        <img id="audio-button" src="/images/headphone-button.gif" alt="Toggle music">
        <audio id="background-music" loop>
            <source src="/audio/iceland-tv-now.mp3" type="audio/mpeg">
        </audio>
    </div>
    </nav>
    <div>
        <%= @inner_content %>
    </div>
</div>

On build, the main Mix task expects there to be a template in lib/templates/#{page}.html.eex which it evaluates, then passes in as the inner_content assign when expanding the main template.

Blog

The blog is fed by markdown documents. I’m using MDex (which uses a cool Rust NIF to be very fast) to parse them into an AST, then YamlElixir to pull out the metadata.

Each blog post has a slug which generates the URL's path, a date and a title, along with an abstract and fetching thumbnail to entice eager readers:

---
date: 2025-01-21
tags: [internet, elixir, mix]
slug: building-web-site
title: How the Web Site is built
abstract: A little breakdown on how this page is built and hosted
thumbnail: /images/blog/03/fs-tree.jpg
---


This homepage is half in-earnest technical project and half performance art. [...]

As a bonus, I've added an RSS feed, which I'll talk about in a later post.

Photos

My photos section is deliberately very low fidelity. There’s a few reasons: For one, I still hold reservations about just lashing high res photos I’ve taken online. Anything more than an “oh that’s nice” glance, like use as a wallpaper, or printing I’d like to know about (if you'd like a higher-resolution copy of any of my photos for this purpose, please email me! Let's talk!).

Secondly, it would be nice to hopefully make them less useful to these collect-it-all AI crawlers. Perhaps I’ll try and find some way to poison or watermark them at some point.

Mostly though, I'm prioritising the early 2000s aesthetic (hence why I’ve turned off image smoothing in the main stylesheet).

In any case there’s a simple processing pipeline; images are fed in in dated folders, then I run imagemagick over all of them to resize them down to 256 x 256 pixels, convert them to JPEG and crucially, strip out any wayward EXIF metadata, like geo-location or Facebook’s image fingerprints (most of the images are from my Instagram archive).

These then get collected into per-month gallery pages.

Music

The only noteworthy addition to the music page is that I’m pulling in my top 100 artists from Last.fm via their developer API.

def top_artists() do
  get_artists()
  |> handle_response()
  |> Enum.sort_by(& &1.playcount)
  |> Enum.reverse()
end

Hosting

Finally, I wanted something really simple, in the sense of really easy to set up. CloudFlare pages' free tier offers unlimited bandwidth, and the bare basics like HTTPS etc. I presume it's also pushed out to their whole edge network, so it's good to know folks from the US, to Europe to Oceania and beyond can load my buckets of content very fast.

Deployment is as trivial as running npx wrangler pages deploy <directory with assets>. They do clever content hashing so that you only upload new versions of files that have changed.

Hosting on CloudFlare is really interesting, and I may have a separate post about the things I've discovered. For one, they push content out to all of their 250+ edge locations, which is a far cry from my old homepage on BlackKnight. There's also fine grained traffic filtering and monitoring which I've not delved into yet. For the moment, it's free, unlimited and easy.

Wrapup

And that’s about all! As I said it’s really about as simple as it gets, aside from maybe literally just using some bash scripts or something. Most of all though it’s extensible, and it’s mine.

Try making yours today!

Your Ad Here