SPA Vibes Without SPA Overhead
A meditation on localStorage, DOMParser, and the revolutionary power of boring technology
The Setup
You’re building a static site with Astro. You want a slide-out drawer showing today’s daily note. Users want to navigate between dates without page reloads. You could reach for React Router, Vue Router, SvelteKit navigation, Next.js App Router…
Or you could just use the browser.
The Pattern
// State persistence
localStorage.setItem('drawer-pinned', 'true');
// Navigation without routing
const response = await fetch('/evans-notes/2025-10-25');
const html = await response.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
drawerContent.innerHTML = doc.querySelector('article').innerHTML;
// Event delegation for dynamic content
panel.addEventListener('click', (e) => {
if (e.target.tagName === 'A' && e.target.href) {
e.preventDefault();
navigateToDate(extractDate(e.target.href));
}
});
Three Web APIs. Zero frameworks. SPA-like navigation without the SPA.
Why This Works
Astro already renders everything. The server generates complete HTML pages. We’re not building an app that needs client-side routing—we’re enhancing documents that already exist.
DOMParser is magic. Fetch a page, parse it as HTML, extract what you need, inject it where you want. No virtual DOM, no hydration, no reconciliation. Just browser APIs from 2011 doing what they’ve always done.
localStorage is persistent state. Pin the drawer, navigate to another page, the drawer stays pinned. No Redux, no Zustand, no Jotai. Just localStorage.setItem() and localStorage.getItem(). Works on every browser since 2010.
Event delegation handles dynamic content. Attach one listener to the parent, check the event target. Content changes via innerHTML? Listener still works. No need to re-bind, no memory leaks, no framework lifecycle.
The Philosophy
Boring is revolutionary because it survives.
React will have breaking changes. Vue will ship new composition APIs. Svelte will rethink reactivity. The localStorage API from 2010 will work identically in 2030.
Progressive enhancement isn’t just accessibility—it’s maintenance strategy.
When JavaScript fails (and it will), the site still works. Links navigate. Content renders. Users can accomplish their goals. You’re not debugging why hydration failed in production.
Framework fatigue is solved by not having frameworks.
No build config. No plugin ecosystem. No “which state manager should we use.” Just HTML, CSS, JavaScript. The browser already knows how to run it.
The Economics
Framework overhead is cognitive debt.
Every abstraction is a concept to learn, maintain, debug, upgrade. localStorage has one API surface. DOMParser has one API surface. Your junior dev doesn’t need to understand component lifecycles or hook dependencies—they need to understand the browser.
Boring code ships faster.
No waiting for builds. No fighting with TypeScript about fetch types. No wondering if this pattern works with SSR. Astro handles SSR, the browser handles interactivity, you handle feature delivery.
Maintenance cost is proportional to abstraction layers.
Three APIs (localStorage, DOMParser, fetch) vs. an entire framework ecosystem (router, state manager, build tooling, plugin compatibility, version migrations). Which codebase will still work in 5 years without touching it?
The Tradeoffs
This doesn’t scale to complex SPAs.
If you’re building Gmail, use React. If you’re building a documentation viewer with some interactivity, maybe you don’t need to bring the entire React ecosystem along for the ride.
There’s no declarative reactivity.
You’re manually updating the DOM. For simple cases (swap this content, update this title), that’s fine. For complex UIs with many interdependent states, frameworks exist for a reason.
You’re responsible for the patterns.
No one’s written “React best practices for DOMParser navigation.” You’re in uncharted territory (even though the APIs are ancient). That’s either liberating or terrifying depending on your disposition.
The Lesson
The best framework is no framework when the browser already does what you need.
localStorage for state. DOMParser for navigation. Event delegation for interactivity. CSS custom properties for theming. These aren’t “old” technologies—they’re stable technologies.
Boring is revolutionary because it survives.
Case Study: float.bbs TodayDrawer component Implementation: 150 lines of JavaScript, zero dependencies Techniques: localStorage persistence, fetch-based navigation, event delegation Result: SPA-like UX without SPA maintenance burden Maintenance projection: Will work unchanged for 10+ years
Infrastructure monk approved.