mdBook
1 is a great utility to create online books from Markdown files. It is used extensively in the Rust community. Here are some books made with mdBook
:
Motivation
- I usually read the books while commuting and this usually consumes mobile data. Given the contents don’t change that often, I thought it would be useful if they were cached.
- Prevent the dreaded “no internet connection” screen (downasaur).
- I want to eventually convert an
mdBook
into a PWA5. Offline support is important for reliability.
Google Workbox
A service worker6 is responsible for caching and retrieving resources from the cache (among other things). Google’s Workbox7 is a library that bakes in a set of best practices and removes the boilerplate every developer writes when working with service workers. It allows us to precache assets, and also cache assets at runtime. There are several caching strategies8 which can be selected depending on your specific use-case.
Customising the mdBook
I followed Google’s workbox codelab9 and modified the steps accordingly. mdBook allows us to add additional javascript via the additional-js
option in book.toml
. We need to add a service worker registration code in the template page:
// source: https://codelabs.developers.google.com/codelabs/workbox-lab/#2
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('sw.js')
.then(registration => {
console.log(`Service Worker registered! Scope: ${registration.scope}`);
})
.catch(err => {
console.log(`Service Worker registration failed: ${err}`);
});
})
}
The code attempts to install a service worker(sw.js
)
We can save the above code in a file, register-sw.js
and save it at the root level of the book. Then in book.toml
add the following:
[output.html]
additional-js=["register-sw.js"]
Next I created a basic service worker file(sw.js
) at the root level of the book.
// source: https://codelabs.developers.google.com/codelabs/workbox-lab/#3
importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.5.0/workbox-sw.js');
if (workbox) {
console.log(`Yay! Workbox is loaded 🎉`);
workbox.precaching.precacheAndRoute([]);
} else {
console.log(`Boo! Workbox didn't load 😬`);
}
However, when I used the code above, routes with search parameters were not being served properly. I removed search parameters using ignoreUrlParametersMatching
10. Also, besides precaching, we need to cache some assets like google fonts which mdBook uses and then update the cache when new versions are available. I changed the sw.js
to the following:
importScripts(
"https://storage.googleapis.com/workbox-cdn/releases/3.5.0/workbox-sw.js"
);
if (workbox) {
console.log(`Yay! Workbox is loaded 🎉`);
workbox.precaching.precacheAndRoute([], {
ignoreUrlParametersMatching: [/.*/]
});
// Cache the Google Fonts stylesheets with a stale while revalidate strategy.
workbox.routing.registerRoute(
/^https:\/\/fonts\.googleapis\.com/,
new workbox.strategies.CacheFirst({
cacheName: "google-fonts-stylesheets"
})
);
self.addEventListener('activate', function(event) {
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.filter(function(cacheName) {
}).map(function(cacheName) {
return caches.delete(cacheName);
})
);
})
);
});
self.addEventListener('message', function(event) {
if (event.data.action === 'skipWaiting') {
self.skipWaiting();
}
});
} else {
console.log(`Boo! Workbox didn't load 😬`);
}
I modified the registration-sw.js
to prompt the user when updates are available based on suggestions on this post11.
var indexController = this;
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker
.register("sw.js")
.then(registration => {
if (registration.waiting) {
indexController.updateReady(registration.waiting);
return;
}
if (registration.installing) {
indexController.trackInstalling(registration.installing);
return;
}
registration.addEventListener("updatefound", function() {
indexController.trackInstalling(registration.installing);
});
console.log(`Service Worker registered! Scope: ${registration.scope}`);
})
.catch(err => {
console.log(`Service Worker registration failed: ${err}`);
});
});
}
function trackInstalling(worker) {
var indexController = this;
worker.addEventListener("statechange", function() {
if (worker.state == "installed") {
indexController.updateReady(worker);
}
});
}
function updateReady(worker) {
var res = confirm("New version available, reload page?");
if (res) {
worker.postMessage({ action: "skipWaiting" });
location.reload();
}
}
Here we are showing a confirm box to prompt the user that there is a new version of the book available. It is based on Jake Archibald’s suggestion12. Ideally, you would show a banner when a service worker has updated and waiting to install.
Now, everytime we build the book, we need to generate the proper “revision hashes” to the files in the manifest entries. We can use the workbox-cli
13 to inject a precache manifest into the service worker. You need node.js
to use the cli. During the setup, we can choose which file types we want to cache. The configuration is saved in workbox-config.js
.
module.exports = {
"globDirectory": "book/",
"globPatterns": [
"**/*.{css,js,html,png,eot,svg,ttf,woff,woff2,json}"
],
"swDest": "book/sw.js",
"swSrc": "sw.js"
};
You can modify the globPatterns
accordingly. The production service worker will be book/sw.js
. After building the book, we need to run the workbox injectManifest workbox-config.js
command.
Automating the build in Travis CI
You can customise the build to generate the proper service worker every time there is a change. I made a sample repo14.
You should set an environment variable GITHUB_TOKEN
in Travis CI
. You’ll need to generate a personal access token with the public_repo
or repo
scope on Github
. This can be done in Settings
> Developer settings
> Personal access tokens
> Generate new token
That’s it!
Hope you find this useful and integrate service workers in your mdBook so your users can enjoy your content wherever they are! If you want to help, you can contribute to make the sample repo14 better.
References
- mdBook [return]
- The Rust Programming Language [return]
- Embedded Book [return]
- mdBook user Documentation [return]
- Progressive Web Apps [return]
- Service Workers [return]
- Workbox [return]
- Workbox strategies [return]
- Workbox Codelab [return]
- Ignore URL Parameters [return]
- Activate updated service worker on refresh [return]
- Toast to show sw updated [return]
- Workbox CLI [return]
- Sample repository [return]