Blog Web 2 min read
How this website is built
No framework, no database and no trackers. A small static generator, self-hosted fonts, a strict security policy and a one-command deploy.

This site is my web development portfolio, so it’s fair to show how it’s made. The short version: plain files in, plain HTML out.
A small static generator
#There is no framework. One Node.js script, build.mjs, reads the page templates, the text and the styles. Then it writes a folder of finished HTML. That folder is the whole website. There is no database, and nothing runs on the server except the web server itself.
The templates are plain HTML with a few tags for text, conditions and loops. All the words live in small JSON files, one per section, so I can change a sentence without touching the layout:
html
<h2 class="h2" id="work-title">{{t.work.title}}</h2>The build also bundles the CSS and the JavaScript, minifies them with esbuild and names each file after its content. A browser can keep those files for a year, because a new version always gets a new name.
Fonts on my own server
#The three typefaces, Fraunces, Manrope and JetBrains Mono, are served from this site, not from a font service. Only the Latin characters are included, which keeps the files small. Opening a page never sends a request to another company’s server.
A strict security policy
#Every page comes with a Content Security Policy. It tells the browser to load scripts, styles, fonts and images from this site only. The one inline script, which picks the dark or light theme before the page appears, is allowed by its exact hash. Anything else is blocked. There are no analytics and no trackers.
Checked before it ships
#A check script reads every page in the finished folder. It looks for broken links, missing alt text, heading order and file sizes. With Chrome installed, it also opens the site and tests the mobile menu and horizontal scrolling on small screens.
One command to deploy
#npm run deploy does the rest:
- builds the site and runs the checks, and stops if anything fails
- copies the files to a Linux server with rsync over SSH
- tests the new nginx config with
nginx -tbefore it goes live, and puts the old one back if the test fails - checks the live site over HTTPS: the redirects, the security headers and the 404 page
And this blog
#Each post, this one included, is a Markdown file that sits next to everything else. The build turns it into a page and adds it to the list and the feed. Drafts only appear on my own computer: the deploy refuses to publish a build that has one.