notes

Shipping this site: Astro, GitHub and Cloudflare

From an unused domain to a working personal website.

What I wanted:

I registered darrenking.ai on 8 June 2026. By 26 September 2026 (a solid 3 months later), I had managed to build absolutely nothing.

I had ideas, references and a fairly clear picture of what I liked. Unfortunately, I also have a tendency to make things so important that starting them becomes unnecessarily difficult. A personal website had somehow become something that needed to be perfect before it could exist at all.

What helped was making the first task much smaller. I defined it as being one homepage that opened on my Mac, contained genuine information, looked reasonable and passed its tests and checks. Get that into Git, then get it online. The rest could wait.

The brief was a small online home for projects and notes about AI, software and Linux (and maybe a bit of medicine as that is my background). I didn’t need a comprehensive portfolio on day one. Having had a look around I liked the Unix personality of YSAP, the green-on-black feel of HAMY, and the simplicity of Cyber Notes. So I borrowed some of those design principles without reproducing somebody else’s entire website. Inspiration rather than plagiarism!

That became a near-black background, warm off-white text, green accents and monospace headings. Terminal-inspired, but with ordinary navigation (for now). Nobody should have to work out a pretend shell command to find my email address (yet).

Technically, the constraints were just as deliberate: static pages, minimum dependencies, no database. no login and content that worked without client-side JavaScript. Most importantly I wanted publishing to be straightforward as well.

What I built:

The site uses Astro with static output, TypeScript using Astro’s strict preset and plain CSS. Astro suited a site centred on writing and project pages without requiring a client-side framework runtime. There was no reason to add React, some complex styling framework or a backend just to introduce myself. (They may come in time).

I used ChatGPT to work through the choices, write the initial brief and break the project into milestones. As front-end isn’t my strong suit and given Claude can do it better I used Claude Code to help implement it locally. I reviewed the output, dug into the many bits I wasn’t familiar with and made changes myself where I could. Part of the point of this was learning how the stack fitted together, rather than just ending up with a page that happened to work.

The project-root CLAUDE.md captured the brief and working boundaries. One particularly useful instruction I used was to finish the requested milestone before suggesting more features. That was as much a reminder for me as it was for Claude.

The first homepage was organised around a handful of files:

  • src/pages/index.astro: the introduction, selected work, notes and contact.
  • src/layouts/BaseLayout.astro: the shared HTML shell, metadata, skip link, header and footer.
  • src/components/SiteHeader.astro: the site name and navigation, with a small ~ $ flourish.
  • src/config/site.ts: site details and optional contact links, without publishing placeholder addresses.
  • src/styles/global.css: the shared styling, with CSS custom properties for the palette, fonts and spacing.

The fonts use a system stack, so there are no external font downloads. KISS. The styling also respects reduced-motion preferences by disabling smooth anchor scrolling when requested.

There was one small ecosystem adjustment for me. When it comes to dependencies I normally use uv as my work is typically based around Python projects. With this being a JavaScript/TypeScript project npm manages its dependencies. Familiarity with a tool is not, on its own, a reason to add it.

Locally, I use four npm scripts:

npm run dev      # start the development server
npm run check    # check Astro and TypeScript files
npm run build    # generate the static site in dist/
npm run preview  # preview the generated production build locally

How publishing works:

The site is hosted on Cloudflare Workers Static Assets. Despite the name, this setup does not need a Worker script, the Astro Cloudflare adapter or server-side rendering. Astro takes all the source files and turns them into ordinary static website files in dist/ when I run npm run build. The dist/ folder then contains the actual HTML, CSS and any other static assets that a browser needs. Cloudflare serves the files. Because nothing needs to be generated dynamically when someone visits the site, I do not need server-side rendering, a Cloudflare-specific Astro adapter or custom Worker code. I know I’ve mentioned this but it’s a point worth repeating. None of this will be news to a front-end developer, but I learned web development much closer to the HTML-and-CSS end of things, so getting my head around the modern build and deployment workflow has been part of the fun.

Wrangler is Cloudflare’s command-line tool for deploying Workers and static assets. In this particular project it doesn’t have much to be doing (which is exactly as I want it). Moreover Wrangler itself is installed locally rather than globally. I prefer it that way as the project then brings its own development tool with it instead of relying on future me remembering which version I installed on my Mac six months ago. The minimal assets-only configuration in wrangler.jsonc is as below with domain settings handled separately:

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "darrenking-ai",
  "compatibility_date": "2026-09-26",
  "assets": {
    "directory": "./dist"
  }
}

