Featured image of post Implementing Hugo Progressive Web App Based on Workbox

Implementing Hugo Progressive Web App Based on Workbox

This article discusses how to use Workbox to add PWA functionality to Hugo static websites, enhancing loading speed and user experience through Service Worker. The advantages of PWA include fast loading, offline access, push notifications, and installation to the home screen. The article provides a detailed introduction to registering Service Worker, using Workbox's caching strategies, and offers detailed configuration steps and example code.

# Implement Hugo PWA based on Workbox

Recently added PWA functionality to a blog built on Hugo , significantly improving loading speed and user experience, even enabling offline access. As for how to achieve this, you need to understand Progressive Web Apps (PWA).

# What is PWA?

Progressive Web Apps (PWA) leverage modern Web APIs and traditional progressive enhancement strategies to create cross-platform web applications. These applications are ubiquitous, feature-rich, and provide users with an experience comparable to native apps.

Advantages of PWA:

  • ⚡️ Faster loading speed: PWA can cache important resources and load quickly even in poor network conditions.
  • ✈️ Offline Access: PWA can cache content, allowing users to access content even when offline.
  • 🔔 Push Notifications: Like native applications, PWAs can send push notifications to users to increase user engagement.
  • 📱 Install to Home Screen: Users can add your application to the desktop of their computer or phone and browse your web application like a native app.

The implementation principle of PWA is Service Worker. Service Worker is a special JavaScript resource that runs independently in the browser background, acting as a proxy between the web browser and the web server. It can intercept and handle network requests, cache resources, and push notifications.

Mainstream front-end frameworks Vue, React, and Angular all provide corresponding PWA plugins. As for static site generators like Hugo, we can implement PWA functionality by manually adding Workbox .

# Workbox

Workbox is a set of modules developed by the Google Chrome team, designed to simplify common Service Worker routing and caching operations. Each module is optimized for a specific aspect of Service Worker development. The goal of Workbox is to simplify the use of Service Workers as much as possible while providing the flexibility to meet the needs of complex applications when necessary.

If there is no Workbox, we need to manually write a Service Worker to listen to fetch events, cache resources, and implement offline access and other functions. Workbox provides a set of tools that can help us automatically generate a Service Worker and comes with some commonly used caching strategies, allowing us to focus more on business logic.

# Configure PWA

In the previous section, we learned about the concept and advantages of PWA, and how Workbox simplifies the development of Service Workers. Next, we will step by step configure PWA functionality for the Hugo blog.

# Register Service Worker

First, we need to register the Service Worker on the page. Add the following code snippet to your Hugo theme’s layouts/partials/footer/custom.html file (other themes may need adjustments based on the file structure):

<script>
    // Check that service workers are registered
    if ('serviceWorker' in navigator) {
        // Use the window load event to keep the page load performant
        window.addEventListener('load', () => {
            navigator.serviceWorker.register('/sw.js').then(reg => {
                console.log('Service worker registered with scope: ', reg.scope);
            }, err => {
                console.log('Service worker registration failed: ', err);
            });
        });
    }
</script>

Note

Note: Before registering the Service Worker, you need to first create the sw.js file, which we will complete in the next section.

After completing the registration, you can view the registration status of the Service Worker in the developer tools (F12) of the browser under “Application” -> “Service Workers” panel.

Service Worker

Service Worker

# Import Workbox

In the static folder of your Hugo site root directory, create the sw.js file. Then, add the following code in the sw.js file to import Workbox using CDN:

importScripts('https://cdn.bootcdn.net/ajax/libs/workbox-sw/7.1.0/workbox-sw.js');

# Cache strategy

Workbox provides some common caching strategies, such as CacheFirst, NetworkFirst, StaleWhileRevalidate, etc. Here we introduce some common strategies first.

# CacheOnly Cache only

CacheOnly

CacheOnly

Forced response from cache.

workbox.routing.registerRoute(
    new RegExp(regex),
    new workbox.strategies.CacheOnly()
);

# NetworkOnly Network Only

NetworkOnly

NetworkOnly

This caching strategy forces all requests to retrieve the latest data from the network, completely bypassing the cache.

workbox.routing.registerRoute(
    new RegExp(regex),
    new workbox.strategies.NetworkOnly()
);

# CacheFirst Cache Priority

CacheFirst

CacheFirst

This caching strategy prioritizes speed, first attempting to retrieve the response from the cache to display content to the user as quickly as possible. If the required data is not in the cache, it will then make a request to the network to obtain the data.

workbox.routing.registerRoute(
    new RegExp(regex),
    new workbox.strategies.CacheFirst()
);

# NetworkFirst 优先网络

