build-log

I Built the Site You're Reading, and Then Redesigned It Three Times

Milo ·

Every other build log on this site is about something else. A security monitor. A Minecraft server panel. My brother’s portfolio. This one is about the thing you’re looking at right now.

The site came out of a gap. I got into building with AI after hearing about it on podcasts, and when I went looking for reviews and guides, every single one was written for people who already knew what a linter was. (A linter is a tool that reads your code and flags sloppy formatting and likely mistakes. If you didn’t know that, you weren’t the audience for those articles, which is exactly the problem.) I didn’t need any of that. I needed to know whether a person who can’t code could actually make something real with these tools.

So I started writing the thing I couldn’t find. At the end of the very first build log I published here, back in March, I wrote that I was already using Claude Code to build “this very website.” That was true. This site has always been one of the builds.

It’s been live since March 11, 2026. Here’s what’s actually under it, including the parts that broke.

What This Site Actually Is

There’s no database. There are no accounts. Nothing on this page was assembled when you asked for it.

That’s called a static site. Every page is built into a plain file ahead of time, and the web host’s only job is to hand that file over when your browser asks. Compare that to something like WordPress, where the server runs a program and queries a database on every single visit. Static is faster, it costs almost nothing, and there’s very little on it to attack, because there’s barely anything running.

The pieces:

  • Astro, a website framework (a toolkit that turns your content and templates into finished pages). It takes my articles and my page templates and produces a folder of plain HTML files.
  • Tailwind CSS for styling. Instead of writing a separate stylesheet, you attach small named styling classes directly to the page elements.
  • Markdown for the articles. Markdown is a plain-text way of writing formatted documents: a # makes a heading, asterisks make bold. Every article on this site is one text file.
  • GitHub for storage and history. A repository (repo) is a project folder with a complete recorded history of every change ever made to it. This one has 118 recorded changes so far, all mine.
  • GitHub Actions for the automation that puts new work on the internet.
  • A shared cPanel hosting account at Spaceship where the finished files land.

That’s the whole stack. Nothing exotic, and that’s deliberate.

Three Days From Empty Folder to Live

The first recorded change is March 8, 2026: Astro, Tailwind, the article system, three seed articles, and the plumbing that makes a site findable. That last part is three separate things, and they’re worth naming properly:

  • Search and social tags. Hidden lines at the top of each page that tell Google what the page is about and tell Facebook, Bluesky, and the rest what title, description, and picture to show when somebody posts the link.
  • An RSS feed. A machine-readable list of everything I’ve published. Anyone can paste this site’s address into a reader app and get new articles delivered, with no algorithm in between deciding whether they see it.
  • A sitemap. One file listing every page on the site, so search engines can find pages that nobody has linked to yet instead of stumbling on them by luck.

The same evening I set up the deploy (more on that in a second), added the security hardening, and then did something I’d recommend to anyone launching anything. I put a “coming soon” page up as the public homepage and parked the actual site out of sight.

The way that works: the repository has branches, which are parallel versions of the project. The one called main is the version that goes live automatically. So main got the coming-soon page, and the real site sat on a second branch called dev where the world couldn’t see it. The commit message I left for myself says it plainly: “Full site remains on dev branch. When ready to launch, merge dev to main.”

That bought me three days to finish writing without a half-built site sitting on the open internet.

On March 11 I finalized the first build log, merged everything into main, and the site went live. The next changes recorded that same day are all mobile fixes: the collapsing menu, buttons wrapping badly, tap targets too small to hit with a thumb. The launch and the first round of “actually, on a phone this is broken” happened within hours of each other.

What Happens When I Publish an Article

Writing an article means creating one Markdown file in a specific folder. At the top of the file is a block called frontmatter: a few lines of settings between two --- markers. Title, description, date, category, tags, and a draft flag.

That block isn’t decorative. The site has a schema, which is a checklist the build enforces. The category has to be one of exactly four values (review, comparison, guide, build-log). The date has to be a real date. The title and description can’t be missing. If I get any of it wrong, the site refuses to build at all instead of quietly publishing a broken page. I’ve been saved by that more than once.

