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/viteplugin. The whole design system is a single@themeblock insrc/styles/global.css, so utilities likebg-console,text-cyanandborder-slatecome 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.