NetworkFirst

NetworkFirst

This caching strategy prioritizes using the latest data, so it will first attempt to fetch the response from the network. If the network request fails, such as when the user is offline or the network connection is unstable, it will fall back to using cached data to ensure that the user can still access the content.

workbox.routing.registerRoute(
    new RegExp(regex),
    new workbox.strategies.NetworkFirst()
);

# StaleWhileRevalidate reads the cache while initiating a network request

StaleWhileRevalidate

StaleWhileRevalidate

This caching strategy prioritizes returning cached content (if available). Even if the cached content is valid, it will initiate a network request in the background to obtain the latest data, ensuring that the user ultimately sees the most up-to-date content. Although this strategy ensures that the cache is regularly updated for the user, it also means that every request generates network traffic, which can be a waste of bandwidth even if the data hasn’t changed.

workbox.routing.registerRoute(
    new RegExp(regex),
    new workbox.strategies.StaleWhileRevalidate()
);

# Strategy Configuration

Workbox not only provides the aforementioned strategies but also allows customization through configuration options such as cacheName, plugins, and expiration. You can customize routing behavior by defining the plugins you want to use. For example, you can configure the cache name, cache expiration, and cacheable response status codes as follows:

workbox.routing.registerRoute(
    new RegExp(regex),
    new workbox.strategies.CacheFirst({
        cacheName: 'my-cache',
        plugins: [
            new workbox.expiration.ExpirationPlugin({
                maxEntries: 60,
                maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days
            }),
            new workbox.cacheableResponse.CacheableResponsePlugin({
                statuses: [0, 200],
            }),
        ],
    })
);

# Site Configuration

# Global configuration

The following is the global cache configuration:

// Cache version number
let cacheVersion = '-240619';
// Maximum number of entries
const maxEntries = 100;

# Twitto Configuration

In order to ensure that users can view comments even when offline, the Twitto Comments API uses a NetworkFirst caching strategy. This means the browser will first attempt to fetch the latest data from the network, and if the network is unavailable, it will use the data from the cache.

workbox.routing.registerRoute(
    new RegExp('^https://comment\.cuterwrite\.top'),
    new workbox.strategies.NetworkFirst({
        plugins: [
            new workbox.expiration.ExpirationPlugin({
                maxEntries: maxEntries,
                maxAgeSeconds: 30 * 24 * 60 * 60,
            }),
            new workbox.cacheableResponse.CacheableResponsePlugin({
                statuses: [0, 200],
            }),
        ],
    })
);

# RSS and Sitemap Configuration

In order to ensure that users always obtain the latest RSS and Sitemap data, these pages are configured to use only the network strategy (NetworkOnly) without caching.

workbox.routing.registerRoute(
    new RegExp('^https://cuterwrite\.top/(index|sitemap)\.xml'),
    new workbox.strategies.NetworkOnly()
);

# HTML Configuration

In order to ensure that users can quickly load pages while also obtaining the latest content, the website uses the StaleWhileRevalidate caching strategy for HTML pages. This means the browser will prioritize displaying the page from the cache while simultaneously making a request to the server in the background to fetch the latest version, which will be used on the next request.

workbox.routing.registerRoute(
    new RegExp('.*\.html'),
    new workbox.strategies.StaleWhileRevalidate({
        cacheName: 'html-cache' + cacheVersion,
        plugins: [
            new workbox.expiration.ExpirationPlugin({
                maxEntries: maxEntries,
                maxAgeSeconds: 30 * 24 * 60 * 60,
            }),
            new workbox.cacheableResponse.CacheableResponsePlugin({
                statuses: [0, 200],
            }),
        ],
    })
);

# Google Fonts Configuration

In order to ensure the font files are updated while also utilizing caching to speed up page loading, the website uses a CacheFirst caching strategy for Google Fonts resources and sets a long cache expiration time.

workbox.routing.registerRoute(
    new RegExp('.*\.(?:woff|woff2|ttf|otf|eot)'),
    new workbox.strategies.StaleWhileRevalidate({
        cacheName: 'google-fonts' + cacheVersion,
        plugins: [
            // Use expiration plugin to control the number and time of cache entries
            new workbox.expiration.ExpirationPlugin({
                // Maximum number of cache entries
                maxEntries: maxEntries,
                // Maximum cache time 30 days
                maxAgeSeconds: 30 * 24 * 60 * 60,
            }),
            // Use cacheableResponse plugin to cache requests with status code 0
            new workbox.cacheableResponse.CacheableResponsePlugin({
                statuses: [0, 200],
            }),
        ],
    })
);

# CDN Configuration