draft: true keeps an article out of the finished site entirely. This post has that flag set right now, which is why you’re either me or someone I sent it to.

Once the file is written, publishing is: save the change to the repository, push it to GitHub, and walk away. From there GitHub does the rest:

  1. Grabs a fresh copy of the project onto a temporary computer it rents me for a few minutes.
  2. Installs the project’s dependencies, the outside software packages my site is built out of, at exactly the versions written down in the project. Recording exact versions like that is called pinning. It means the build that runs today produces the same site as the build that ran in March, instead of quietly picking up whatever somebody else released last night.
  3. Runs the build step, the command that turns Markdown and templates into a folder of finished HTML files.
  4. Saves a copy of that finished folder as an artifact, a downloadable snapshot kept for seven days. That’s the rollback insurance.
  5. Writes out the deploy key, connects to my web host over SSH (an encrypted connection to another computer), and copies the files across with rsync, a tool that transfers only what actually changed and deletes anything on the server that’s no longer in the build.
  6. Deletes the key.

That’s the entire publish. I write a file, save it, push it, and the automation takes it from there without me opening a connection to the server myself.

Most of the time. On August 7 I had to do it by hand, and that turned out to be the most reassuring thing that’s happened to this site all year.

The Morning GitHub Went Down and I Deployed by Hand

While researching this very post I found a real bug in the site, and it had been sitting there since March.

The server config had been pointing “page not found” errors at a file called /404.html since the very first commit. No such page was ever built. So every mistyped address on this site fell through to the host’s generic error screen instead of anything that looks like NonDevBuilds. It was worse than that, actually: asking for /404.html directly came back as an HTTP 500, which is the code for “the server itself broke.” The server went looking for its own error page, couldn’t find it, and then tried to report that failure using the page it had just failed to find. A loop. Quietly wrong for five months, and invisible unless you went looking, because nobody emails you to say your 404 page is ugly.

So I wrote the page, saved the change, pushed it, and waited for the deploy. Nothing happened. Not a failure, not a red X. Nothing ran at all, because the part of GitHub that watches for pushes was down. Actions and Pages were both in major outage that morning, for about six hours. I tried to start the deploy manually from the command line instead, and GitHub’s own interface answered with an HTTP 500 of its own. Twice.

I had a five-month-old bug fixed on my laptop and no automation left to ship it with. So I shipped it myself: ran the build locally, copied the finished folder up to the server with rsync using the same restricted deploy key the automation uses, and loaded the site to confirm the new page was actually live. Three commands, one of which was “look at it.”

The reason that was even possible is the part worth taking away. My deploy isn’t a pipeline in any meaningful sense. It’s “build a folder, copy the folder up,” and GitHub Actions is just a rented computer running those two steps so I don’t have to be awake for them. When the rented computer vanished, the fallback was the same two steps done by a person. If I’d built the deploy around some service that only GitHub knew how to talk to, I’d have spent six hours holding a finished fix with no way to ship it.

The Deploy Broke Three Times in One Evening

This is the part that actually taught me something.

The first version of the deploy used FTPS, which is the old file-transfer protocol with encryption bolted on. It works, but it means storing a password that has full access to the hosting account, and the connection had to be trusted rather than verified. I replaced it the same evening with SSH keys and rsync.

A key here is a pair of matched files instead of a password. One half lives on the server, the other half is stored encrypted at GitHub.

The half on the server isn’t a general-purpose login. It’s registered with a wrapper that catches whatever command arrives and hands it to a small validator script before anything is allowed to run. That script permits exactly one thing: a file copy into this site’s own folder.

I got to prove that on the day of the hand deploy, and I proved it by being told no. The rsync worked: the files went up, the new page went live, so the copy-files path is definitely open. Then I tried the other thing you’d normally do with a key, which is open an ordinary session on the server, the kind where you get a prompt and can wander around. The server refused, and it refused with an actual sentence:

Rejected: only rsync to nondevbuilds.com is allowed

That is not my setup notes describing what should happen. That is the validator script itself, on the live server, turning me away, using the very same key that had copied files up successfully minutes earlier. One key, one permitted action, and I no longer have to take my own word for it.

