当サイトにPWA対応を入れました。ツール集を「ホーム画面に追加」でアプリのように使えるようにしたかったのがきっかけです。やる前はService Workerやキャッシュ戦略など難しそうなイメージがありましたが、やってみたら拍子抜けするほどシンプルでした。
PWAに対する誤解
PWA = Service Worker必須?
PWAと聞くと、Service Worker・オフライン対応・キャッシュ戦略……と身構えてしまいます。自分もそうでした。
でも調べてみると、manifest.jsonを置くだけでも「インストール可能」になることが分かりました。Service Workerはオフライン対応やプッシュ通知など高度な機能を使いたいときに必要になるもので、PWAの入り口としては必須ではありません。
静的サイトやブログなら、manifest.jsonだけで十分なケースが多いです。
manifest.json だけでできること
最小構成の manifest.json
当サイトで実際に使っている manifest.json がこちらです。
{ "name": "KDD Tools", "short_name": "KDD Tools", "description": "気まぐれ駆動開発のツール集", "start_url": "/", "scope": "/", "display": "standalone", "background_color": "#fafaf9", "theme_color": "#1a1a2e", "icons": [ { "src": "/images/icons/icon-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/images/icons/icon-512x512.png", "sizes": "512x512", "type": "image/png" }, { "src": "/images/icons/icon-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ]}これだけで、ブラウザにインストールボタンが表示され、ホーム画面に追加できるようになります。
各プロパティの役割
| プロパティ | 役割 |
|---|---|
name | アプリの正式名称。インストール時のダイアログに表示される |
short_name | ホーム画面のアイコン下に表示される短い名前 |
description | アプリの説明文 |
start_url | アプリ起動時に開くURL |
scope | PWAとして動作する範囲。この範囲外へのナビゲーションはブラウザで開く |
display | 表示モード。standaloneでブラウザのUIを非表示にしてアプリらしく見せる |
background_color | アプリ起動時のスプラッシュスクリーンの背景色 |
theme_color | ステータスバーなどのテーマカラー |
icons | ホーム画面やスプラッシュスクリーンで使うアイコン |
display: standalone を選んだのは、ツール利用時にアドレスバーなどのブラウザUIが邪魔にならないようにするためです。ブログを読むだけなら minimal-ui でもいいのですが、サイト全体を1つのPWAにしているので、ツール利用時のUXを優先しました。
アイコンの purpose: "maskable" は、Android端末でアイコンが丸や四角に切り抜かれる場合に、いい感じに収まるようにするための指定です。通常アイコンと maskable アイコンの両方を登録しておくと、端末に応じて適切なほうが使われます。
それでもService Workerを入れた理由
更新が反映されない問題
最初はmanifest.jsonだけで進めようとしました。実際にそれだけでもインストール可能になり、問題なさそうに見えました。
しかし、インストール後にアイコンやアプリ名を変更しても、インストール済みのアプリに変更が反映されないことに気づきました。ブラウザはService Workerの更新チェックをトリガーにmanifest.jsonを再読み込みする仕組みなので、Service Workerがないと更新を検知できないのです。
キャッシュなし・4行の最小構成
そこで導入したのが、こちらの最小Service Workerです。
self.addEventListener('install', () => self.skipWaiting());self.addEventListener('activate', (event) => { event.waitUntil(self.clients.claim());});たった4行です。やっていることはシンプルで、2つのイベントに応答しているだけです。
skipWaiting()— 新しいService Workerがインストールされたら、待機せず即座にアクティブにするclients.claim()— アクティブになったら、すでに開いているページも新しいService Workerの制御下に置く
キャッシュを一切使っていないので、オフライン対応はしません。その代わり、副作用がゼロです。このService Workerが行うのは「自分自身の更新を素早く反映すること」だけで、ブラウザはService Workerの更新チェック時にmanifest.jsonの変更も検知してくれます。
設計で迷ったポイント
スコープ設計 — サイト全体を1つのPWAに
manifest.jsonの scope プロパティで、PWAとして動作する範囲を制御できます。当サイトでは scope: "/" としてサイト全体を1つのPWAにしました。
ツールごとに個別のPWAを作る選択肢もありましたが、現時点でツールは4個。個別化するとそれぞれにmanifest.jsonとService Workerが必要になり、管理コストが増えます。サイト全体を1つのPWAにしておけば、ツール間やブログとの行き来もスムーズです。
ツールが10個を超えてきたら、利用頻度の高いものを個別PWA化することを再検討する方針です。
AdSenseとの共存を見据えたキャッシュなし設計
将来的にAdSenseの導入を検討しているため、キャッシュ戦略には慎重になりました。Service Workerのキャッシュがある場合、広告スクリプトの読み込みに干渉するリスクがあります。キャッシュされた古い広告スクリプトが配信されたり、オフライン時に広告が空白になったりする問題が起きえます。
今回のキャッシュなし最小構成なら、広告との干渉はゼロです。将来キャッシュを追加する場合は、広告関連のドメインを Network Only 戦略にするなどの対応が必要になりますが、まずは何も壊さない構成からスタートしたほうが安全です。
Astroでの組み込み方
Astroで使うために、BaseLayout.astroの <head> 内に3つの要素を追加しました。
<link rel="manifest" href="/manifest.json" /><meta name="theme-color" content="#1a1a2e" /><script is:inline> if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js'); }</script>1つ目はmanifest.jsonの読み込み、2つ目はステータスバーのテーマカラー設定、3つ目はService Workerの登録です。
<script> に is:inline を付けているのは、Astroのビルド処理でスクリプトがバンドル・最適化されるのを防ぐためです。Service Workerの登録はページ読み込み時にそのまま実行してほしいので、インラインスクリプトとして残す必要があります。
まとめ
- manifest.json だけでもインストール可能になる。PWAの第一歩はここから
- **最小Service Worker(4行)**を追加すれば、manifest.jsonの更新も反映される
- キャッシュ戦略は後から追加可能。最初はキャッシュなしで始めるのが安全
- スコープ設計はサイトの規模に合わせて判断。小規模ならサイト全体で1つが管理しやすい
- 広告との共存を見据えるなら、キャッシュなし設計が干渉リスクをゼロにできる
PWAというと大掛かりなイメージがありますが、静的サイトへの導入なら manifest.json と最小限のService Workerで十分です。必要になったタイミングでキャッシュやオフライン対応を段階的に追加していけばいい——そんな気軽さがPWAの良いところだと感じました。
PWAって身構えちゃうけど、manifest.jsonと4行のService Workerだけならハードル低いよね。このサイトもPWA対応しているから、スマホで「ホーム画面に追加」して試してみてね!ツールをアプリみたいに使えるよ。