Offline support almost without Javascript

Posted on 2024-02-27 in Programmation

Recently I wandered wether I could build a website with offline support without building a full SPA. The answer is yes it’s doable: you only need Javascript for the service worker. Just for the fun, I also tried it with navigation done with HTMX and without much surprise it also works.

Avis

To do these tests, you must use a small webserver to serve your content. I used python3 -m http.server.

If you use Firefox, you must shutdown the server to be offline. If you go offline in the dev tools (for some reason I don’t know), your navigation won’t be handled by the service worker and you will appear truly offline.

Let’s start by building a simple index.html page:

 1 <!DOCTYPE html>
 2 <html>
 3     <head>
 4         <base href="/">
 5         <script>
 6             if ('serviceWorker' in navigator) {
 7               window.addEventListener('load', () => {
 8                 navigator.serviceWorker.register('/service-worker.js')
 9                   .then(registration => {
10                     console.log('Service Worker registered with scope:', registration.scope);
11                   })
12                   .catch(error => {
13                     console.error('Service Worker registration failed:', error);
14                   });
15               });
16 
17               navigator.serviceWorker.ready.then((registration) => {
18                 fetch('/page2.html');
19               })
20             }
21           </script>
22           <!-- <script src="/htmx.min.js"></script> -->
23     </head>
24 <body>
25 
26 <h1>Home page</h1>
27 
28 <p>My first paragraph.</p>
29 
30 <ul>
31     <li><a href="/">Index</a></li>
32     <li><a href="/page1.html">Page 1</a></li>
33     <li><a href="/page2.html">Page 2</a></li>
34 </ul>
35 
36 </body>
37 </html>

The HTML part should be straightforward. In the script tag, I register my service worker (lines 7 to 15) and prefetch a page once it is ready so it’s in the cache (line 17 to 19). The rest is just code to navigate between the different pages and a title to identify the page.

I build two other page by copy/pasting this into page1.html and page2.html and changing the h1. I then created my service-worker.js:

 1 const CACHE_NAME = 'my-cache-v1';
 2 const urlsToCache = [
 3   '/',
 4   '/page1.html',
 5 ];
 6 
 7 self.addEventListener('install', event => {
 8   event.waitUntil(
 9     caches.open(CACHE_NAME)
10       .then(cache => {
11         return cache.addAll(urlsToCache);
12       })
13   );
14 });
15 
16 self.addEventListener('fetch', event => {
17   console.log('Captured fetching', event.request)
18   event.respondWith(
19     caches.match(event.request)
20       .then(response => {
21         if (response) {
22           console.log('Serving from cache')
23           return response;
24         }
25         console.log('Fetching from server')
26         return fetch(event.request)
27           .then(fetchResponse => {
28             return caches.open(CACHE_NAME)
29               .then(cache => {
30                 cache.put(event.request, fetchResponse.clone()); // Add the fetched response to the cache
31                 return fetchResponse;
32               });
33           });
34       })
35   );
36 });

In it, I add some content to the cache by default (lines 7 to 14).

I then register a listener to the fetch event so I can choose how to respond to HTTP requests (lines 16 to 36). I choose to serve the response from the cache if I have it. If not, I fetch it then put it in the cache for later usage.

It’s very basic, but it’s enough to make it work. You can also test it by navigating here.

Note

page2.html (fetched from the main page) won’t be put into the cache until you refresh the page. From what I understood, it’s for consistency: never serve from the cache until you are sure the service worker is active and working.

You can now test the app and see how it behaves.

To test with HTMX, I replaced the ul (lines 30 to 34) with either:

<!-- Test with HTMX boost to use standard links -->
<ul hx-boost="true">
    <li><a href="/">Index</a></li>
    <li><a href="/page1.html">Page 1</a></li>
    <li><a href="/page2.html">Page 2</a></li>
</ul>

or:

<!-- Test with HTMX links to use standard links -->
<ul>
    <li><a hx-get="/" hx-push-url="true">Index</a></li>
    <li><a hx-get="/page1.html" hx-push-url="true">Page 1</a></li>
    <li><a hx-get="/page2.html" hx-push-url="true">Page 2</a></li>
</ul>

That’s it. The prefetching is very basic, but I think you get the idea: generate a list of URL and then call them.

If you want to learn more about a related topic, I wrote an article about PWA and Django a while back.