So the blast radius of a leaked deploy key is this site’s files, not the whole hosting account, and the other sites sharing that host aren’t reachable with it. That’s a much better place to be, but I want to be honest about what it still means: anyone who could overwrite these files could serve something harmful to people reading the site. It’s a smaller fire, not no fire.

Good, except it then broke three times in a row.

GitHub has a safety feature where any secret value that shows up in the logs gets replaced with ***, so you can’t accidentally publish your own password by printing it. I turned that on for the hostname and the port too, which felt like the careful thing to do.

The deploy also uses a file called known_hosts. It records the fingerprint of my server, so the deploy refuses to hand over anything if it ever connects to a machine claiming to be my host but isn’t. It’s the protection against someone impersonating the server.

The masking applies to everything in the job, not just the printed output. So the moment the hostname got written into known_hosts, GitHub helpfully replaced it with *** inside the file. The fingerprint file was now corrupt, and the deploy refused to connect to anything at all.

Three fixes in a row, and my commit messages read like a man slowly understanding his own mistake:

  • “Fix SSH deploy: split setup from masked deploy step”
  • “Remove redundant add-mask, GitHub secrets are auto-masked”
  • “Use ssh-keyscan instead of stored known_hosts secret”

The real fix was the third one: stop storing the fingerprint as a saved secret, and instead go and ask the server for it fresh at the start of every deploy. Three days later I put the masking back, this time applied before the values are ever used and passed in as proper variables rather than pasted into the script text.

A security feature broke a security feature. That is a very normal way for this stuff to go wrong, and nothing about it required knowing how to code. It required reading the error, forming a guess, and testing the guess.

Security on a Site With No Login

There’s no login here, no comments, no shopping cart, nothing to steal. It would be easy to argue this site doesn’t need security at all.

I set it up anyway, and it cost one evening, because I copied it wholesale from another site I run. The commit is literally called “Add security hardening to match claude-blog standards.” That’s the recipe from itzmater.ai, dropped in here in one go. Every site I’ve built since has started from the same recipe.

Two pieces do the work.

The .htaccess file. That’s a small config file the web server reads for the folder it sits in. Mine tells the server:

  • Don’t guess at file types, use exactly what I declared. (Guessing is how a text file gets treated as a program.)
  • Never allow this site to be loaded inside a frame on another site. That stops a technique where an attacker invisibly stacks their page on top of yours and harvests the clicks.
  • Don’t leak the full page address of where a visitor came from when they click a link out.
  • Turn off access to the visitor’s location, camera, and microphone entirely.
  • Always use HTTPS, the encrypted version of the connection, for a year at a time, including on any subdomain.
  • Never show a listing of files in a folder that has no index page.
  • Refuse to serve any file whose name starts with a dot. Those are the hidden config files, and that rule includes the .htaccess file itself.

There’s one header deliberately turned off: the old browser cross-site-scripting filter. It’s disabled on purpose because that filter turned out to cause more problems than it solved, and modern browsers dropped it. Setting it to zero is the current correct answer, which is a good reminder that copying a five-year-old security checklist off the internet isn’t the same as having a current one.

The Content Security Policy. A CSP is a short list, sent along with the page, of what that page is allowed to load and from where. Mine starts from “nothing is allowed” and then opens up the narrowest set of exceptions: scripts, images, styles and fonts from this site only, no forms, no framing, nothing from anyone else’s server.

If a stray script ever ended up in one of my pages, the browser would refuse to run it. It’s a seatbelt for a crash I don’t expect.

That policy has changed how the site gets built, twice:

  • When I added a smooth-scrolling library to the site, it couldn’t be loaded from a public code-hosting service the way most tutorials tell you to. It had to be bundled into the site’s own JavaScript file so it counted as coming from here.
  • When I self-hosted a display font for the redesign, the policy blocked it, because the original list didn’t mention fonts at all. Adding font-src 'self' to allow fonts from this site was part of that release.

That’s the trade. A strict policy makes some things more annoying to add, and in exchange the browser refuses to fetch scripts, styles, fonts, or images from anywhere but this site. That shuts down the most common way a page gets hijacked, which is quietly pulling in code from somebody else’s server that has since been tampered with.

