Add offline support with service worker and offline page - #27
Add offline support with service worker and offline page#27linus-jansson wants to merge 1 commit into
Conversation
Registers a service worker that caches /offline.html on install and serves it when navigation requests fail due to no internet connection. https://claude.ai/code/session_019bgQ3Du3hfaf2v4WDUmnCv
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR adds offline support to the application. A React component registers a service worker that intercepts navigation requests, attempts network fetches, and serves a pre-cached offline page on network failure. The offline page displays a user-friendly message with a reload button. ChangesOffline Support Feature
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@components/ServiceWorkerRegistration.tsx`:
- Around line 5-13: ServiceWorkerRegistration currently calls
navigator.serviceWorker.register("/sw.js") without handling promise rejections
or update events; wrap the registration in async handling (either make the
effect callback async or handle the returned promise) to add a .catch(...) that
logs registration errors (include the error object) and add logic on the
registration object (e.g., registration.onupdatefound or
registration.installing.onstatechange) to detect and log updates; update
references: ServiceWorkerRegistration and the
navigator.serviceWorker.register(...) call so failures are caught and update
events are observed for better diagnostics.
In `@public/offline.html`:
- Line 86: The decorative emoji in the span with class "icon" should be hidden
from assistive tech; update the span.icon element to include aria-hidden="true"
(and optionally focusable="false") so screen readers ignore the emoji while
keeping visual presentation intact.
- Line 11: The CSS rule uses min-height: 100svh which lacks support in older
browsers; add a fallback by declaring min-height: 100vh before the min-height:
100svh declaration in the same selector (i.e., keep the existing min-height:
100svh but precede it with min-height: 100vh) so browsers that don't understand
100svh will use 100vh while newer browsers will override with 100svh; update the
CSS in public/offline.html where min-height: 100svh is set.
In `@public/sw.js`:
- Around line 22-30: The fetch handler currently responds with
cache.match(OFFLINE_URL) which can be undefined; update the service worker fetch
listener (self.addEventListener("fetch", ...)) so that event.respondWith uses a
Promise that falls back to a constructed Response when cache.match(OFFLINE_URL)
yields no result: after opening CACHE_NAME and attempting
cache.match(OFFLINE_URL), detect undefined and return a simple offline Response
(e.g., a short HTML/text payload with 503 status and appropriate headers) so
event.respondWith never receives undefined; ensure this logic is applied where
fetch(event.request).catch(...) is used and references CACHE_NAME and
OFFLINE_URL.
- Around line 4-9: The install handler calls self.skipWaiting() synchronously,
which can activate the SW before the offline page is cached; update the
"install" event listener so skipWaiting() is chained to the cache population
promise (the caches.open(CACHE_NAME).then(...)) — i.e., call skipWaiting() only
after cache.add(OFFLINE_URL) resolves (use the existing event.waitUntil promise
chain in the install listener) to guarantee OFFLINE_URL is stored before
activation.
- Around line 11-20: In the activate listener
(self.addEventListener("activate")), chain self.clients.claim() into the promise
passed to event.waitUntil so it runs after the cache cleanup completes: extend
the Promise.all(...) chain returned from caches.keys().then(...) to call
self.clients.claim() (or return a Promise that resolves after caches.delete
calls and clients.claim()), ensuring the code paths around CACHE_NAME cleanup
(caches.keys, Promise.all) complete before invoking self.clients.claim().
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 04d11f17-03fd-4f1a-a47d-8303aeaa12da
📒 Files selected for processing (4)
app/layout.tsxcomponents/ServiceWorkerRegistration.tsxpublic/offline.htmlpublic/sw.js
| export function ServiceWorkerRegistration() { | ||
| useEffect(() => { | ||
| if ("serviceWorker" in navigator) { | ||
| navigator.serviceWorker.register("/sw.js"); | ||
| } | ||
| }, []); | ||
|
|
||
| return null; | ||
| } |
There was a problem hiding this comment.
Add error handling for service worker registration.
navigator.serviceWorker.register() returns a promise that can reject if the script is invalid, unreachable, or blocked by permissions. Without .catch() or try/catch, registration failures result in unhandled rejections logged to the console with no actionable feedback for debugging or monitoring.
🛡️ Proposed fix with error handling and update detection
export function ServiceWorkerRegistration() {
useEffect(() => {
if ("serviceWorker" in navigator) {
- navigator.serviceWorker.register("/sw.js");
+ navigator.serviceWorker
+ .register("/sw.js")
+ .then((registration) => {
+ console.log("Service Worker registered:", registration.scope);
+ })
+ .catch((error) => {
+ console.error("Service Worker registration failed:", error);
+ });
}
}, []);
return null;
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/ServiceWorkerRegistration.tsx` around lines 5 - 13,
ServiceWorkerRegistration currently calls
navigator.serviceWorker.register("/sw.js") without handling promise rejections
or update events; wrap the registration in async handling (either make the
effect callback async or handle the returned promise) to add a .catch(...) that
logs registration errors (include the error object) and add logic on the
registration object (e.g., registration.onupdatefound or
registration.installing.onstatechange) to detect and log updates; update
references: ServiceWorkerRegistration and the
navigator.serviceWorker.register(...) call so failures are caught and update
events are observed for better diagnostics.
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | ||
|
|
||
| body { | ||
| min-height: 100svh; |
There was a problem hiding this comment.
Add 100vh fallback for older browsers.
100svh is not supported in Safari < 15.4 or Chrome < 108. Older browsers will ignore the rule, leaving min-height unset and the layout potentially not filling the viewport.
♻️ Proposed fix
body {
+ min-height: 100vh;
min-height: 100svh;
display: flex;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@public/offline.html` at line 11, The CSS rule uses min-height: 100svh which
lacks support in older browsers; add a fallback by declaring min-height: 100vh
before the min-height: 100svh declaration in the same selector (i.e., keep the
existing min-height: 100svh but precede it with min-height: 100vh) so browsers
that don't understand 100svh will use 100vh while newer browsers will override
with 100svh; update the CSS in public/offline.html where min-height: 100svh is
set.
| <body> | ||
| <div class="blob"></div> | ||
| <div class="content"> | ||
| <span class="icon">📡</span> |
There was a problem hiding this comment.
Mark decorative emoji as aria-hidden.
Screen readers may announce the emoji's Unicode description inconsistently. Since the icon is purely decorative (the heading and text already convey the offline state), hide it from assistive technologies.
♿ Proposed fix
- <span class="icon">📡</span>
+ <span class="icon" aria-hidden="true">📡</span>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <span class="icon">📡</span> | |
| <span class="icon" aria-hidden="true">📡</span> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@public/offline.html` at line 86, The decorative emoji in the span with class
"icon" should be hidden from assistive tech; update the span.icon element to
include aria-hidden="true" (and optionally focusable="false") so screen readers
ignore the emoji while keeping visual presentation intact.
| self.addEventListener("install", (event) => { | ||
| event.waitUntil( | ||
| caches.open(CACHE_NAME).then((cache) => cache.add(OFFLINE_URL)) | ||
| ); | ||
| self.skipWaiting(); | ||
| }); |
There was a problem hiding this comment.
Critical race condition: skipWaiting() must wait for cache to populate.
skipWaiting() is called synchronously outside the install promise chain, so the service worker can activate before /offline.html is cached. This breaks the offline fallback: if a navigation request fails before caching completes, the fetch handler will attempt to serve a non-existent cached response.
🔒 Proposed fix
Chain skipWaiting() to ensure it runs only after the offline page is cached:
self.addEventListener("install", (event) => {
event.waitUntil(
- caches.open(CACHE_NAME).then((cache) => cache.add(OFFLINE_URL))
+ caches.open(CACHE_NAME)
+ .then((cache) => cache.add(OFFLINE_URL))
+ .then(() => self.skipWaiting())
);
- self.skipWaiting();
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| self.addEventListener("install", (event) => { | |
| event.waitUntil( | |
| caches.open(CACHE_NAME).then((cache) => cache.add(OFFLINE_URL)) | |
| ); | |
| self.skipWaiting(); | |
| }); | |
| self.addEventListener("install", (event) => { | |
| event.waitUntil( | |
| caches.open(CACHE_NAME) | |
| .then((cache) => cache.add(OFFLINE_URL)) | |
| .then(() => self.skipWaiting()) | |
| ); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@public/sw.js` around lines 4 - 9, The install handler calls
self.skipWaiting() synchronously, which can activate the SW before the offline
page is cached; update the "install" event listener so skipWaiting() is chained
to the cache population promise (the caches.open(CACHE_NAME).then(...)) — i.e.,
call skipWaiting() only after cache.add(OFFLINE_URL) resolves (use the existing
event.waitUntil promise chain in the install listener) to guarantee OFFLINE_URL
is stored before activation.
| self.addEventListener("activate", (event) => { | ||
| event.waitUntil( | ||
| caches | ||
| .keys() | ||
| .then((keys) => | ||
| Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))) | ||
| ) | ||
| ); | ||
| self.clients.claim(); | ||
| }); |
There was a problem hiding this comment.
Chain clients.claim() inside the activation promise.
clients.claim() is called synchronously outside the waitUntil promise, so it may execute before stale caches are deleted. While less critical than the install race, this violates the service worker lifecycle contract and could cause the worker to claim clients while still serving outdated cache entries.
♻️ Proposed fix
self.addEventListener("activate", (event) => {
event.waitUntil(
caches
.keys()
.then((keys) =>
Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
)
+ .then(() => self.clients.claim())
);
- self.clients.claim();
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| self.addEventListener("activate", (event) => { | |
| event.waitUntil( | |
| caches | |
| .keys() | |
| .then((keys) => | |
| Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))) | |
| ) | |
| ); | |
| self.clients.claim(); | |
| }); | |
| self.addEventListener("activate", (event) => { | |
| event.waitUntil( | |
| caches | |
| .keys() | |
| .then((keys) => | |
| Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))) | |
| ) | |
| .then(() => self.clients.claim()) | |
| ); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@public/sw.js` around lines 11 - 20, In the activate listener
(self.addEventListener("activate")), chain self.clients.claim() into the promise
passed to event.waitUntil so it runs after the cache cleanup completes: extend
the Promise.all(...) chain returned from caches.keys().then(...) to call
self.clients.claim() (or return a Promise that resolves after caches.delete
calls and clients.claim()), ensuring the code paths around CACHE_NAME cleanup
(caches.keys, Promise.all) complete before invoking self.clients.claim().
| self.addEventListener("fetch", (event) => { | ||
| if (event.request.mode !== "navigate") return; | ||
|
|
||
| event.respondWith( | ||
| fetch(event.request).catch(() => | ||
| caches.open(CACHE_NAME).then((cache) => cache.match(OFFLINE_URL)) | ||
| ) | ||
| ); | ||
| }); |
There was a problem hiding this comment.
Add defensive fallback when offline page is unavailable.
If cache.match(OFFLINE_URL) returns undefined (e.g., due to the activation race or cache eviction), respondWith receives undefined and the browser displays a generic network error instead of a graceful offline message. While fixing the activation race (lines 4–9) prevents the root cause, adding a fallback response hardens the handler against cache failures.
🛡️ Proposed fix with fallback response
self.addEventListener("fetch", (event) => {
if (event.request.mode !== "navigate") return;
event.respondWith(
- fetch(event.request).catch(() =>
- caches.open(CACHE_NAME).then((cache) => cache.match(OFFLINE_URL))
+ fetch(event.request).catch(() =>
+ caches.open(CACHE_NAME).then((cache) =>
+ cache.match(OFFLINE_URL).then((response) =>
+ response || new Response("Offline", {
+ status: 503,
+ statusText: "Service Unavailable",
+ headers: new Headers({ "Content-Type": "text/plain" })
+ })
+ )
+ )
)
);
});🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@public/sw.js` around lines 22 - 30, The fetch handler currently responds with
cache.match(OFFLINE_URL) which can be undefined; update the service worker fetch
listener (self.addEventListener("fetch", ...)) so that event.respondWith uses a
Promise that falls back to a constructed Response when cache.match(OFFLINE_URL)
yields no result: after opening CACHE_NAME and attempting
cache.match(OFFLINE_URL), detect undefined and return a simple offline Response
(e.g., a short HTML/text payload with 503 status and appropriate headers) so
event.respondWith never receives undefined; ensure this logic is applied where
fetch(event.request).catch(...) is used and references CACHE_NAME and
OFFLINE_URL.
Summary
This PR adds offline support to the application by implementing a service worker that caches the offline page and serves it when network requests fail.
Key Changes
Service Worker (
public/sw.js): Implements offline functionality with:Offline Page (
public/offline.html): A user-friendly offline fallback page featuring:Service Worker Registration (
components/ServiceWorkerRegistration.tsx): A client-side React component that registers the service worker on app initializationLayout Integration (
app/layout.tsx): Integrated the ServiceWorkerRegistration component into the root layout to ensure the service worker is registered on all pagesImplementation Details
event.request.mode === "navigate") to avoid caching API calls or assetslimpan-v1) for easy cache invalidation in future updateshttps://claude.ai/code/session_019bgQ3Du3hfaf2v4WDUmnCv
Summary by CodeRabbit