Web

Building and deploying a blog website

The frontend, the build, and going live on Vercel with a Cloudflare domain.

On this page

A blog is read far more often than it changes. So every page here is built once, ahead of time, and sent as a finished HTML file.

Pre-built pagesNothing is generated per visit, so pages open instantly.
Almost no JavaScriptReading needs no scripts, so there's very little to download.
One fluid layoutSizes adapt to the width, so every screen and rotation fits.

The tools

Each tool does one job, and most of them only run during the build.

EleventyTurns HTML posts into full pages, and ships no code to the browser.
esbuildJoins all the CSS and scripts into one small file, very quickly.
ShikiColours code during the build, so readers download no highlighter.
LucideClean open-source icons, added as inline SVG at build time.
gitleaksScans every commit, so tokens never reach GitHub.
PlaywrightOpens every page at 12 screen sizes and fails on layout breaks.
VercelHosts the files on a global CDN, with HTTPS included.
CloudflareHolds the domain and points it at Vercel.

Why not Tailwind? It makes styling faster, especially on big teams, but it doesn't change how a site looks. Plain CSS variables were enough here.

How it works

  1. WriteAn HTML post
  2. BuildPages and bundles
  3. DeployFiles go to Vercel
  4. ReadThe CDN serves them

Adding a post

A post is one HTML file. The top block is metadata, and the rest is the article.

content/posts/hello.html
---
title: Hello world
date: 2026-11-02
---
<p>The article starts here.</p>

The file name becomes the URL, so this one lives at /posts/hello/. The build then does the rest:

Home pageThe newest post appears first.
ContentsBuilt from the headings.
BundlesCSS and scripts become one file each.
ChecksUnsafe HTML stops the build.

What is SVG?

A PNG stores pixels and blurs when enlarged. An SVG stores drawing instructions, so it stays sharp at any size.

R

This logo is the code below. The browser draws it live, so it's not a picture file.

logo.svg
<svg viewBox="0 0 32 32">
  <rect x="2" y="2" width="28" height="28" rx="8"
        fill="#070b14" stroke="#f2b544" stroke-width="2"/>
  <text x="16" y="22" fill="#f2b544" font-size="17"
        text-anchor="middle">R</text>
</svg>

The viewBox sets a 32 by 32 grid, and each shape sits on that grid. An SVG can be written by hand, exported from a tool like Figma, or taken from an icon set.

Dark and light mode

The switch changes one attribute on the page, and CSS does the rest.

Night
#070b14
Amber
#f2b544
Paper
#faf7f0
Ink amber
#955e0e
  1. Colours are variablesEach one is defined twice, and everything else uses the name.
    tokens.css
    :root { --bg: #070b14; }
    :root[data-theme="light"] { --bg: #faf7f0; }
    body { background: var(--bg); }
  2. The button flips the themeIt changes the attribute and saves the choice.
    theme.ts
    const next = root.dataset.theme === "light" ? "dark" : "light";
    root.dataset.theme = next;
    localStorage.setItem("theme", next);
  3. No flash on loadA tiny script in the <head> runs before anything is drawn.
    head
    const saved = localStorage.getItem("theme");
    if (saved) document.documentElement.dataset.theme = saved;

Keeping secrets out

A token pushed to GitHub has to be treated as leaked, so gitleaks scans for them first. It looks for known formats, like ghp_, and for values that look random.

  1. CommitThe hook starts
  2. ScanStaged changes are read
  3. BlockA secret stops the commit
  4. PushCI scans it all again
lefthook.yml
pre-commit:
  commands:
    gitleaks:
      run: gitleaks git --pre-commit --staged --redact --no-banner

It can't catch everything. A plain random password can look harmless, so real secrets belong in the host's settings, never in the code.

Hosting on Vercel

Vercel only needs to know how to build the site and where the finished files go.

Build commandeleventy
Output directorydist

Connecting the domain

Think of DNS as a phone book. Cloudflare holds the book, Vercel hosts the site, and one entry joins them.

  1. In Vercel, add the domainOpen Settings, then Domains, and add blog.example.com. Vercel shows a value. Copy it.
  2. In Cloudflare, add the recordOpen DNS, then Records, and add this:
    TypeCNAME
    Nameblog
    TargetThe copied value
    ProxyDNS only
  3. Back in Vercel, wait for the tickVercel checks the record and adds HTTPS by itself.

Keep the Cloudflare cloud grey. Grey means Cloudflare only answers DNS and Vercel handles the site.