← berndsen.dev

Building This Site

The Motivation of Building This Site

As a change of pace from my usual work revolving around AI/ML pipelines and concerning myself with the question of how we can make intelligence(artificial or human) fail safely in a predictable manner, I choose to go the exact opposite direction. Not every system needs to be hyper intelligent and therefore overly complex. Sometimes minimal tools, a well defined problem space and a focus is all it takes. I wanted a personal site that was fast, minimal, and didn’t look like every other developer portfolio.

That means: - No bloated JavaScript frameworks. - No heavy build pipelines. - Just simple tools like plain HTML, CSS, vanilla JS and Bash.

For part of the implementation I used Claude, since my front end skills have gotten a little rusty over the years.

The visual design

The visual language is inspired by Bauhaus — geometry, primary colors, no decoration. The centerpiece is a 5x5 grid of circles that fades from light to dark using a simple color interpolation:

const fade = (i + j) / (2 * (gridSize - 1));
const rgb = interpolateColor(light, dark, fade);

Each page load, three dots are randomly placed in the grid: red for GitHub, yellow for the blog, and blue for LinkedIn. Hovering over any of them reveals the label in the footer, color-matched. It’s a small interaction that rewards curiosity without demanding it — the bottom nav has the same links for anyone who doesn’t hover.

No build step

The entire site is static HTML and a single CSS file shared across the landing page and blog. Blog posts are written in Markdown and converted to HTML using a simple template with $title$, $date$, and $body$ placeholders. No static site generator, no Node, just pandoc as the only external dependency.

The file structure is flat:

index.html
style.css
script.js
blog/
  index.html
  template.html
  tools/
    publish.sh
  posts/
    <contains the blog posts in .md format>
  pub/
    <contains the rendered html>

The grid animation

Circles animate in with a staggered pop-in effect. The delay is based on grid position:

const delay = (i + j) * 60;

This creates a diagonal wave from top-left to bottom-right. The animation itself is pure CSS:

.circle {
  opacity: 0;
  transform: scale(0);
  animation: pop-in 0.4s ease forwards;
}

@keyframes pop-in {
  to {
    opacity: 1;
    transform: scale(1);
  }
}

Data-driven colored dots

Rather than hardcoding each colored circle with if/else branches, the three special dots are defined as a data array:

const circles = [
  { index: redIndex, className: 'circle', href: '...', label: 'GitHub', color: '#e04545' },
  { index: yellowIndex, className: 'circle yellow', href: '...', label: 'Blog', color: '#FFD300' },
  { index: blueIndex, className: 'circle blue', href: '...', label: 'LinkedIn', color: '#0A66C2' },
];

The grid loop checks each cell against this array. Adding a new colored dot means adding one object to the array — no logic changes needed.

Tradeoffs

There are things this site intentionally doesn’t do:

The constraint was to keep the total payload under what a single React component would cost. The entire site — HTML, CSS, JS, all pages — is smaller than most hero images.

What I’d change

If the blog grows beyond a handful of posts, the manual template workflow won’t scale. At that point I’d add a small build script — probably a 20-line shell script that loops over Markdown files and generates HTML. Still no framework, just automation for the repetitive parts.

Resources

https://github.com/berni-b/berni-b.github.io