Personal Site Build
The Goal: For the last few years I have made several attempts to create a website/portfolio for myself that I could easily access and update. I had already gone through using both Wordpress and Canva but I was not satisfied with the amount of customization available to me, and was left paying monthly subscription fees.
The Solution: So, I decided to build a static site from scratch using standard HTML and Tailwind CSS. It was a good opportunity to get more comfortable with code and set up a proper development workflow. It enabled me to have full control over the domain and hosting without the overhead of a website builder.
Escaping the "Builder" Trap
The main driver for this project was cost and flexibility. Website builders are convenient, but they lock you into their ecosystem. By switching to HTML, I could create the exact minimalist layout I wanted. I used Tailwind CSS to handle the styling because it keeps the file small, manageable, and easy to update. The color palette lives in one CSS file as variables, and the Tailwind config points at those variables instead of repeating the hex codes, so changing a color is a single edit in a single place.
module.exports = {
darkMode: 'class',
content: ['../*.html', '../*.js'],
theme: {
extend: {
colors: {
page: 'var(--color-page)',
surface: 'var(--color-surface)',
ink: 'var(--color-text-primary)',
muted: 'var(--color-text-muted)',
accent: 'var(--color-accent-text)'
}
}
}
}Getting Off the CDN
For a while I loaded Tailwind straight from their CDN script. That is the fastest way to get started, but it compiles the stylesheet in the visitor's browser on every page load, and their own docs say not to ship it that way. So I moved the build onto my laptop. The browser now downloads about 22KB of finished CSS and nothing else. The one catch is that adding a new Tailwind class means I have to remember to rebuild before I push.
cd tailwind-build
npm run build:css # writes ../assets/tailwind.css
# then the usual three
git add .
git commit -m "Rebuild stylesheet"
git push origin mainThe Deployment Workflow
I wanted the site to be as easy to update as possible. To do this I set up a continuous deployment pipeline using GitHub and Cloudflare Pages. Now, when I want to add a new project or fix a typo, I just edit the code on my laptop and push the changes with my command Terminal to the repository. Cloudflare detects the commit and rebuilds the site automatically in seconds.
# The entire update process takes three commands
git add .
git commit -m "Added new case study"
git push origin main
# Cloudflare automatically builds and deploys to https://olivertwilliams.comMy Project Update System
Early on, I used a custom HTML page for each individual project. But quickly I realized this was both wasteful with repetitive code, and was very time consuming. Every time I wanted to change the layout, I'd have to edit twenty different files. So after some research, I managed to build a single reusable template and a central JavaScript file to pull information from. Every case study on the site now comes out of this one file, including this page.
const projectData = {
"mainframe": {
group: "design",
layout: "case",
title: "Mainframe Studios First Friday",
card: { chip: "Advertising", blurb: "..." },
background: "...", challenge: "...",
solution: "...", result: "...",
gallery: [ /* images with captions */ ]
},
"dmarc": {
group: "data",
layout: "technical",
title: "DMARC Network Analysis",
sections: [ /* headings, text, code panels, figures */ ]
}
};How the Template Picks a Project
Each project is an object, and a set of functions turn it into HTML. Which function runs depends on a layout field, so design projects get the case study format with the image gallery, and the data and product pages get a version with code panels and figures next to them. I kept those functions free of anything browser-specific, which turned out to matter more than I expected. It means the build can run the same code on my laptop and write each finished page out to its own file. So I still only maintain one template, but what gets deployed is a real HTML page per project instead of an empty one that fills itself in.
// the same renderers the browser would use,
// run at build time instead
for (const id of Object.keys(projectData)) {
const data = projectData[id];
const page = fill(template, {
head: metaFor(id, data),
body: projectLayout(data)
});
write('projects/' + id + '.html', page);
}