💡

この記事の要点

この記事の重要ポイント

Webサイトの「アプリをインストール」バナーは、現代のニュースレター購読ポップアップのようなものです。便利ではありますが、ユーザーのコンテキストを無視して表示されることがあまりにも多いのが現状です。

今回のアップデートでは、ブログの ランタイムの安定性 (Three.jsのクラッシュ修正)と ユーザー体験 (PWAフローの再設計)という2つの核心的な問題に取り組みました。

1. 「エンゲージメント・ファースト」なPWA戦略

ブラウザは標準で「アプリをインストール」する機能を提供していますが、これには欠点があります。多くの場合、ページ読み込み直後にトリガーされ、ユーザーが最初の一文すら読み始めていない段階で割り込んでしまいます。

デフォルトプロンプトの問題点

検索結果からブログにたどり着いたユーザーの目的は、読むことであってインストールすることではありません。ポップアップ攻撃は、直帰率と認知的負荷を高めるだけです。

解決策:時間差による介入

私は パッシブ(受動的)インストールUI を実装しました。ルールはシンプルです: 「興味を持ってくれたユーザーにのみ提案する」 こと。

ライフサイクルの可視化

以下は、プロンプト表示を決定するロジックです。

sequenceDiagram
 participant User
 participant Browser
 participant PWA as PWA Component

 User->>Browser: ブログ記事を開く
 Browser->>PWA: "beforeinstallprompt" 発火
 PWA->>Browser: Prevent Default (バナー阻止)
 PWA->>PWA: 30秒タイマー開始 ⏳

 rect rgb(30, 30, "30)
 note right of User: 記事を読んでいる...
 end

 alt 30秒以内に離脱
 PWA */}>User: 何もしない (集中を尊重)
 else 30秒以上滞在
 PWA->>User: "アプリをインストール" トースト表示 📱
 User->>PWA: インストールをクリック
 PWA->>Browser: prompt()
 end
1

ページが読み込まれ、ユーザーが読み始める。

2

ブラウザが `beforeinstallprompt` を発火。`preventDefault()` でネイティブバナーを阻止。

3

30秒のタイマーを開始。この間にユーザーが離脱すれば何もしない。

4

まだ記事を読んでいれば、右下に控えめなトーストを表示する。

実装詳細

PWAInstallToast.astro コンポーネントでイベントを制御しています。

src/components/pwa/PWAInstallToast.astro
// イベントを保持して後でトリガーできるようにする
window.addEventListener("beforeinstallprompt", (e) => {
 e.preventDefault();
 deferredPrompt = e;

 // 最近閉じた履歴がなければ...
 if (!sessionStorage.getItem("pwa-install-dismissed")) {
 // エンゲージメントの高いユーザーを狙って遅延させる (30秒)
 setTimeout(showToast, 30000);
 }
});

この小さな変更により、「インストールしてください!」という押し付けから、「お、気に入ってくれましたか?アプリ版もありますよ」という提案へと、関係性がシフトします。


2. “Module Not Defined” クラッシュの解決

アップデート中、ビルドをクラッシュさせる重大なエラーに遭遇しました。

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

原因:手動エイリアスの弊害

依存関係を「最適化」しようとして、astro.config.mjs に手動でエイリアスを追加していました:

// 悪い設定例
resolve: {
 alias: {
 "react": "path/to/react", // <--- これがクラッシュの原因
 "three": "path/to/three" }
}

これにより、Viteは reactthree を特定のパスに解決することを強制されました。しかし、@react-three/fiber(特定のバージョンに依存)が期待するパスと競合してしまったのです。

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

結果として、 複数のReactインスタンス が読み込まれたり、ESM環境で module 参照が壊れたりしていました。

解決策:dedupe を信頼する

解決策は「最適化」よりもシンプルでした。手動エイリアスを削除し、Viteの dedupe 設定に任せることで、ライブラリが1つだけバンドルされるようにしました。

astro.config.mjs (Fixed)
vite: {
 resolve: {
 // 手動エイリアスは完全に削除!
 dedupe: ["react", "react-dom", "three"]
 },
 optimizeDeps: {
 exclude: ["amazon-paapi", "sharp"],
 include: ["@react-three/fiber", "@react-three/drei"]
 }
}

これを適用すると、pnpm buildExit Code 0 で即座に成功しました。


3. 開発時の不要な警告を消す

もう一つの厄介事は、開発中に Pagefind 検索エンジンが 404 エラーを吐き続けていたことです。Pagefind はビルドに実行される静的検索ツールなので、開発モード(dev)では pagefind.js が存在しません。

そこで、SearchDialog.astro を更新し、開発モードでは明示的にロードをスキップするようにしました。

// SearchDialog.astro
if (!pagefind) {
 // 開発中はPagefindのロードをスキップして404を防ぐ
 if (import.meta.env.DEV) {
 return;
 }
 // Try import...
}

結論

安定性とUXは、何を追加するかよりも、 何を削除するか (手動エイリアス、即時のポップアップ)にかかっていることが多いです。これらの修正により、HonoGearはより安定し、ユーザーの注意力を尊重するブログになりました。

[!TIP] 教訓 : React/Vite/Astroの混合スタックで奇妙な “ReferenceError” や “Module not found” に遭遇したら、まず astro.config.mjs を確認してください。9割方、解決の競合が原因です。

次は、コンテンツの拡充と新しい「スマート検索」機能の強化に注力します。