Why I built my personal site in the GOTH stack

· 9 min read

image

If you've been anywhere near web development circles recently, you've probably heard the growing murmurs about returning to server-rendered HTML. For years, the industry default has been to reach for a heavy Single Page Application setup - React, Next.js, Vue, whatever the flavor of the month is, even for a website that's basically a blog with some book data.

I built my personal site with the GOTH/GOTHA stack from the start, mostly because I wanted to see if the grass was actually greener on the other side. I kept seeing people rave about HTMX and Alpine.js, and since Go is already my 'go-to' language for backend work, pairing it with Templ for templating felt like the obvious, low-friction way to try the idea out. So I went all-in on what people are calling the "GOTH"/"GOTHA" stack - Go, Templ, HTMX, Alpine, and my own site became the guinea pig.(I guess my site can be called a goth baddie now!)

This is what that experience actually felt like: what changed in how I think about building a web app, what got easier, and what fought back the whole way.


The Motivation: Go Was the Anchor, Everything Else Just Tagged Along

The starting point for this project wasn't "I want to try hypermedia-driven development" in the abstract, it was much more practical than that. I already default to Go for backend work because I like its simplicity, its speed, and the fact that a single compiled binary is basically the entire deployment story. The question I was actually asking myself was: do I even need a separate JavaScript frontend, or can Go just carry the whole thing on its back?

That's what pulled me toward HTMX and Templ specifically. I wasn't shopping for a frontend framework, I was seeing how far I could stay inside the Go ecosystem before I had to type npm install and accept my fate. Alpine.js came in almost as an afterthought, for the small sprinkle of client-side interactivity that even the most server-rendered site on Earth still needs.

My actual requirements were pretty modest: a fast, dynamic-feeling site, with none of the overhead of node_modules, build pipelines, or a client-side state store quietly judging me. No SPA framework, no hydration step, no JSON contract to babysit between frontend and backend. Just Go, all the way down.

The Mental Shift: The Server Becomes the Single Source of Truth

If you've spent some time in React or Next.js, you're trained to think in JSON. The server returns data, the client parses it, shoves it into some global store, and a component tree re-renders based on that state. Your job as a developer is largely about keeping that client-side model in sync with reality, forever, like a very needy houseplant.

HTMX just deletes that whole middle layer. You stop thinking in JSON and start thinking in hypermedia.

Concretely: instead of an API endpoint that returns a JSON array of blog posts, my Go backend returns the actual HTML for those posts - rendered, styled, done. HTMX intercepts the request, grabs the HTML fragment, and swaps it into the DOM. The user experiences something that feels exactly like a snappy SPA transition. As the developer, I've written nothing more exotic than a server-rendered page and one hx-get attribute.

1<div hx-get="/posts?page=2" hx-target="#post-list" hx-swap="innerHTML">
2  Load more posts
3</div>

image

There's no client-side model of "what a post looks like" living anywhere. There's exactly one model of a post, and it lives in Go. That single fact cascades into pretty much everything else that felt different about this project.

What Was Easier

1. End-to-end type safety, without the SPA tax

Templ compiles your templates into actual Go code, so instead of the usual Go html/template experience, where a typo in a field name blows up at runtime, ideally in production, you get a compile error instead. Combined with sqlc generating typed query functions from my SQL, I ended up with an unbroken chain of types from the database column all the way to the rendered <td>. If the project compiles, I know there isn't a nil pointer or an undefined field lurking somewhere. Most SPA setups only get that guarantee by bolting on TypeScript, GraphQL codegen, and a client-side schema validator — three tools doing what one language did here for free.

2. State management basically disappears

The most tedious part of SPA development is keeping client state and server state on speaking terms. Optimistic updates, cache invalidation, stale-while-revalidate logic, an entire subfield of frontend engineering exists purely to paper over the fact that there are two copies of your data, and they occasionally have disagreements.

With this stack, there's only one copy. The database is the state. When a user clicks a button, the server recomputes the truth, renders it as HTML, and HTMX drops it into the page. I never wrote a useEffect, never reached for Redux or Zustand, never had to debug why the UI and the database were telling two different stories. An entire category of bugs simply doesn't get the chance to exist.

3. Deployment became a non-event

Shipping a Node app usually means installing dependencies, running a build step, and keeping a Node runtime alive somewhere in production. My entire site - routes, templates, business logic, compiles into a single static binary. No node_modules folder that could double as a small planet, no separate build artifact to keep in sync with source, no runtime version to babysit. Containerizing it was almost boring: the Docker image is tiny because there's nothing in it but a binary and some static assets.

4. Alpine.js is exactly as much JavaScript as I needed