In order to maximize the use of cache to speed up page loading, the website adopts a CacheFirst caching strategy for resources from common CDNs and sets a long cache expiration time.

workbox.routing.registerRoute(
    new RegExp('^https://(?:cdn\.bootcdn\.net|unpkg\.com|cdn\.jsdelivr\.net)'),
    new workbox.strategies.CacheFirst({
        cacheName: 'cdn' + cacheVersion,
        fetchOptions: {
            mode: 'cors',
            credentials: 'omit',
        },
        plugins: [
            new workbox.expiration.ExpirationPlugin({
                maxEntries: maxEntries,
                maxAgeSeconds: 30 * 24 * 60 * 60,
            }),
        ],
    })
);

# Umani website statistics configuration

In order to ensure the accuracy of website statistics, the website adopts the NetworkOnly strategy for Umani website statistics requests and uses the BackgroundSyncPlugin to ensure that data is eventually sent successfully even when the network is offline.

workbox.routing.registerRoute(
    new RegExp('^https://analytics\.cuterwrite\.top/uma'),
    new workbox.strategies.NetworkOnly({
        plugins: [
            // Use background sync plugin to implement background synchronization
            new workbox.backgroundSync.BackgroundSyncPlugin('Optical_Collect', {
                maxRetentionTime: 12 * 60,
            }),
        ],
    })
);

# Image Configuration

In order to speed up image loading and reduce the number of network requests, the website uses a CacheFirst caching strategy for image resources and sets a long cache expiration time.

workbox.routing.registerRoute(
    new RegExp('^(https://cuterwrite-1302252842\.file\.myqcloud\.com|https://s2\.loli\.net)'),
    new workbox.strategies.CacheFirst({
        cacheName: 'image-cache' + cacheVersion,
        plugins: [
            new workbox.expiration.ExpirationPlugin({
                maxEntries: maxEntries,
                maxAgeSeconds: 30 * 24 * 60 * 60,
            }),
            new workbox.cacheableResponse.CacheableResponsePlugin({
                statuses: [0, 200],
            }),
        ],
    })
);

# Suffix match configuration

In order to balance loading speed and content updates, the website uses the StaleWhileRevalidate caching strategy for static files (such as images, CSS, and JavaScript files) that are not matched by the domain name.

workbox.routing.registerRoute(
    new RegExp('.*\.(?:png|jpg|jpeg|svg|gif|webp|ico)'),
    new workbox.strategies.StaleWhileRevalidate()
);
workbox.routing.registerRoute(
    new RegExp('.*\.(css|js)'),
    new workbox.strategies.StaleWhileRevalidate()
);

# Default behavior configuration

In order to handle requests that are not matched by any custom routing rules, the website is configured with a default caching behavior, using the NetworkFirst strategy and setting a network timeout to balance resource retrieval speed and offline availability.

workbox.routing.setDefaultHandler(
    // Prefer using cache, if cache is not available then use network request
    new workbox.strategies.NetworkFirst({
        networkTimeoutSeconds: 3,
    })
);

# Full configuration

sw.js
importScripts('https://cdn.bootcdn.net/ajax/libs/workbox-sw/7.1.0/workbox-sw.js');

// Cache version number
let cacheVersion = '-240619';
// Maximum number of entries
const maxEntries = 100;

