# 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.
# 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
Forced response from cache.
workbox.routing.registerRoute(
new RegExp(regex),
new workbox.strategies.CacheOnly()
);
# NetworkOnly Network Only
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
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 优先网络
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
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://cloud.cuterwrite.fun/
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
- 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.
- 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.