Our client, a leading company in the streaming industry, acquired a highly successful indie website with over 50 million unique visitors per month. The site had been built by a single developer who prioritized functionality over maintainability.
While the features were mostly bug-free, the design was outdated, and the code relied on aging technologies. The priority had been shipping fixes and features quickly, rather than long-term maintainability.
The main goal of our project was to deliver a modern, responsive, and maintainable site while ensuring business continuity, reducing risk, and making developer onboarding faster and easier.
The Challenge: Balancing Modernization with Stability
Working with the UX designer, our first quick win was a design refresh. This allowed us to become familiar with the existing codebase and features, while also buying time to identify deeper technical issues and plan a technology path forward.
The site ran on a Python Flask backend with Jinja templates and jQuery handling client-side interactions. Although migrating to an SPA framework was tempting, we quickly ruled it out: it would have been excessive for the project’s needs and introduced unnecessary complexity. Instead, we chose to stay with a server-rendered, static-style approach.
The Technology Shift: Introducing the AHA Stack
The solution was inspired by the AHA Stack (Astro, HTMX, Alpine.js). In our case, Astro was replaced by the existing Jinja templates.
The philosophy was simple: build with the web’s core technologies (HTML, HTTP, CSS, JS), avoid fragile dependencies, and prevent the app from breaking with every API or framework shift (as demonstrated plasticly in the classic https://motherfuckingwebsite.com).
Why HTMX? Lightweight Server-Driven Interactions
To keep the site fast and accessible – even on low-end devices – we minimized JavaScript. Since the site was already server-rendered, HTMX was a natural fit.
HTMX allows sending HTML snippets directly over the wire, following HATEOAS principles, and swapping DOM elements without needing JSON parsing or heavy client-side logic. It strikes a balance between old-school full-page reloads and modern SPA complexity.
Why Alpine.js? Simple State Management Without an SPA
HTMX alone was not enough: we needed lightweight state management and interaction handling. jQuery was an option, but it felt outdated and unattractive to new developers.
We adopted Alpine.js, a dependency-free, intuitive library that complements HTMX perfectly. After a proof-of-concept rewrite of a key page, Alpine proved to be efficient, easy to learn, and expressive enough for our use cases.
By progressively rewriting less critical pages first, then moving to core routes and new business features, we introduced Alpine incrementally. This minimized disruption, made feature requests easy to integrate, and preserved stability.
Implementation Hurdles and Solutions
Migrating to the AHA stack required adjustments:
- Combining modern modular JavaScript with legacy non-modular code.
- Adding TypeScript for type safety while managing untyped legacy code.
- Updating backend handlers to return conditional HTML snippets for both old and new functionality.
Despite these challenges, the benefits were clear. Alpine components had well-defined boundaries, making unit testing with Jest straightforward.
Lessons Learned from Using Alpine.js
While Alpine was a joy to use, we discovered important nuances:
- Using a class-based approach kept types manageable, but required careful handling of constructor vs. init lifecycle methods.
- Passing backend/template values into Alpine components needed special attention, e.g.:
<div x-data=”horizontalScroller(‘value of my_var’)”>
Alpine.data(‘horizontalScroller’, (my_var: string | null) => new HorizontalScroller(my_var));
- Always let init manage component setup, and rely on $nextTick to ensure logic executes after the component is mounted.
The Trade-Offs of SPAs vs Server-Rendered Apps
While it is often tempting to work with SPAs and their full-stack counterparts (e.g., React + Next.js, Vue + Nuxt, etc.), since they offer a familiar developer experience – sometimes even described as intuitive – they come at a cost. Teams must carefully evaluate whether the trade-offs are worth it. Without going into too much detail, I’ll highlight the two main drawbacks: speed and overhead.
No matter how “lightning fast” a framework advertises itself to be, it will never be as fast as plain HTML served directly to the browser – unless that’s exactly what it does. While Next.js and similar frameworks do deliver static HTML initially, the trade-off comes right after: a large JavaScript bundle still follows the HTML down the wire. This doesn’t usually impact FCP (First Contentful Paint), but the additional download combined with hydration delays TTI (Time to Interactive).
Using abstractions also means moving further away from the fundamentals. That becomes problematic when you need functionality your framework doesn’t expose through an API – or worse, when it hides things under the hood. In those cases, debugging and implementing custom solutions can quickly become difficult.
HTMX, Alpine.js and the AHA Stack as Alternatives
Breaking away from SPAs doesn’t have to come at the expense of developer experience. The HTMX team intentionally keeps the library thin, with an intuitive API and lightweight footprint. Used properly, HTMX often eliminates the need to write any JavaScript for a simple CRUD application.
Of course, if you’re aiming for a more complex UX, you’ll need to complement HTMX with a JavaScript layer. We chose Alpine.js for reasons mentioned earlier, but it could be any lightweight tool you enjoy working with – as long as it helps you get the job done.


