TLDR: How I converted my hugo blog into a progressive web app for a richer and native-like user experience
Special Mention: I use a modified version of hugo-papermod and I just want to appreciate the developer of the theme. Its just right for my use case and the extensive documention is a treat to work with.
I am bullish on Progressive Web Apps. I get the limitations it faces in the current times but the idea in itself is quite promising and possesses a lot of untapped potential.
The limitations I’m referring to are the ones that I personally faced:
- The documentation and information present around the topic on the internet.
- Cross browser compatibilty. Chrome heavily invests in the advancement of PWAs. Safari not so much. The consensus is still questionable. iOS, iPadOS support is out! But I’ve faced some problems on Safari desktop (maybe that could be another post).
- The ease to test features. Cross browser support is a contenter to this problem. But in general, testing on Chrome also has been quite a bit of a hassle. Local env testing doesn’t completely guarantee it’ll work on production. This could be a personal thing, as I haven’t invested a lot of time on developing testing opinions and frameworks for this project. If you are reading this, and could guide me in the right direction, hit me up!
Regardless, I decided to try it out. The positives it would provide me were apt for my usecase i.e. a personal blog. Some of them being:
- A blog is mostly static. So providing
offline-support
by caching static assets made sense. Developing an mobile app for a blog felt like over-engineering - My main goal with using PWAs is to eventually use push notifications instead of newsletters to send updates about my posts. I find it less intrusive and frankly, I haven’t seen anyone do it yet. (This is still in progress)
- Doesn’t require app store
Adding PWA support to this blog was comprised of two parts mainly:
adding a manifest.json
The manifest is the entry point for a PWA and is basically what tells the browser about all the metadata information required for the browser to qualify your website as a PWA. The main components of a barebones manifest.json
are:
- name and shortname: these are what your PWA will be called once installed.
- icons: self-explanatory. point all the accepted resolution icon sizes so the platform can lookup icons to display for your app.
- start_url: the first thing that should open when you click on the app icon
- orientation: self-explanatory
- background-color and theme-color: this is to define a site wide theme color and colors for the app splash screen
adding a service worker sw.js
This is where the PWA capabilities would be added in thr form of code. On the exterior, its a javascript file. How it works is it registers as a service worker on your compatible browser and is responsible for functionality like, sync, push-notification without opening the browser.
Some note-worthy things about a service worker are:
- its a worker so it does not have access to the DOM.
- it acts as a network proxy. So this would be a good place to manage network requests on your page.
First step is registering the service worker on your page. Google provides the boilerplate to do that here
I have currently derived my service worker from this relatively popular repo and am tailoring it based on my needs. It enables the below functionalities for you:
- It fetches content the first time and persists it in the browser cache. This enables faster user experience along with providing offline support to already visited pages.
- Just like a 404 page, an offline page can be served.
- It has a TTL (Time to live) configuration which decided the refresh timeline.
- Upon detecting a new version, it replaces cache with new content.
Some of the important things to note in the sw.js are:
- BASE_CACHE_FILES: defines the files in your hugo blog that need to be caches. I keep my css, manifest and favicons here.
- OFFLINE_CACHE_FILES: this contains path names to files require to serve the “No internet. You’re offline” page
- NOT_FOUND_CACHE_FILES: this is for 404 page.
- MAX_TTL: the time to live is defined in this variable (in seconds)
- CACHE_BLACKLIST: this value prevents me from caching something that’s not mine. So I stop my service worker from cachine anything that does not start with https://vishalraj.xyz (Note: for local testing, I’ve found adding http://localhost also works!)
- the 3 event listeners in the sw.js worth noting are:
- install
- activate
- fetch
To delve deeper into the lifecycle of the service worker, you can go through this document by Jake Archibald. He does an excellent job explaining the fundamentals
Below is the pseudo-code explaining the working of this sw.js file.
Register the service worker on the browser
Install and activate the PWA
Intercept request for content
if(content present in cache){
if(content expired based on TTL values){
try{
fetching fresh content.
store it in cache
return
}catch{
serve cached content
}
}
else{
serve cached content
}
}else{
try{
fetch the content for the first time
if(OK status){
store in cache
return
}else{
show "not found page"
}
}catch{
show "offline page"
Done.
Adding static files
Based on my needs, I had to add 1 additional static file for the offline
page. Since its a hugo project, I defined a new layout inside layouts/offline/single.html
with the following content
{{- define "main" }}
<div class="offline">You're offline</div>
{{- end }}{{/* end main */ -}}
How to Test
Chrome is an accepted browser of choice for testing PWAs. You can find the details under Applications
tab within developer tools
Lighthouse on chrome also helps audit and benchmark your PWA. It also provides guidelines to improve the PWA experience.
Note to readers: I’m not sure if this will work for everyone. If in any case you face issues or have suggestions on how to improve, do let me know.