Skip to content

Instantly share code, notes, and snippets.

@joseph0x45
Last active April 6, 2026 12:25
Show Gist options
  • Select an option

  • Save joseph0x45/b843fe8553f12dfe21ad81f0debe15f0 to your computer and use it in GitHub Desktop.

Select an option

Save joseph0x45/b843fe8553f12dfe21ad81f0debe15f0 to your computer and use it in GitHub Desktop.
Setup PWA support for Go SSRd apps
package main
import (
"bytes"
"embed"
"html/template"
"github.com/go-chi/chi/v5"
"github.com/joseph0x45/goutils"
)
//go:embed static
var staticFS embed.FS
func serveAppJS(r chi.Router) {
var appJS = template.Must(template.New("appJS").Delims("[[", "]]").Parse(`
import { kv } from '/idb.js';
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js', { scope: '/' })
.catch(err => console.error('SW registration failed:', err));
}
let deferredPrompt = null;
window.addEventListener('beforeinstallprompt', event => {
event.preventDefault();
deferredPrompt = event;
document.getElementById('install-btn')?.removeAttribute('hidden');
});
window.addEventListener('appinstalled', () => {
deferredPrompt = null;
document.getElementById('install-btn')?.setAttribute('hidden', '');
});
document.getElementById('install-btn')?.addEventListener('click', () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
deferredPrompt.userChoice.then(() => { deferredPrompt = null; });
});
`))
var buf bytes.Buffer
err := appJS.Execute(&buf, nil)
if err != nil {
panic(err)
}
goutils.ServeJS(r, "/app.js", buf.String())
}
func serveManifest(r chi.Router, appName string) {
var manifestJSON = template.Must(template.New("manifest").Delims("[[", "]]").Parse(`
{
"name": "[[.AppName]]",
"short_name": "[[.AppName]]",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000",
"icons": [
{ "src": "/static/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/static/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" },
{ "src": "/static/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
]
}
`))
var buf bytes.Buffer
err := manifestJSON.Execute(&buf, map[string]string{
"AppName": appName,
})
if err != nil {
panic(err)
}
goutils.ServeJS(r, "/manifest.json", buf.String())
}
func setupIDBWrapper(r chi.Router, appName string) {
var idbWrapper = template.Must(template.New("idb").Delims("[[", "]]").Parse(`
const DB_NAME = '[[.AppName]]-db';
const DB_VERSION = 1;
const STORE_NAME = 'kv';
function openDB() {
return new Promise((resolve, reject) => {
const req = indexedDB.open(DB_NAME, DB_VERSION);
req.onupgradeneeded = e => {
e.target.result.createObjectStore(STORE_NAME);
};
req.onsuccess = e => resolve(e.target.result);
req.onerror = e => reject(e.target.error);
});
}
function tx(mode, fn) {
return openDB().then(db => new Promise((resolve, reject) => {
const store = db.transaction(STORE_NAME, mode).objectStore(STORE_NAME);
const req = fn(store);
req.onsuccess = e => resolve(e.target.result);
req.onerror = e => reject(e.target.error);
}));
}
export const kv = {
get: key => tx('readonly', s => s.get(key)),
set: (key, value) => tx('readwrite', s => s.put(value, key)),
delete: key => tx('readwrite', s => s.delete(key)),
clear: () => tx('readwrite', s => s.clear()),
keys: () => tx('readonly', s => s.getAllKeys()),
};
`))
var buf bytes.Buffer
err := idbWrapper.Execute(&buf, map[string]string{
"AppName": appName,
})
if err != nil {
panic(err)
}
goutils.ServeJS(r, "/idb.js", buf.String())
}
func setupServiceWorker(r chi.Router, appName string, appVersion string) {
var swTemplate = template.Must(template.New("sw").Delims("[[", "]]").Parse(`
const CACHE_NAME = '[[.AppName]]-[[.AppVersion]]';
const PRECACHE_ASSETS = [
'/',
'/static/app.js',
'/static/idb.js',
'/static/manifest.json',
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(PRECACHE_ASSETS))
);
self.skipWaiting();
});
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)))
)
);
self.clients.claim();
});
self.addEventListener('fetch', event => {
if (event.request.url.includes('/api/')) return; // let API calls through always
event.respondWith(
caches.match(event.request).then(cached => {
return cached ?? fetch(event.request);
})
);
});
`))
var buf bytes.Buffer
err := swTemplate.Execute(&buf, map[string]string{
"AppName": appName,
"AppVersion": appVersion,
})
if err != nil {
panic(err)
}
goutils.ServeJS(r, "/sw.js", buf.String())
setupIDBWrapper(r, appName)
serveManifest(r, appName)
serveAppJS(r)
goutils.ServeFS(r, "/static", staticFS)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment