jdtechprojects>_jdtechprojects

jd@console:~/blog$ cat how-this-site-is-built.md

How this site is built

A walkthrough of the stack behind jdtechprojects.net: Astro, Tailwind v4, a Node server on Railway, and two scheduled agents that publish the Houston events page.

$ published

  • astro
  • tailwind
  • railway
  • automation

This site is small on purpose. It has to do three things well: list my projects, publish a couple of event digests a week without me touching it, and look like a terminal while doing it. Here is what runs underneath.

The stack

  • Astro with output: 'static'. Every page is prerendered at build time. There are no client-side framework islands; the only JavaScript on the site is a few lines that close the mobile menu after you pick a link.
  • Tailwind CSS v4 through the @tailwindcss/vite plugin. The whole design system is a single @theme block in src/styles/global.css, so utilities like bg-console, text-cyan and border-slate come straight from the color tokens.
  • Self-hosted fonts. JetBrains Mono for anything that reads like a prompt, Inter for headings and body copy. Both ship as variable fonts from @fontsource-variable/*, so the site makes no third-party font requests.
  • @astrojs/node in standalone mode serves the built output. It is still a static site; the Node adapter is just a convenient way to host it on Railway.

Deploying to Railway

Railway builds the site with Nixpacks and runs the server entry that Astro emits:

{
  "build": { "builder": "NIXPACKS", "buildCommand": "npm run build" },
  "deploy": { "startCommand": "HOST=0.0.0.0 node ./dist/server/entry.mjs" }
}

Railway injects PORT and the adapter reads it automatically. A GitHub Actions workflow runs astro check and a full build on every push and pull request, so a type error or a broken import never reaches the deploy step.

The terminal concept

Section headers are written as shell prompts, like jd@console:~/projects$ ls -la, and the brand mark is a >_ cursor that blinks unless you have asked your OS for reduced motion. Project cards carry a breadcrumb path (~/projects/macos) and a status tag ([ ACTIVE ], [ IN PROGRESS ], [ ARCHIVED ]). The 404 page answers with bash: cd: No such file or directory. It is a gimmick, but it is a consistent one, and it keeps the copy short.

The Houston page writes itself

The /houston page is the part I am happiest with. Two scheduled Claude agents run on a routine: one on Monday morning for the week ahead, one on Friday morning for the weekend. Each researches public event listings for the greater Houston area and commits a JSON digest into the repository:

{
  "kind": "weekend",
  "title": "Houston This Weekend",
  "rangeLabel": "Fri Oct 9 – Sun Oct 11, 2026",
  "generatedAt": "2026-10-09T13:05:00Z",
  "events": [{ "name": "…", "date": "2026-10-10", "hook": "…", "url": "…" }]
}

A small validation script in scripts/ checks the shape of each file before it is accepted. The page reads both digests at build time, sorts them freshest first, and groups events by day. When a digest is committed the site redeploys, so the agent’s research is live within minutes with no manual step in between.

What’s next

The blog you are reading is the newest addition, along with a detail page for each project. Posts are plain Markdown files in a content collection, and there is an RSS feed at /rss.xml if you would rather not check back by hand.