💡

Key Points

Key Takeaways

The “Install App” banner is the newsletter popup of the modern web. It’s useful, but often deployed with zero regard for the user’s current context.

In this update, I spent time refining two core aspects of the blog: Runtime Stability (fixing a nasty Three.js crash) and User Experience (redesigning the PWA flow). Here’s how I solved them.

1. The PWA “Engagement First” Strategy

Browser vendors provide a default “Install App” mechanism, but it has flaws. It often triggers immediately on page load, interrupting the user before they”ve even read a single sentence.

The Problem with Default Prompts

If a user lands on your blog from a search result, they want to read, not install. Bombarding them with popups increases bounce rates and cognitive load.

The Solution: Timed Interception

I implemented a Passive Install UI that follows a simple rule: Only ask engaged users.

The PWA Lifecycle Visually

sequenceDiagram
 participant User
 participant Browser
 participant PWA as PWA Component

 User->>Browser: Opens Blog Post
 Browser->>PWA: Fire "beforeinstallprompt"
 PWA->>Browser: Prevent Default (Stop Banner)
 PWA->>PWA: Start 30s Timer ⏳

 rect rgb(30, 30, "30)
 note right of User: User reads content...
 end

 alt User leaves < 30s
 PWA */}>User: No Prompt (Respects attention)
 else User stays > 30s
 PWA->>User: Show "Install App" Toast 📱
 User->>PWA: Clicks Install
 PWA->>Browser: prompt()
 end
1

Page loads, user starts reading.

2

Browser fires `beforeinstallprompt`. We `preventDefault()` to hide the native banner.

3

A 30-second timer begins. If the user leaves, we do nothing.

4

If still present, a subtle toast appears at the bottom right.

The Implementation

We use a custom Astro component PWAInstallToast.astro that listens for the beforeinstallprompt event.

src/components/pwa/PWAInstallToast.astro
// Stash the event so it can be triggered later.
window.addEventListener("beforeinstallprompt", (e) => {
 e.preventDefault();
 deferredPrompt = e;

 // Check if user has dismissed it recently
 if (!sessionStorage.getItem("pwa-install-dismissed")) {
 // Delay prompt to target engaged users (30 seconds)
 setTimeout(showToast, 30000);
 }
});

This small change shifts the dynamic from “Please install me!” to “Oh, you like this? Here”s an app version.”


2. Resolving the “Module Not Defined” Crash

During the update, I encountered a critical runtime error that crashed the build:

Build Error
ReferenceError: module is not defined
 at .../node_modules/three/build/three.module.js

The Root Cause: Manual Aliases

In an attempt to “optimize” dependencies, I had manually added aliases to astro.config.mjs:

// BAD CONFIGURATION
resolve: {
 alias: {
 "react": "path/to/react", // <--- This caused the crash
 "three": "path/to/three" }
}

This forced Vite to resolve react and three to specific paths, which conflicted with how @react-three/fiber (which depends on specific versions) expects to find them.

graph TD
 subgraph "Before (Crash)"
 A[Astro/Vite]  */}|Alias| B[React (v18.x at /path/A)]
 A  */}|Node Resolve| C[React (v18.x at /path/B)]
 D[@react-three/fiber]  */} C
 style C fill:#f96,stroke:#333,stroke-width:2px,color:#fff
 style B fill:#f96,stroke:#333,stroke-width:2px,color:#fff
 end

 subgraph "After (Fix)"
 X[Astro/Vite]  */}|Dedupe| Y[React (Single Instance)]
 Z[@react-three/fiber]  */} Y
 style Y fill:#9f9,stroke:#333,stroke-width:2px,color:#000
 end

It resulted in multiple instances of React loading, or module references breaking in ESM environments.

graph TD
 subgraph "Before (Crash)"
 A[Astro/Vite]  */}|Alias| B[React (v18.x at /path/A)]
 A  */}|Node Resolve| C[React (v18.x at /path/B)]
 D[@react-three/fiber]  */} C
 style C fill:#f96,stroke:#333,stroke-width:2px,color:#000
 style B fill:#f96,stroke:#333,stroke-width:2px,color:#000
 end

 subgraph "After (Fix)"
 X[Astro/Vite]  */}|Dedupe| Y[React (Single Instance)]
 Z[@react-three/fiber]  */} Y
 style Y fill:#9f9,stroke:#333,stroke-width:2px,color:#000
 end

The Fix: Trust dedupe

The solution was simpler than the “optimization”. I removed the manual aliases and relied on Vite’s dedupe setting, which ensures only one copy of these libraries is bundled.

astro.config.mjs (Fixed)
vite: {
 resolve: {
 // No manual aliases for react/three!
 dedupe: ["react", "react-dom", "three"]
 },
 optimizeDeps: {
 exclude: ["amazon-paapi", "sharp"],
 include: ["@react-three/fiber", "@react-three/drei"]
 }
}

Once applied, pnpm build passed immediately with Exit Code 0 .


3. Silencing Dev Warnings

Another annoyance was the Pagefind search engine throwing 404s during development. Pagefind is a static search tool that runs after the build, so the pagefind.js file doesn’t exist in dev mode.

I updated SearchDialog.astro to explicitly skip loading in development:

// SearchDialog.astro
if (!pagefind) {
 // In development, skip loading Pagefind to avoid 404s
 if (import.meta.env.DEV) {
 return;
 }
 // Try import...
}

Conclusion

Stability and UX are often about what you remove (manual aliases, immediate prompts) rather than what you add. With these fixes, HonoGear is now more stable and respects the user’s attention span.

Next, I”ll be focusing on content expansion and the new “Smart Search” features.