For things that genuinely don't need a server round trip - a dark mode toggle, a mobile nav menu, a dismissible banner, HTMX is the wrong tool, and a full framework would be like bringing a forklift to move a coffee mug. Alpine fills that gap nicely. It lets me drop small, declarative bits of reactive state directly into HTML attributes, no build step, no bundler. It's got the immediacy of old-school jQuery with the readability of Vue's template syntax.

5. Debugging got dramatically simpler

This one genuinely surprised me. When something looks wrong in a React app, you're often chasing state through a component tree, a few re-renders, and possibly a browser extension having an opinion. Here, "view source" is the debugging tool. What the server sent is what's on the page — no hydration mismatch, no virtual DOM to reason about, no "why did this render three times" mystery novel. If the HTML is wrong, the bug lives in one Go function, and it's usually obvious which one.

6. Performance came almost for free

No SPA framework runtime to ship, no hydration cost, no client-side router. The JavaScript payload for the whole site is HTMX plus Alpine, and both are almost comically small: HTMX comes in at around 14 KB minified and gzipped, and Alpine adds roughly another 7-8 KB on top. Together that's somewhere around 22 KB of JS for the entire site's interactivity layer, before any app code, a bare React + ReactDOM setup alone is already several times that. Lighthouse scores and first-contentful-paint times improved without me doing any deliberate performance tuning — the architecture just doesn't create the problems those metrics usually measure.

What Was Genuinely Difficult

1. Unlearning the SPA reflex I didn't even know I had

Even though I never built an SPA version of this site, years of building other things the "fetch some JSON" way had wired a reflex into me. My default move was still "build a JSON endpoint, fetch it on the client", muscle memory doesn't care that this project started fresh. Catching myself mid-thought and rewriting that as "just return the HTML fragment" took real, conscious effort for the first week or two.

2. Loading states and page-transition feel

Because HTMX requests happen via fetch under the hood, the browser's native loading spinner never shows up, there's no full page navigation to trigger it. In an SPA, you'd just flip an isLoading boolean and call it a day. Here, I had to wire up a top-of-page progress bar (NProgress) manually, hooking into HTMX's htmx:beforeRequest and htmx:afterRequest events so the site felt responsive instead of silently doing nothing for a few hundred milliseconds and making people wonder if they'd broken something.

3. Templ's syntax has a real learning curve

Templ's type safety is worth it, but mixing Go control flow directly into what looks like HTML markup is visually strange at first, like finding out your quiet friend also plays drums in a metal band. You can't paste in a chunk of HTML from a design mockup and expect it to just work; attributes, conditionals, and especially passing components as children all require understanding how Templ threads its rendering context through. It took longer to feel fluent in than I expected.

4. The ecosystem is thinner

This is the tradeoff nobody's HTMX blog post mentions enough: React's ecosystem is enormous. Need a date picker, a rich text editor, a drag-and-drop kanban board? There's a battle-tested npm package for it, usually several, usually with strong opinions about each other. In the HTMX/Alpine world, you're frequently building that component yourself, or bending a vanilla-JS library that was never designed with this pattern in mind.

And when something breaks, the safety net is thinner too. React's Stack Overflow tag has millions of questions, and there's a good chance yours has already been answered by someone in your exact situation, possibly using your exact variable names. HTMX and Templ's communities are active and growing, but noticeably smaller.

For a personal site, it's a fun Saturday. For a larger product, it's a real cost.

5. Some things are legitimately awkward in hypermedia-land

Complex client-side state that has nothing to do with the server - a multi-step wizard with branching logic, real-time collaborative editing, heavy optimistic UI, doesn't map cleanly onto "server returns HTML." HTMX has patterns for a lot of this (out-of-band swaps, hx-vals, WebSocket extensions), but at some point you can feel yourself pushing against the grain of the tool instead of moving with it.

Is It Worth It?

For my use case - yes, easily. Building my site this way was one of the more refreshing development experiences I've had in quite some time, mostly because of what it removed rather than what it added: no build step, no client/server state duplication, no JavaScript framework fatigue quietly eating my weekend.

I wouldn't reach for this stack to build the next Figma or Google Docs - real-time, offline-first, deeply stateful client apps are exactly the case React and friends were built for, and hypermedia isn't a natural fit there. But for the other 90% of the web - blogs, portfolios, marketing sites, admin dashboards, most CRUD apps, this stack strips out an enormous amount of complexity that we've all just quietly agreed to accept as "how web development works now."

If you already like Go and you're a little tired of JavaScript framework churn, it's worth the week it takes to unlearn the fetch-a-JSON-blob reflex. You might just remember how fun this used to feel.