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
Page loads, user starts reading.
Browser fires `beforeinstallprompt`. We `preventDefault()` to hide the native banner.
A 30-second timer begins. If the user leaves, we do nothing.
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.
// 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:
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.
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.






⚠️ コメントのルール
※違反コメントはAIおよび管理者により予告なく削除されます
まだコメントがありません。最初のコメントを投稿しましょう!