This site is a Progressive Web App (PWA). Visitors can install it to their home screen on Android or iOS, and most of the content works without an internet connection after the first visit. Despite using no backend and deploying via GitHub Pages, it scores well on Lighthouse for performance, accessibility, and PWA criteria.
This article covers how the PWA features are implemented on top of Jekyll.
What Makes a Site a PWA?
Three things are required to meet the minimum PWA criteria:
- HTTPS — served over a secure connection (GitHub Pages does this for free)
- Web App Manifest — a JSON file describing the app: name, icons, theme colour, display mode
- Service Worker — a JavaScript file that runs in the background, enabling offline support and caching
Beyond those basics, a good PWA also loads fast, works on all screen sizes, and feels like a native app rather than a website opened in a browser tab.
The Web App Manifest
The manifest lives at /manifest.json and tells the browser how to present the app when installed:
{
"name": "Shivering",
"short_name": "Shivering",
"description": "A collection of HTML5 games, interactive experiments, and browser tools.",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#667eea",
"icons": [
{
"src": "/images/touch/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/images/touch/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
"display": "standalone" makes the app open without browser chrome (no address bar, no back/forward buttons) when launched from the home screen. It genuinely looks and feels like a native app.
The manifest is linked from the HTML <head>:
<link rel="manifest" href="/manifest.json">
The Service Worker
The service worker is the heart of a PWA. It intercepts network requests, caches responses, and serves cached content when the network is unavailable.
Registration
The service worker is registered from a small inline script in the page:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js');
}
Jekyll renders this via an _includes/service-worker-register.html partial, which is included in the default layout.
Cache Strategy: Cache-First with Network Fallback
The service worker uses a cache-first strategy for static assets: it serves the cached version immediately (fast!) and only contacts the network if the asset isn’t in the cache yet.
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(cached => {
if (cached) return cached;
return fetch(event.request).then(response => {
const clone = response.clone();
caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
return response;
});
})
);
});
Versioned Cache Names
To force browsers to fetch fresh assets after a deployment, the cache name includes a version number:
const CACHE_NAME = 'shivering-games-v29';
On activation, the service worker deletes any caches with a different name. This ensures users always get the latest version after a site update, while enjoying offline access the rest of the time.
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(
keys
.filter(key => key !== CACHE_NAME)
.map(key => caches.delete(key))
)
)
);
});
Pre-caching on Install
During the service worker’s install phase, it pre-fetches and caches the most important assets so the site works immediately offline after the first visit — even before the user has navigated to those pages:
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache =>
cache.addAll([
'/',
'/games/',
'/playground/',
'/games/snake/',
// ... more URLs
])
)
);
});
Performance: Why Static + CDN Wins
GitHub Pages is a free static hosting service. Combined with Cloudflare as a CDN, every page load benefits from:
- Edge caching: Assets are served from a data centre near the visitor
- HTTP/2: Multiple assets load in parallel over a single connection
- Brotli/gzip compression: Text-based assets (HTML, CSS, JS) are compressed before transfer
- Immutable caching headers: Assets with version hashes can be cached indefinitely
For a personal project, this setup costs effectively nothing while delivering performance that rivals paid hosting.
Offline Game Play
One practical benefit of the service worker is that games work offline. Once a visitor has played Snake or Tetris once, those pages are cached. Open the app on a plane or train with no connectivity, and the games still load and run perfectly.
This is possible because games on this site are entirely self-contained: pure HTML, CSS, and vanilla JavaScript. No API calls, no external dependencies at runtime. The service worker caches everything needed at install time.
Theme Toggle: Persisted Without a Server
The site supports light and dark themes. The preference is stored in localStorage and applied before the page renders to avoid a flash of the wrong theme:
// In the <head>, before any CSS loads
const theme = localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
Reading from localStorage and setting a data attribute is synchronous, so it happens before the browser has a chance to paint anything. The result is seamless: no visible flicker between themes.
Jekyll: Static Generation Without Compromise
Jekyll compiles the site at deploy time. All the game pages, blog posts, and tool pages are pre-rendered to plain HTML files. There’s no PHP, no Node server, no database — just files.
This means:
- Security: No server-side code means no server-side vulnerabilities
- Scalability: Static files can be served from any CDN without special infrastructure
- Simplicity: The deployment pipeline is
git push; GitHub Actions builds and deploys automatically
Using Jekyll’s layout system, every game page inherits the same HTML shell (navigation, footer, theme toggle, service worker registration, AdSense) from the default layout. Game-specific settings like background colours are just front matter variables.
What Lighthouse Says
Running Lighthouse on the homepage gives scores in the 90s for Performance, Accessibility, Best Practices, and SEO. The main drag on the Performance score is the Google AdSense script, which is a third-party resource I can’t optimise directly.
Progressive Web App criteria are all met: installable, works offline, has a manifest, uses HTTPS.
Conclusion
Building a PWA on top of a static Jekyll site is surprisingly achievable. The key ingredients are:
- A well-structured manifest with proper icons
- A versioned service worker with a cache-first strategy
- A sensible pre-cache list covering the most important pages
- Fast, static HTML that doesn’t rely on server-side rendering
The result is a site that feels fast, works offline, and can be installed like a native app — all without a backend, a build system beyond Jekyll, or a monthly hosting bill.
Check out the live site at shivering.eu and try installing it from your browser’s “Add to home screen” option.