Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
ServiceWorker for github pages.

ServiceWorker for github pages

This is a ServiceWorker template to turn small github pages into offline ready app.

Why ?

Whenever I make small tools & toys, I create github repo and make a demo page using github pages (like this one).
Often these "apps" are just an index.html file with all the nessesary CSS and JavaScript in it (or maybe 2-3 html/css/js files). I wanted to cache these files so that I can access my tools offline as well.

Notes

Make sure your github pages have HTTPS enforced, you can check Settings > GitHub Pages > Enforce HTTPS of your repository.

Persoanl github pages are all hosted under one root domain (https://[username].github.io/). This caused some stumblings...

  • When deleting outdated caches for the app, caches.keys() returns all the caches under the domain. We need to filter and delete only caches associated with the app.
  • Path for ServiceWorker should be specified in absolute path
  • the last '/' in URL is important. See following section "When is SwerviceWorker summoned"

When is ServiceWorker summoned ?

In this example ServiceWorker is present to every page under https://{guthub_username}.github.io/{repository}/, but not https://{guthub_username}.github.io/{repository}. Notice any difference? that's right, the trailing /

Assuming you have index.html at the root of your repository, a visitor may access /{repository} or /{repository}/ to get same index.html. This is bad practice in general and one should redirect to the other (or that's what Jake said), but most importantly when a visitor is seeing index.html from /{repository} (no '/' at the end), ServiceWorker will not be present on the page.

Just in case your server doesn't handle trailing /, I have <link rel="canonical" href="https://{guthub_username}.github.io/{repository}/" /> in my index.html header *1.

*1 I mean ... I don't know why I had link rel="canonical" in my code. I do remember having issue with trailing / thought ! So it's the only reason I can think of me wanting to use unfamiliar thing like canonical ¯_(ツ)_/¯

<!DOCTYPE html>
<html>
<head>
<title>Title</title>
<link rel="canonical" href="https://{guthub_username}.github.io/{repository}/" />
<style type="text/css">
/* --- application CSS here --- */
*{
background-color: #F5F4F0;
font-family: Georgia, serif;
}
</style>
</head>
<body>
<h1>Content</h1>
<script>
// register ServiceWorker, remember to use absolute path!
if (navigator.serviceWorker) {
navigator.serviceWorker.register('/{repository}/sw.js', {scope: '/{repository}/'})
}
// --- Application code here ---
</script>
</body>
</html>
var APP_PREFIX = 'ApplicationName_' // Identifier for this app (this needs to be consistent across every cache update)
var VERSION = 'version_01' // Version of the off-line cache (change this value everytime you want to update cache)
var CACHE_NAME = APP_PREFIX + VERSION
var URLS = [ // Add URL you want to cache in this list.
'/{repository}/', // If you have separate JS/CSS files,
'/{repository}/index.html' // add path to those files here
]
// Respond with cached resources
self.addEventListener('fetch', function (e) {
console.log('fetch request : ' + e.request.url)
e.respondWith(
caches.match(e.request).then(function (request) {
if (request) { // if cache is available, respond with cache
console.log('responding with cache : ' + e.request.url)
return request
} else { // if there are no cache, try fetching request
console.log('file is not cached, fetching : ' + e.request.url)
return fetch(e.request)
}
// You can omit if/else for console.log & put one line below like this too.
// return request || fetch(e.request)
})
)
})
// Cache resources
self.addEventListener('install', function (e) {
e.waitUntil(
caches.open(CACHE_NAME).then(function (cache) {
console.log('installing cache : ' + CACHE_NAME)
return cache.addAll(URLS)
})
)
})
// Delete outdated caches
self.addEventListener('activate', function (e) {
e.waitUntil(
caches.keys().then(function (keyList) {
// `keyList` contains all cache names under your username.github.io
// filter out ones that has this app prefix to create white list
var cacheWhitelist = keyList.filter(function (key) {
return key.indexOf(APP_PREFIX)
})
// add current cache name to white list
cacheWhitelist.push(CACHE_NAME)
return Promise.all(keyList.map(function (key, i) {
if (cacheWhitelist.indexOf(key) === -1) {
console.log('deleting cache : ' + keyList[i] )
return caches.delete(keyList[i])
}
}))
})
)
})
@jpraet

This comment has been minimized.

Copy link

@jpraet jpraet commented Feb 3, 2018

Thanks for this! Works perfectly.

@satyakresna

This comment has been minimized.

Copy link

@satyakresna satyakresna commented Mar 16, 2018

It works. Thank's a lot Mariko! Anyway, there's a typo:
screen shot 2018-03-16 at 7 33 16 pm

@kosamari

This comment has been minimized.

Copy link
Owner Author

@kosamari kosamari commented Mar 16, 2018

fixed !

@lhuria94

This comment has been minimized.

Copy link

@lhuria94 lhuria94 commented Jun 24, 2018

Hey @kosamari, I tried implementing this, but it didnt work on my repo. It just keep on taking my site to 404 not found.

Here is the repo: https://github.com/lhuria94/lhuria94.github.io

Can you suggest?

@dabyland

This comment has been minimized.

Copy link

@dabyland dabyland commented Feb 17, 2019

This is an awesome tutorial, thank you so much @kosamari!! 🎉 👍

@AndreyKondakov

This comment has been minimized.

Copy link

@AndreyKondakov AndreyKondakov commented Mar 15, 2019

https://andreykondakov.github.io/ What is not wrong, can you check ?(

@MatthewRiggott

This comment has been minimized.

Copy link

@MatthewRiggott MatthewRiggott commented May 2, 2019

This was tremendously useful!
Also, $CACHE_NAME = $APP_NAMESPACE + $VERSION never occurred to me as a means of cache busting a service worker.

@digitella

This comment has been minimized.

Copy link

@digitella digitella commented Sep 17, 2020

Noob question. I have apex domain and cname hooked up to github pages repo. How will the sw.js and html code in tag change?

right now I have <script>
// register ServiceWorker, remember to use absolute path!
if (navigator.serviceWorker) {
navigator.serviceWorker.register('/aaig.cf/sw2.js', {scope: '/repo name/'}) // use repo name instead of a cname / domain name?
}

// --- Application code here ---
</script>

@uluumbch

This comment has been minimized.

Copy link

@uluumbch uluumbch commented Dec 13, 2020

this is work,
even in my console say "service worker : installed" but i still can't install the pwa to my device.
am i have wrong scope in manifest.json? can you give me example pleasee

@HasanQari

This comment has been minimized.

Copy link

@HasanQari HasanQari commented Jun 18, 2021

it's work on localhost with no errors
but when hosted on GitHub it doesn't work it give me this error 👎

Failure reason

No matching service worker detected. You may need to reload the page, or check that the scope of the service worker for the current page encloses the scope and start URL from the manifest.

AND
this error in the console
A bad HTTP response code (404) was received when fetching the script.


My repo name: PWAPP
My project is

  • index.html
  • PWA *folder
    • sw.js
    • manifest.json
  • icon *folder has png image

My index.html
<title>Document</title>
<main>
    <div id="content-area-start-page" class="container mb-5">
        <div id="frame-start-page" class="text-center align-content-center flex-column mt-5 py-3 px-4"
            style="border: 3px solid #ccc; border-radius: 50%;">
            <img id="logo-start-page" src="res/img/icon.svg" alt="logo" class="img-fluid" width="70%">
            <h5 id="app-name-start-page" class="mb-3">App Name</h6>
                <p id="app-description-start-page">مرحبا بكم في [اسم التطبيق] سهل وسريع</p>
                <a id="btn-next-start-page" class="btn btn-primary" href="">التالي</a>
        </div>
    </div>
</main>

<!-- BS Js -->
<script src="asset/js/BSjs/bootstrap.min.js"></script>
<script src="asset/js/BSjs/bootstrap.js"></script>

<!-- Main Js -->
<script src="asset/js/app.js"></script>

<!-- Extra Js -->
<script>
    if ('serviceWorker' in navigator) {
        navigator.serviceWorker.register('PWAPP/service-worker.js', { scope: '/PWAPP/'})
            // ('/service-worker.js')
            .then(function (registration) {
                console.log("success load");
                console.log(registration);
            })
            .catch(function (err) {
                console.log(err);
            });
    }
</script>

#######################################################

my sw.js

var APP_PREFIX = 'ApplicationName_' // Identifier for this app (this needs to be consistent across every cache update)
var VERSION = 'version_01' // Version of the off-line cache (change this value everytime you want to update cache)
var CACHE_NAME = APP_PREFIX + VERSION
var URLS = [ // Add URL you want to cache in this list.
'/PWAPP/', // If you have separate JS/CSS files,
'/PWAPP/index.html' // add path to those files here
]

// Respond with cached resources
self.addEventListener('fetch', function (e) {
console.log('fetch request : ' + e.request.url)
e.respondWith(
caches.match(e.request).then(function (request) {
if (request) { // if cache is available, respond with cache
console.log('responding with cache : ' + e.request.url)
return request
} else { // if there are no cache, try fetching request
console.log('file is not cached, fetching : ' + e.request.url)
return fetch(e.request)
}

  // You can omit if/else for console.log & put one line below like this too.
  // return request || fetch(e.request)
})

)
})

// Cache resources
self.addEventListener('install', function (e) {
e.waitUntil(
caches.open(CACHE_NAME).then(function (cache) {
console.log('installing cache : ' + CACHE_NAME)
return cache.addAll(URLS)
})
)
})

// Delete outdated caches
self.addEventListener('activate', function (e) {
e.waitUntil(
caches.keys().then(function (keyList) {
// keyList contains all cache names under your username.github.io
// filter out ones that has this app prefix to create white list
var cacheWhitelist = keyList.filter(function (key) {
return key.indexOf(APP_PREFIX)
})
// add current cache name to white list
cacheWhitelist.push(CACHE_NAME)

  return Promise.all(keyList.map(function (key, i) {
    if (cacheWhitelist.indexOf(key) === -1) {
      console.log('deleting cache : ' + keyList[i] )
      return caches.delete(keyList[i])
    }
  }))
})

)
})

#######################################################

my manifest

{
"name": "App Name",
"short_name": "App",
"start_url": "../",
"icons": [
{
"src": "../icons/manifest-icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "../icons/manifest-icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable any"
}
],
"theme_color": "#3367D6",
"background_color": "#3367D6",
"display": "fullscreen",
"orientation": "portrait"
}


anybody can help me?

thank you so mush

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment