Add PWA manifest, service worker, and install prompt
- manifest.webmanifest: standalone display mode, theme #2563eb, start_url=/app.html (so users who install land in the app shell, not the marketing page), scope=/. Three icons: 192px any-purpose, 512px any-purpose, 512px maskable for adaptive icons on Android. - icons/: PNGs generated from favicon.svg. - sw.js: cache-first for the precached app shell, network-first for /api/* and /auth/* (we never serve stale auth or notes). Versioned cache name (librenotes-shell-v1) so a SW update evicts old assets. skipWaiting + clients.claim so a new SW takes over without a manual reload. - pwa.js: registers the SW on every page and handles beforeinstallprompt by showing #install-btn. Hides the button again on appinstalled. Defer loaded so it never blocks render. - All HTML pages link the manifest, set the theme-color meta, and load pwa.js. Landing page exposes the install button next to the existing CTAs. Closes #19. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Notes — librenotes</title>
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<meta name="theme-color" content="#2563eb">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body class="app-page">
|
||||
@@ -24,5 +26,6 @@
|
||||
|
||||
<script src="/auth-client.js"></script>
|
||||
<script src="/app.js"></script>
|
||||
<script src="/pwa.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -10,6 +10,8 @@
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://librenot.es">
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<meta name="theme-color" content="#2563eb">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
@@ -34,6 +36,7 @@
|
||||
<p class="ctas">
|
||||
<a class="btn primary" href="/login.html">Sign in with email</a>
|
||||
<a class="btn ghost" href="https://git.librete.ch/public/librenotes">Self-host it</a>
|
||||
<button class="btn ghost" id="install-btn" type="button" hidden>Install app</button>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -69,5 +72,6 @@
|
||||
<p class="muted">MIT licensed. Built on <a href="https://github.com/alonswartz/notesium">Notesium</a>.</p>
|
||||
</div>
|
||||
</footer>
|
||||
<script src="/pwa.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Sign in — librenotes</title>
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<meta name="theme-color" content="#2563eb">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body class="auth-page">
|
||||
@@ -34,5 +36,6 @@
|
||||
<p class="footer-link"><a href="/">Back to home</a></p>
|
||||
</main>
|
||||
<script src="/login.js"></script>
|
||||
<script src="/pwa.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "librenotes",
|
||||
"short_name": "librenotes",
|
||||
"description": "Open-source multi-tenant notes with bi-directional links.",
|
||||
"start_url": "/app.html",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"orientation": "any",
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "#2563eb",
|
||||
"categories": ["productivity", "utilities"],
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-maskable.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// pwa.js — service worker registration and install prompt.
|
||||
//
|
||||
// Loaded on every page. Registers the SW from the root scope so
|
||||
// it controls the entire site, and surfaces an install button on
|
||||
// browsers that fire beforeinstallprompt (Chrome / Edge / Android).
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", function () {
|
||||
navigator.serviceWorker.register("/sw.js", { scope: "/" })
|
||||
.catch(function (err) {
|
||||
console.warn("librenotes: service worker registration failed", err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let deferredPrompt = null;
|
||||
|
||||
window.addEventListener("beforeinstallprompt", function (ev) {
|
||||
ev.preventDefault();
|
||||
deferredPrompt = ev;
|
||||
const btn = document.getElementById("install-btn");
|
||||
if (btn) {
|
||||
btn.hidden = false;
|
||||
btn.addEventListener("click", function () {
|
||||
if (!deferredPrompt) return;
|
||||
deferredPrompt.prompt();
|
||||
deferredPrompt.userChoice.finally(function () {
|
||||
deferredPrompt = null;
|
||||
btn.hidden = true;
|
||||
});
|
||||
}, { once: true });
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("appinstalled", function () {
|
||||
const btn = document.getElementById("install-btn");
|
||||
if (btn) btn.hidden = true;
|
||||
deferredPrompt = null;
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,80 @@
|
||||
// sw.js — librenotes service worker.
|
||||
//
|
||||
// Strategy:
|
||||
// - App shell (HTML/CSS/JS in PRECACHE): cache-first.
|
||||
// This is what makes the page render offline.
|
||||
// - API calls (/api/*, /auth/*): network-first.
|
||||
// We never serve stale auth or stale notes from cache. If the
|
||||
// network is down, the request fails and the app handles it.
|
||||
// - Other static assets (icons, manifest): cache-first with
|
||||
// versioned cache name so a deploy of a new SW evicts old.
|
||||
|
||||
const VERSION = "v1";
|
||||
const CACHE = "librenotes-shell-" + VERSION;
|
||||
|
||||
const PRECACHE = [
|
||||
"/",
|
||||
"/index.html",
|
||||
"/login.html",
|
||||
"/verify.html",
|
||||
"/app.html",
|
||||
"/style.css",
|
||||
"/auth-client.js",
|
||||
"/login.js",
|
||||
"/verify.js",
|
||||
"/app.js",
|
||||
"/manifest.webmanifest",
|
||||
"/favicon.svg",
|
||||
];
|
||||
|
||||
self.addEventListener("install", function (ev) {
|
||||
ev.waitUntil(
|
||||
caches.open(CACHE).then(function (cache) {
|
||||
return cache.addAll(PRECACHE);
|
||||
})
|
||||
);
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener("activate", function (ev) {
|
||||
ev.waitUntil(
|
||||
caches.keys().then(function (keys) {
|
||||
return Promise.all(
|
||||
keys.filter(function (k) { return k !== CACHE; })
|
||||
.map(function (k) { return caches.delete(k); })
|
||||
);
|
||||
}).then(function () { return self.clients.claim(); })
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", function (ev) {
|
||||
const req = ev.request;
|
||||
if (req.method !== "GET") return;
|
||||
|
||||
const url = new URL(req.url);
|
||||
if (url.origin !== self.location.origin) return;
|
||||
|
||||
// Network-first for API and auth endpoints.
|
||||
if (url.pathname.startsWith("/api/") || url.pathname.startsWith("/auth/")) {
|
||||
ev.respondWith(
|
||||
fetch(req).catch(function () {
|
||||
return new Response("offline", { status: 503, statusText: "offline" });
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Cache-first for everything else (the app shell).
|
||||
ev.respondWith(
|
||||
caches.match(req).then(function (cached) {
|
||||
if (cached) return cached;
|
||||
return fetch(req).then(function (resp) {
|
||||
if (resp && resp.status === 200 && resp.type === "basic") {
|
||||
const copy = resp.clone();
|
||||
caches.open(CACHE).then(function (cache) { cache.put(req, copy); });
|
||||
}
|
||||
return resp;
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -5,6 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Signing in — librenotes</title>
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<meta name="theme-color" content="#2563eb">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body class="auth-page">
|
||||
@@ -24,5 +26,6 @@
|
||||
</main>
|
||||
<script src="/auth-client.js"></script>
|
||||
<script src="/verify.js"></script>
|
||||
<script src="/pwa.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user