Deployment runs through Cloudflare Builds connected to the GitHub repository and listening for pushes to main:

Step Command
Build npm run check && npm run build
Deploy npx wrangler deploy

The separate check matters because astro build does not perform type checking. Instead it transpiles the code with esbuild without running type checking (as I learned). A failing check stops this build-and-deploy sequence. It does not catch every bug or prove that a page looks right, we still need a human in the loop; I still need to inspect the result.

The workflow is now:

Edit locally → preview → check and build → commit → push to GitHub
    → Cloudflare checks and builds → successful deployment → live website

Those are still separate steps. A local commit does not publish anything, and a push triggers the deployment process rather than guaranteeing its success. Uploading commits also does not automatically make the source public: that depends on the repository’s visibility. Deployment authorisation belongs in Cloudflare’s build settings, not in credentials committed alongside the site.

To test the whole process, I rewrote the introduction, previewed it locally, committed and pushed the change and saw the new wording on darrenking.ai. That was more satisfying than the first deployment as I now knew how to update the site, not just how to get it online once.

Hiccups along the way:

Workers without a Worker (and with zero bindings)

This was probably the main Cloudflare oddity for me. I had considered “Workers” as code running at the edge, so using Cloudflare Workers for a site with no Worker script at all felt slightly backwards.

In this case, though, the Worker is really just serving the contents of dist/. There is no server-side rendering, no application logic and, as such, no need for any bindings.

That last part made me wonder if I had missed something. The Cloudflare dashboard showed the domain pointing at my Worker alongside an ominously empty Bindings 0 box. It looked suspiciously like unfinished configuration. It wasn’t. Bindings are for giving a Worker access to things like KV, D1, R2 (Cloudflare’s edge storage tools).

For a static assets-only site, Cloudflare can serve the generated files directly. An ASSETS binding is optional and is only needed if Worker code itself needs programmatic access to those files. Since I do not have a Worker script in the first place, adding one purely to make the dashboard look busier would have been solving a problem I didn’t have. Who knows, I may have a reason to add a binding later. For now, zero is the correct number.

Once I understood that, the setup became much simpler in my head: Astro builds the site into dist/ and Cloudflare serves it. (Again, may seem obvious to most but I was getting my head around an unfamiliar framework and platform).

What actually happens when I push to main

Understanding git commits and pushes wasn’t the problem. What I wanted to get clear was where GitHub stopped and Cloudflare started.

The answer is actually really simple: I push to main, Cloudflare sees the change, runs the check and build commands and deploys the resulting dist/ directory. So while commit, push, build and deploy are still technically separate things, the workflow, from my point of view, is now basically:

edit → check → commit → push → live

There is one important consequence here though; I now have to treat main as production rather than somewhere to push half-finished changes.

Why the domain worked but www did not

Attaching darrenking.ai as a Custom Domain does not automatically attach www.darrenking.ai: Cloudflare matches those hostnames separately.

I ended up adding a proxied DNS record for ‘www’ and a permanent redirect back to the root domain. Nothing difficult but it was one of those small pieces of plumbing that requires figuring out how Cloudflare expects things to be done; specifically it required this wildcard rule:

Request URL:           http*://www.darrenking.ai/*
Target URL:            https://darrenking.ai/${2}
Status code:           301
Preserve query string: Enabled

The first wildcard covers the difference between HTTP and HTTPS; the second captures the remainder after the hostname and slash. That is why the target uses ${2}, not ${1}. This follows Cloudflare’s documented pattern. The DNS gets the request to Cloudflare and the redirect changes the browser’s URL.

The extra workers.dev address.

Cloudflare also gives the Worker its own workers.dev URL. Useful while getting things running but once darrenking.ai was working I didn’t really need a second public address for the same site. To disable that route through subsequent Wrangler deployments, the setting is "workers_dev": false in the wrangler.jsonc file (not just a dashboard toggle). Cloudflare documents that distinction. It is a configuration choice rather than a prerequisite for a working custom domain.

Local Node versus the hosted build.

The project declares Node 24 in .nvmrc and package.json and Cloudflare can read .nvmrc when building. I can still run the site locally under a newer version of Node, but at that point I’m testing under a slightly different runtime from the one doing the production build.

For a site this simple it may never matter but keeping the two aligned removes one more variable from the inevitable ‘well it works on my machine’ conversation.

The first milestone was a working local homepage. The second milestone was a live site with an update process I had tested and documented. This note is the third milestone: actually using the thing I have built.

Obviously the site is not finished. The useful difference is that it no longer needs to be finished before it can exist. And it now exists. Hoorah!