Introduction
Hello my name is Connor, I’m a self taught full stack engineer. I first got into software development about 8 years ago. Like many people I started my journey making simple games and eventually found myself working on the web. Professionally I spend most of my time day to day writing React and Go. But I’m no stranger to exploring all the new and exciting toys.
I decided it was time for me to make a portfolio to showcase my work. So naturally like most developers, I spent probably far too much time fighting with myself over the stack. I knew early on I wanted two versions of my site, a standard web version, and a version available over SSH in the terminal. As you might have noticed from my artistic choices I love me a good TUI. But, that choice complicated content immediately.
I needed a central store for basically any content on the page, to make my life easier down the road, and to make sure both front ends were more or less identical aside from styling and interface. So when it came to the stack the natural first choice to solve this problem? Use the stack I’m most familiar with; Basic react frontend, ssh app with Bubbletea and Go backend to host the content. Simple…ish. After thinking about it, no not really. I immediately started to feel like that plan was a little much for a simple portfolio…
Do I really need a backend?
The way I saw it there were two kinds of content to deal with: the site’s copy — my skills, experience, bio — and then the blog posts. Both of these share something important: they don’t change between page loads. Nobody’s leaving comments, there’s no user state, no real-time anything. It’s just me writing things down and you reading them.
Once I framed it that way, a backend started to feel like I was solving a problem I didn’t actually have. So I scrapped it.
All the site content could live in a shared content directory — YAML files for structured stuff like experience and skills, markdown files for blog posts and the about page. Both the web app and the SSH app can read from that directly. Same source of truth, no middleman. And since everything lives in the repo anyway, version control is free, might as well use it to my advantage. Want to write a post? Create a markdown file. Want to update your job history? Edit the YAML file. Push it, rebuild, done. No CMS, no API keys, no database to babysit. Same workflow I use everyday.
So now I had a good direction — two heads, one heart — but one wrinkle remained: the web app is TypeScript and the TUI is Go.
At work we use an OpenAPI schema to generate shared types across the frontend and backend, so both sides agree on the same contracts without anyone having to manually keep them in sync. I thought, well, why couldn’t that same idea work here?
So I defined the content schemas as JSON files and wrote a small script to generate types from them — a TypeScript interface for the web side, a Go struct for the SSH side. Same concept as OpenAPI codegen, just pointed at two frontends instead of a frontend and a backend. Update a schema, re-run the script, both sides are back in sync.
Why Astro?
Now that I had my content model figured out, it was time to make my decision on the important part, what people actually see.
The TUI side was an easy choice, a Bubbletea app hosted with Wish. There are many great TUI frameworks out there, but I don’t think anyone needs to hear it from me, how amazing the Charm ecosystem of tools are for building exactly what I needed here.
The harder choice came for the web side, originally I was thinking standard react app? maybe… but SEO is a pain. I could try NextJS, but the goal here is simple. That’s when I found Astro, somewhat lost in the never ending abyss of frontend web frameworks, but Astro’s approach of complexity only when needed seemed to tick all the right boxes for this project.
[✓] Mostly Static content- making SEO a breeze
[✓] Zero JS by default — no JavaScript is shipped to the browser unless you explicitly put it there
[✓] Server rendering when you need it, for dynamic content without client side bloat
[✓] Bring your Own Framework, for some sense of comfort when things get complex
At the end of the day this is still just a portfolio. But I think that’s kind of the point. Every decision I made here was about keeping things simple without cutting corners — a single place for content, generated types so nothing drifts out of sync, and a framework that does exactly as much as you ask of it and no more. It ended up being a pretty fun little system to build, and honestly working on it gave me a chance to play with some tools I wouldn’t normally reach for at work. That’s always worth something.
Getting it Online
The last piece was deployment. The goal was simple: push to main, site rebuilds, done. I didn’t want to think about it too much.
Both apps are containerized with Docker — each gets their own image, sharing the same content directory via a bind mount so neither app needs to know the other exists.
A GitHub Actions workflow handles the rest. On any push to main it SSHs into my VPS, pulls the latest, and runs a deploy script — new images get built, containers restarted, and anything stale gets pruned. The whole thing wraps up in under a minute.
Last, I wired up Discord notifications with a simple webhook. That gives me near real-time updates on the deployment as things happen, good or bad. That’s the whole loop. Simple, effective, effortless to maintain.
If you want to dig into any of the details, the full source is up on GitHub. Thanks for reading.