How this website is built 🏗️
Every now and then someone asks me what framework I use for this website. The answer is always the same: none. No React, no Next.js, no Hugo, no Jekyll, no Gatsby, no Astro, no Eleventy. Just a human and a text editor. Well, and now an AI too. More on that later. This entire website is hand-written HTML. Every single page. Every single tag. Every single sidenote you're reading right now.
You might wonder why on earth someone would do that in 2026. And that's a fair question. The short answer is that I find it fun, and the long answer is that I believe the web got unnecessarily complicated for the kind of thing most personal websites actually need.
So let me walk you through the entire stack, from the CSS to the cloud. Grab a beverage of your preference and let's go! ☕
The design: Tufte CSS
The visual identity of this site is built on Tufte CSS, a stylesheet inspired by the work of Edward Tufte, the godfather of data visualization. If you've never read "The Visual Display of Quantitative Information" I highly recommend it. It'll change how you look at charts forever. Tufte's design philosophy values clarity, minimalism, and a high data-to-ink ratio. Translated to web design, that means: wide margins, serif typography, and sidenotes instead of footnotes.
The font you're reading is
ET Book,
a digitization of the typeface used in Tufte's books.
It's served directly from the et-book/ directory alongside the HTML —
no Google Fonts, no CDN, no third-party requests.
This is partly a privacy choice. Loading fonts from Google means Google gets a ping
every time someone visits your site.
Self-hosting fonts is trivially easy and avoids that entirely.
Your browser loads everything from one place.
I've also made some tweaks to the original Tufte CSS.
The background is a very subtle mint-ish white (#f9fefc),
the text is a warm dark brown (#443b36),
and links get a nice red accent (#dc5945) on hover.
I spent an unreasonable amount of time choosing these three colors.
Then I found
Huemint
and realized AI could have done it for me in seconds.
Classic.
Sidenotes: the best feature you didn't know you needed
The thing that makes Tufte CSS special is the sidenote system. Instead of footnotes that force you to scroll down and lose your place, sidenotes sit right there in the margin, next to the text they annotate. Yes, I am using a sidenote to explain sidenotes. How delightfully meta of me. 🪞
The best part? The entire system works without a single line of JavaScript.
It's pure HTML and CSS. On desktop, the notes float in the right margin.
On mobile, they collapse behind a little ⊕ toggle button.
That toggle is just a hidden <input type="checkbox">
paired with a <label>.
When you tap the label, the checkbox toggles, and CSS does the rest.
Here's what the markup looks like:
<label class="sidenote-number"></label>
<label for="unique-id" class="margin-toggle">⊕</label>
<input type="checkbox" id="unique-id" class="margin-toggle">
<span class="marginnote sidenote">Your note here</span>
It looks verbose when you read the source, yes. But it means the page is fully functional with CSS disabled, gracefully degrades on ancient browsers, and doesn't need a build step to work. There is exactly zero JavaScript on this entire website. Not even analytics. I don't track you. You're welcome. 🫡
The banner hack 🙈
You know that big photo of my eyes at the top of every page?
It's a full-width image that stretches edge to edge.
Tufte CSS provides a .full-width class for this,
which sets max-width: none to let the image
escape the text column.
But here's the embarrassing part. If you're a frontend developer reading this, I apologize in advance. I know this is cursed. PRs are welcome. The content below the banner was overlapping with the image because the full-width image doesn't participate in the normal document flow the way I needed it to. My very professional solution? I added a second invisible copy of the same image right after the visible one.
<img style="position: relative; opacity: 0; z-index: -9;"
src="/media/00-common/lef_eyes_2021-06.jpg">
It's invisible, it sits behind everything, and its only job is to push the content down by exactly the right amount. Does it work? Yes. Is it elegant? Absolutely not. Will I fix it someday? ...probably not. 🤷
Deployment: from git push to the cloud ☁️
The deployment pipeline is my favorite part because once I set it up, I never had to think about it again. Here's how it works:
Every time I push changes to the src/ directory on GitHub,
a
GitHub Actions
workflow kicks in.
The workflow only triggers on changes to src/ or .github/workflows/.
So I can mess around with documentation, tools, or configuration
without accidentally deploying anything.
It takes the entire src/ directory and syncs it to an
AWS S3 bucket
configured for static website hosting.
In front of S3 sits Cloudflare, handling DNS, HTTPS, and caching. The last step of every deploy purges the Cloudflare cache so that you always see the latest version of the site.
But here's the part that really sparks joy.
This is essentially a poor man's
Vercel preview deployments,
except it costs approximately nothing and runs on S3.
When I push to a branch that isn't main,
the pipeline creates a separate S3 bucket and a Cloudflare CNAME record for it.
So if I push to a branch called cool-feature,
the site goes live at cool-feature.lef.fyi.
Automagically.
No manual DNS config, no separate hosting setup.
In fact, if you're reading this on a subdomain right now, that's exactly what happened. This post was originally deployed to a feature branch subdomain before being merged to main. A blog post about the deployment system, deployed by the deployment system, to verify the deployment system. Inception. 🎬
Images and Git LFS 📸
One thing you don't want is a Git repository bloated with megabytes of images.
Every .png, .jpg, and .jpeg
in this repo is tracked with
Git LFS
(Large File Storage).
Git LFS replaces large files with tiny pointer files in the repo,
while storing the actual content on a remote server.
Your git clone stays fast, and the real files
are pulled only when you need them.
Media files are organized by post.
Shared assets like the banner live in src/media/00-common/,
while each post gets its own directory matching its filename:
src/media/20260301-how-this-website-is-built/.
The 00-common prefix ensures shared assets sort first.
The date prefix on everything else keeps things chronologically sorted.
Small things, but they add up when you're navigating a file tree.
The little Python helper 🐍
Tucked away in the tools/ directory there's a tiny Python script
that generates the sitemap.txt file.
A sitemap tells search engines what pages exist on your site.
The .txt format is the simplest kind —
just a list of URLs, one per line.
It crawls the src/ directory, turns every file into a full URL,
and writes it all out. You can exclude paths too,
like the font files that search engines definitely don't need to index.
cd tools && uv run python src/sitemap_generator.py https://lef.fyi ../src --except "et-book"
It's the only Python in the entire project, managed with uv in its own little corner. A small script doing one thing well. Unix philosophy and all that. 🐧
Teaching an AI to help 🤖
The newest addition to this website's toolbox is a set of files that
teach AI assistants how the project works.
There's a CLAUDE.md at the root with the overall developer guide,
and a .claude/ directory with context files and step-by-step skills.
Think of it like onboarding documentation, except the new hire
is a large language model that reads really fast
and never asks you to repeat yourself.
The idea is simple: if you give an AI enough context about your project's conventions, file structure, and common tasks, it can help you without constantly asking "what framework is this?" or "where do the blog posts go?". It already knows.
For example, there's a skill file that describes exactly how to create a new blog post: what to name the file, what template to start from, where to put the media, how to update the homepage, and how to regenerate the sitemap. Full disclosure: this very post was written with the help of an AI that was using those exact skill files. It's turtles all the way down. 🐢
The meta beauty of it is that the AI helped write the scaffolding that teaches it how to help. And then it used that scaffolding to write a blog post about the scaffolding. We've achieved some kind of documentation ouroboros. 🐍
Why not just use a static site generator?
Look, I get it. Tools like Hugo, Jekyll, or Astro exist for a reason. They're great for people who want to write in Markdown and have a build step handle the templating. For many projects that's the right call.
But for this little corner of the internet, I like the directness. Try it: right-click, "View Page Source". What you see is what I wrote. No build artifacts, no minified bundles, no generated divs. Just HTML the way Tim Berners-Lee intended. ❤️ What I write is exactly what gets deployed. There's no mystery layer between my editor and your browser. The entire "build process" is running one CSS minification command. That's it.
There's also something satisfying about a website that would work perfectly fine if you opened the HTML files directly from disk. No server needed, no build needed, no dependencies needed. Just files.
The full stack, summarized
For the skimmers among you, here's the whole thing at a glance:
- Content: Hand-written HTML
- Styling: Tufte CSS + ET Book font (self-hosted)
- Interactivity: CSS-only sidenote toggles (zero JavaScript)
- Version control: Git + Git LFS for images
- Hosting: AWS S3 (static website)
- CDN & DNS: Cloudflare
- Deployment: GitHub Actions (auto-deploy on push)
- Preview: Automatic branch subdomains
- SEO: Python-generated sitemap +
robots.txt - AI assistance: Claude Code with project-specific scaffolding
Total JavaScript: 0 bytes. Total frameworks: 0. Total joy: immeasurable. 🕺
Thanks for reading! If you want to peek behind the curtain, the entire source code is on GitHub. PRs welcome, especially if you know how to fix that banner image hack. 😅