PWA for Zola

I figured out the way to add a PWA without restructuring templates or the workflow: A capable PWA • charlesrocket

Thou it would be much easier if I could template the manifest/script files as well.

1 Like

So cool to see another theme adding a PWA! I added a PWA to the Abridge theme a while ago. You are absolutely correct that without a pre cache list you can avoid npm entirely (cache as you go). I wrote the abridge theme mostly for my technical documentation/blog and I wanted to be able to cache the entire site for offline use so abridge uses npm/node to generate the file list, I also use it for a couple other things like minifying and bundling js files.

I completely agree with keeping the workflow simple where possible though, and if Zola gets the ability to template files other than html I will probably craft a way to work within Zola only.

I enjoyed reading your article, Thanks!

1 Like

Thank you! Abridge is fantastic! Helped me a lot. And the CSS is just magic.

What about the background sync? It sounds like one could cache everything without lists. The permission requirement is not as streamlined as a hardcoded cache list, though. Trying to figure out if I can get minimum test coverage before jumping there :slight_smile:

1 Like

On a dynamic server such as php I would think the service worker could get a list of files from the server, and then cache them. I am not sure there is a way to simply tell the background sync find all files and cache them all with a static site, but honestly I have not explored that route. You could also write a template that outputs the list of files to an html page, tinysearch does this, they basically output all of their json data into an html file, to later be consumed by the tinysearch executable, again this would be a lot cleaner if Zola could template files other than html.

If your solution already does what you need then I would not worry about it, but if you are also trying to find a way to cache the entire site, and you find a way to make that work with background sync then please let me know!

1 Like

The HTML page actually worked. I built a macro that would iterate over taxonomies, the post section, and post assets that I used to show cached links on the offline page and populate the cache list. But that required the service worker pulling the whole page via fetch() - that felt too much. So I built another (invalid) HTML page just for the offline list via the same macro. It was still a little awkward, so I tried to approach it with another strategy, and it also worked.

It looks like I could use the macro to build available assets for the offline page, then access the list via the service worker’s loader script and pass it to the service worker itself using postMessage() ServiceWorker: postMessage() method - Web APIs | MDN

Then “listen” for the message in the service worker and start the background sync if the permission check passes.

If this works, it makes sense to pull available assets directly from the service worker to populate the offline page. I’ll see if I can test it.

Currently, I am iterating over section pages and getting the array into the service worker loader (iteration inside the head) via script tag attribute. And I can get add() working, and the cache path array is correct in the SW console. But I can’t get this array working with addAll() for some reason :slight_smile: Feels like I am almost there, though.

loader:

let cacheList = new Array(document.currentScript.getAttribute('data-cache'));

const registerServiceWorker = async () => {
  if ("serviceWorker" in navigator) {
    try {
      const registration = await navigator.serviceWorker.register("/sw.js", {
        scope: "/",
      });

      if (registration.installing) {
        console.log("Service worker installing");
      } else if (registration.waiting) {
        console.log("Service worker installed");
      } else if (registration.active) {
        console.log("Service worker active");
        registration.active.postMessage({
          type: "PRECACHE",
          payload: {
            urls: cacheList,
          },
        });
      }
    } catch (error) {
      console.error(`Registration failed with ${error}`);
    }
  }
};

registerServiceWorker();

sw message event:

onmessage = (event) => {
  console.log(`Service worker received payload: ${event.data.payload.urls}`);
};

log:

Caching the response to http://127.0.0.1:1111/syntax-theme-light.css?h=de41e87d2c9eca203b99 sw.js:20:21
Service worker received payload: ["/posts/information/", "/posts/markup/", "/posts/comments/", "/posts/pwa/", "/posts/audio/", "/posts/video/", "/posts/image/", "/posts/project/", "/posts/data/", ] sw.js:35:11
Service worker fetching http://127.0.0.1:1111/webfonts/Pixeboy.woff2

head partial:

{% if config.extra.manifest %}
  {% set posts = get_section(path="posts/_index.md") %}
  <script defer src="{{ get_url(path='sw-load.js', trailing_slash=false, cachebust=true) | safe }}" integrity="sha512-{{ get_hash(path='sw-load.js', sha_type=512, base64=true) | safe }}" data-cache='[{% for post in posts.pages %}"{{ post.path | safe }}", {% endfor %}]'></script>
{% endif %}

So far it’s just posts, but it works. And I can trigger add(cacheLink) with the payload. Now the last piece, addAll()

UPD
TypeError: Cache got basic response with bad status 404 while trying to add request http://127.0.0.1:1111/[%22/posts/information/%22,%20%22/posts/markup/%22,%20%22/posts/comments/%22,%20%22/posts/pwa/%22,%20%22/posts/audio/%22,%20%22/posts/video/%22,%20%22/posts/image/%22,%20%22/posts/project/%22,%20%22/posts/data/%22,%20] I see you

pushed feature-precache branch - looks like I might be able to sync everything without the sync API, and “pesky” permissions :slight_smile:

Service worker adding 
Array [ <1 empty slot> ]
​
0: "['/posts/information/','/posts/markup/','/posts/comments/','/posts/pwa/','/posts/audio/','/posts/video/','/posts/image/','/posts/project/','/posts/data/',]"
​
length: 1
​
<prototype>: Array []
Service worker failed precache TypeError: Cache got basic response with bad status 404 while trying to add request https://feature-precache--halve-z.netlify.app/['/posts/information/','/posts/markup/','/posts/comments/','/posts/pwa/','/posts/audio/','/posts/video/','/posts/image/','/posts/project/','/posts/data/',]
onmessage = (event) => {
  const data = event.data.payload.urls;
  event.waitUntil(
    (async () => {
      const cache = await caches.open(cacheName);
      console.log("Service worker adding" ,data);
      await cache.addAll(data).catch((error) => console.log("Service worker failed precache", error));
    })(),
  );
};

Hmm

Got the precache working! Finishing up before pushing

Pushed the current implementation. Now I need to start considering payload size/uplink speed in the precache events.

precache PR feat(pwa): implement precache by charlesrocket · Pull Request #24 · charlesrocket/halve-z · GitHub

@jieiku precache works great (tho I might need to investigate the kickin/installation)

Now I need to figure out how to approach the network…

Polished cache events and the dynamic list, and started to look for more things to build, but the rest of the PWA features are not very attractive. Support for sync APIs is very limited due to questionable implementations (same for the network) at the moment, and I don’t see it changing anytime soon. But it looks like push notifications are more accessible than I thought—I might get a light and free solution if I can digest that much JavaScript Setup Push Notification For PWA. Downside—possible delays, but I am planning to start with a basic “New Post” with daily notifications, so it should not be noticeable. Rewriting the service worker in Zig/WASM is on the list, but I am not sure yet if I could seamlessly deploy that without a custom helper for Zola.

Service worker notifications merged! feat(pwa): implement service worker notifications by charlesrocket · Pull Request #34 · charlesrocket/halve-z · GitHub

Dynamic cache name feat(pwa): use dynamic cache name · charlesrocket/halve-z@bd46521 · GitHub

It turned out that parameters via SW URL are not a good idea. So I improved stale-revalidate and optimized more for static content.