if (workbox) {
    console.log(`Yay! Workbox is loaded 🎉`);
    // Comment cache
    workbox.routing.registerRoute(
        new RegExp('^https://comment\.cuterwrite\.top'),
        new workbox.strategies.NetworkFirst({
            plugins: [
                new workbox.expiration.ExpirationPlugin({
                    maxEntries: maxEntries,
                    maxAgeSeconds: 30 * 24 * 60 * 60,
                }),
                new workbox.cacheableResponse.CacheableResponsePlugin({
                    statuses: [0, 200],
                }),
            ],
        })
    );
    // Do not cache rss and sitemap
    workbox.routing.registerRoute(
        new RegExp('^https://cuterwrite\.top/(index|sitemap)\.xml'),
        new workbox.strategies.NetworkOnly()
    );
    // Cache HTML
    workbox.routing.registerRoute(
        new RegExp('.*\.html'),
        new workbox.strategies.StaleWhileRevalidate({
            cacheName: 'html-cache' + cacheVersion,
            plugins: [
                new workbox.expiration.ExpirationPlugin({
                    maxEntries: maxEntries,
                    maxAgeSeconds: 30 * 24 * 60 * 60,
                }),
                new workbox.cacheableResponse.CacheableResponsePlugin({
                    statuses: [0, 200],
                }),
            ],
        })
    );
    // Cache Google Fonts
    workbox.routing.registerRoute(
        new RegExp('.*\.(?:woff|woff2|ttf|otf|eot)'),
        new workbox.strategies.StaleWhileRevalidate({
            cacheName: 'google-fonts' + cacheVersion,
            plugins: [
                // Use expiration plugin to control cache entry number and time
                new workbox.expiration.ExpirationPlugin({
                    // Maximum number of cache entries
                    maxEntries: maxEntries,
                    // Maximum cache time 30 days
                    maxAgeSeconds: 30 * 24 * 60 * 60,
                }),
                // Use cacheableResponse plugin to cache requests with status code 0
                new workbox.cacheableResponse.CacheableResponsePlugin({
                    statuses: [0, 200],
                }),
            ],
        })
    );
    // Cache public libraries like bootcdn, unpkg, jsdelivr using regex
    workbox.routing.registerRoute(
        new RegExp('^https://(?:cdn\.bootcdn\.net|unpkg\.com|cdn\.jsdelivr\.net)'),
        new workbox.strategies.CacheFirst({
            cacheName: 'cdn' + cacheVersion,
            fetchOptions: {
                mode: 'cors',
                credentials: 'omit',
            },
            plugins: [
                new workbox.expiration.ExpirationPlugin({
                    maxEntries: maxEntries,
                    maxAgeSeconds: 30 * 24 * 60 * 60,
                }),
            ],
        })
    );
    // Self-built UMA statistics script: https://analytics.cuterwrite.top/uma
    workbox.routing.registerRoute(
        new RegExp('^https://analytics\.cuterwrite\.top/uma'),
        new workbox.strategies.NetworkOnly({
            plugins: [
                // Use background sync plugin for background synchronization
                new workbox.backgroundSync.BackgroundSyncPlugin('Optical_Collect', {
                    maxRetentionTime: 12 * 60,
                }),
            ],
        })
    );
    // Cache bucket images https://cuterwrite-1302252842.file.myqcloud.com/
    workbox.routing.registerRoute(
        new RegExp('^(https://cuterwrite-1302252842\.file\.myqcloud\.com|https://s2\.loli\.net)'),
        new workbox.strategies.CacheFirst({
            cacheName: 'image-cache' + cacheVersion,
            plugins: [
                new workbox.expiration.ExpirationPlugin({
                    maxEntries: maxEntries,
                    maxAgeSeconds: 30 * 24 * 60 * 60,
                }),
                new workbox.cacheableResponse.CacheableResponsePlugin({
                    statuses: [0, 200],
                }),
            ],
        })
    );

    // Suffix matching for other static files not matched by domain
    workbox.routing.registerRoute(
        new RegExp('.*\.(?:png|jpg|jpeg|svg|gif|webp|ico)'),
        new workbox.strategies.StaleWhileRevalidate()
    );
    workbox.routing.registerRoute(
        new RegExp('.*\.(css|js)'),
        new workbox.strategies.StaleWhileRevalidate()
    );

    // Default match for remaining requests
    workbox.routing.setDefaultHandler(
        // Prefer cache, if cache is not available, use network request
        new workbox.strategies.NetworkFirst({
            networkTimeoutSeconds: 3,
        })
    );

} else {
    console.log(`Boo! Workbox didn't load 😬`);
}

# manifest.json

  1. Create manifest.json file

Create a manifest.json file in the static folder at the root directory of your Hugo blog, which contains metadata about your blog, such as name, icon, and display options.

{
    "name": "Your Blog Name",
    "short_name": "Blog Short Name",
    "start_url": "/",
    "display": "standalone",
    "background_color": "#ffffff",
    "theme_color": "#000000",
    "icons": [{
            "src": "/icon-192x192.png",
            "sizes": "192x192",
            "type": "image/png"
        },
        {
            "src": "/icon-512x512.png",
            "sizes": "512x512",
            "type": "image/png"
        }
    ]
}

Note

Note: Replace icon-192x192.png and icon-512x512.png with your own icon filenames. And make sure to place these two icon files in the static folder of your Hugo blog. If you want to modify the theme color and background color, you can modify the theme_color and background_color fields.

  1. Link manifest.json file

In your Hugo blog’s layouts/partials/head/custom.html file, add the following code to link the manifest.json file to your website:

<link rel="manifest" href="/manifest.json">

After completing the above steps, your Hugo blog will have PWA functionality, allowing users to access your site as if it were a native application.

# References

Licensed under CC BY-NC-SA 4.0
本博客已稳定运行
总访客数: Loading
总访问量: Loading
发表了 25 篇文章 · 总计 60.67k

Built with Hugo
Theme Stack designed by Jimmy
基于 v3.27.0 分支版本修改