It is not a force field, and I’d rather say so than oversell it. The policy can’t do anything about an extension running inside your browser, and it can’t tell good code from bad if the bad code is sitting in a file I served you myself. It narrows the ways in. It doesn’t seal them.

The rest of the posture is in the automation. The actions the deploy uses are pinned to exact commit fingerprints rather than to version names like “v6,” so a compromised update to somebody else’s tool can’t silently change what runs against my server. The deploy job is granted read access and nothing more. Only one deploy can run at a time, so two pushes in quick succession can’t overwrite each other mid-copy. And every sensitive value (the host, the user, the port, the destination folder, the key) is an encrypted secret masked out of the logs, which is why this post can describe the whole pipeline without naming the server.

There are two other automations worth mentioning:

  • Rollback. I can trigger a workflow by hand, give it the ID of any deploy from the last seven days, and it downloads that exact saved snapshot and puts it back on the server. I have never had to use it. That is not a boast, it is the whole argument for building it: it cost four lines and one evening, it has sat there doing nothing for five months, and the one morning I need it I will not be in a state to write it.
  • A rotation reminder. On the first day of every third month, a scheduled job opens a to-do item on the project telling me to replace the deploy key. It’s a recurring calendar reminder that lives in the project instead of in my head.

Redesigned Three Times

The site has been through three visual redesigns. Two of them happened twenty-three minutes apart.

March 15, 4:45pm. Gradient hero background with a dot grid, an animated badge, a shimmering border on the featured article, glowing cards, a sticky frosted-glass navigation bar, and a downloaded font. Every trend at once. Its own commit message lists all of it proudly, one bullet at a time. It was built on the dev branch and merged into main eight seconds later, which means it went live: it was not an experiment left parked out of sight where nobody would see it. It shipped.

March 15, 5:08pm. Twenty-three minutes later, I deleted most of it. I don’t have to reconstruct why, because I named it at the time. The replacement is titled “Redesign: clean typography, white space, no trends,” and the note under it reads: “Strip everything back to fundamentals. Light theme, system fonts, generous white space, clear hierarchy. No gradients, no glow, no animations. Fast load times, zero external fonts.”

The size of that change says the rest. It added 156 lines and removed 379. The stylesheet, the one file that holds nearly all of a site’s visual design, lost 259 lines on its own. That is not a tweak or a walk-back. It is most of an afternoon’s design being taken back out. It also dropped the downloaded font in favor of the ones already installed on your device, which means the page now makes no font request at all. Nothing to wait for, nothing to fail. I never measured the before and after, so I’m not going to put a number on it.

Then, at 6:39 that evening, I wrote the rule down. A line went into the project’s notes file, the plain-language document that every future change on this site gets checked against: “No gradients, no glow effects, no flashy animations.” It is still sitting there today, and it still decides what this site is allowed to look like.

That line is a scar from those twenty-three minutes. I built the trendy version, put it live, looked at it, took most of it back out inside half an hour, and then left myself written instructions so I wouldn’t build it again. I can’t tell you what I felt at 5:07pm, and I’m not going to invent it. What the record shows is what I did, and what I called it while doing it: no trends.

April 1: dark mode. If your computer is set to dark mode, this site is dark, and there’s no toggle and no JavaScript involved. Every color is defined as a named variable, and a single rule swaps the values when the browser reports a dark preference. No markup changes, no flash of the wrong theme.

May 22 and 23: the brand. This one got a written design spec before a single line changed, and the spec is still in the repo. Its stated goal was to make the landing page “distinctive and memorable instead of safe-and-forgettable.” It also had a hard requirement I like a lot in hindsight: it must not look like itzmater.ai, the other site I run, which already owns the terminal-and-monospace aesthetic. Two of my own sites looking like each other is a real risk when the same person builds both in the same month.

What came out of it is what you’re looking at: a heavy display font called Tanker (self-hosted, per the CSP), a denim blue accent, the “Not a developer? Build it anyway.” headline, the strip of three pillars under it, uniform text-forward cards instead of a ragged grid of some-have-images-some-don’t, a new start here page, and a new logo.

