Blog » How the Web Site is built
internet elixir mixThis 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.
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!