The spec also raised an accessibility concern, and this next part is the most useful thing in this whole post, so stay with me.

Contrast ratio is a measurement of how strongly text stands out against the background behind it. Black on white is about 21 to 1. Light grey on white is maybe 2 to 1, which plenty of people genuinely cannot read. The accepted minimum for normal-size text is 4.5 to 1, and it exists because “looks fine to me on my monitor” is not a standard.

The spec flagged the new denim blue as borderline against the light background, put a number on it (about 4.3 to 1, which would fail), and specified a fix: use the darker denim for links inside articles, and save the brighter one for big headlines and filled buttons where a lower ratio is acceptable.

Nobody ever applied that fix. Links in the body of every article on this site are still the brighter denim, and the darker value only ever shows up when you hover.

So while writing this post I stopped and actually measured the colors that are live in the stylesheet today, rather than trusting the number in the plan. Every figure below is measured, not quoted:

WhereMeasured ratioAgainst the 4.5 minimum
Light mode, link on the page background4.70 to 1Passes
Light mode, link on a card5.04 to 1Passes
Dark mode, link on the page background7.61 to 1Passes
The darker denim the spec wanted (light mode)6.35 to 1Passes, with more room

Nothing on this site is failing that standard today, in either light or dark mode. The two numbers do disagree, though. The spec says about 4.3 to 1 and the stylesheet measures 4.70 to 1, and I can’t account for the difference. The measured one is the one that counts, because it comes from the colors that are actually live on the page you’re reading. Beyond that, I don’t know why the plan says something else, and I’d rather leave a flat “I don’t know” here than a tidy explanation I made up.

I’m keeping this in the post because the lesson is better than a clean win would have been. A written plan said “check this, and here’s the fix.” Nobody circled back. It turned out fine, but it turned out fine by luck, not because any part of my process caught it. The measurement I should have run in May is the one I ran today, writing an article. That’s the gap worth closing, and it isn’t closed by the numbers happening to land in my favor.

One thing broke in that release, and it’s a funny one. I added momentum scrolling, the effect where the page glides to a stop instead of halting dead. It stuttered. The cause: browsers have their own built-in smooth scrolling, which I’d already turned on months earlier, and the two systems were both trying to animate the same scroll at the same time. The fix was to switch off the browser’s version whenever the library is running. If you’ve told your device you prefer reduced motion, none of it runs at all.

The Small Bugs Worth Remembering

The date was off by one. For a while, an article dated March 27 displayed as March 26. Dates in the frontmatter have no time attached, so they’re treated as midnight UTC, and the code was then formatting that moment in the local time of whatever computer ran the build. Midnight UTC in a timezone behind UTC is still the previous evening. The fix was one line: format the date in UTC too. Any site that builds in one timezone and is read in another can hit this.

I couldn’t preview my own drafts. Unpublished articles are excluded from the built site. Fine. Except the same rule was also excluding them from my own local preview, so the only way to see a draft as a real page was to publish it. In May I gated that: drafts appear when I’m previewing on my own machine, and are still stripped out of anything that reaches the internet.

The midnight publisher. I wanted the Cursor review to appear at midnight rather than whenever I happened to be awake. So there was a one-off scheduled job whose entire life’s purpose was to wake up at midnight Central on March 15, change draft: true to draft: false in one file, save it, merge it, and trigger the deploy. It worked, published the article, and then sat in the project doing nothing for two months until I deleted it. I left a note in the original commit saying it was safe to delete, and past me telling future me “you can throw this away” turned out to be worth more than the automation itself.

The em dash purge. In May I went through all twelve articles and removed every em dash, replacing them with commas, colons, parentheses, or just two shorter sentences. 272 lines changed. The em dash is the punctuation mark that AI-written text leans on hardest, and once you notice it you can’t stop noticing it. If a machine does most of the typing, the tells are the part you have to edit out yourself.

What It Costs

ItemMonthly Cost
Shared cPanel hosting via Spaceship$3.88, and it is not just this site
Domain (nondevbuilds.com) via Spaceship$9.08 a year, so about $0.76
SSL certificateFree (included with the host)
GitHub (private repo + Actions minutes)Free, and I have never gone over the included minutes
Claude CodePart of an existing subscription
TotalAbout $4.64 a month, and the hosting is shared

That is roughly $56 a year, and even that overstates it, because the same $3.88 hosting plan also serves another site of mine on the same account. The domain is the only line item that belongs to this site alone. There is no build service, no content platform, no analytics subscription, no monthly fee for the thing that turns the writing into a website. A static site means the whole running cost is a folder of files sitting on a disk, and disks are cheap.

The Actions minutes are worth a note, since that is where a free tier usually stops being free. A deploy takes about thirty seconds. At two or three articles a week, plus the occasional fix, this site has never come close to the limit. If the site ever gets big enough to cost real money, the thing that got big would be the writing, not the infrastructure.

What I Learned

  • Copy your own security setup between projects. The headers, the policy, the rollback, the rotation reminder: all of it arrived here in one commit from a site I’d already built. It took one evening the first time and near zero every time after. The version of this that fails is the one where you rebuild it from scratch, badly, on each new project.
  • Build the rollback before you need it. Saving each build as a snapshot for a week costs nothing and takes four lines. Deciding to add that after a bad deploy is deciding too late.
  • A boring deploy is one you can still run by hand. When GitHub was down for six hours, the fix went out anyway, because “build a folder and copy it up” is something a person can do with three commands. Automation that hides the steps from you is fine right up until the automation is the thing that’s broken.
  • Pin everything. Exact dependency versions, exact fingerprints on other people’s automation. “Latest” is someone else’s decision applied to your site while you sleep.
  • A security feature can break another security feature. The secret-masking versus the host-fingerprint file is my favorite example. Neither piece was wrong. The combination was.
  • Measure it, don’t quote the plan back at yourself. The contrast note above is the cleanest example I have. A number written down in May went unchecked until August, and the only reason that cost me nothing is that the number happened to land on the right side of the line. Getting away with it is not the same as catching it.
  • This site has no tests, and that’s the honest gap. Every other project I run has a real test suite. Here the only gate is “did it build,” and a build succeeding proves the pages exist, not that they’re correct. It’s a defensible trade for a site with no interactive features, but it is a trade, and I’d rather say so than pretend it’s a design decision.

What’s Next

Researching this post turned up three problems with the site. Two of them are already fixed, which is the best argument I have for making yourself write the thing down:

  • The link colors, described above. Not broken, as it turns out. The measured contrast passes in both light and dark mode. But the spec’s recommendation was never applied, and its 4.3 figure still doesn’t match what I measured. Switching body links to the darker denim would take one line and buy a comfortable margin instead of a narrow one. Still open.
  • The missing 404 page: fixed. That’s the bug in the hand-deploy story above, wrong since the first commit in March and shipped during a GitHub outage five months later. Mistyped addresses now land on a real page in this site’s own voice, with a way back to the homepage.
  • The README described settings that don’t exist: fixed. It documented the article frontmatter using field names (pubDate, heroImage) that don’t match what the site actually uses (publishDate, image). Anyone following my own documentation, me very much included, would have written a file that failed the build. Corrected against the real schema in the same change as the 404 page. Documentation rots the moment you stop reading it. Mine rotted.

Nothing else is planned. No search, no email list, no comments, no per-post preview images. Not because those are bad ideas, but because none of them is the thing this site actually needs.

What it needs is for me to write on it regularly again, which is a habit rather than a feature, and not something I can solve by adding another workflow. The infrastructure has been finished and waiting for months. That is the honest state of it: the part I automated is done, and the part that only I can do is the part that is behind.

Tools I Used

  • Claude Code: the AI coding assistant that did most of the typing, on this site and everything else here
  • Astro: the website framework
  • Tailwind CSS: styling
  • GitHub Actions: the automation that builds and deploys on every change
  • Spaceship: hosting and the domain
  • Tanker: the display font in the headlines

Some links on this page are affiliate links. I only recommend tools I actually use.

claude-code build-log astro github-actions security vibe-coding