diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png
new file mode 100644
index 0000000..6f5738c
Binary files /dev/null and b/public/apple-touch-icon.png differ
diff --git a/public/favicon-96x96.png b/public/favicon-96x96.png
new file mode 100644
index 0000000..087d336
Binary files /dev/null and b/public/favicon-96x96.png differ
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..8771dcf
Binary files /dev/null and b/public/favicon.ico differ
diff --git a/public/favicon.png b/public/favicon.png
deleted file mode 100644
index 4b82d09..0000000
Binary files a/public/favicon.png and /dev/null differ
diff --git a/public/favicon.svg b/public/favicon.svg
new file mode 100644
index 0000000..79cd69f
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index ada80d0..bdbd0ca 100644
--- a/public/index.html
+++ b/public/index.html
@@ -6,8 +6,13 @@
kovi — Your reading, beautifully understood
-
-
+
+
+
+
+
+
+
diff --git a/public/kovi-logo.png b/public/kovi-logo.png
index f6a99ce..15fe938 100644
Binary files a/public/kovi-logo.png and b/public/kovi-logo.png differ
diff --git a/public/kovi-mark.png b/public/kovi-mark.png
index 6f19ed8..4847b6c 100644
Binary files a/public/kovi-mark.png and b/public/kovi-mark.png differ
diff --git a/public/site.webmanifest b/public/site.webmanifest
new file mode 100644
index 0000000..8f211ef
--- /dev/null
+++ b/public/site.webmanifest
@@ -0,0 +1,23 @@
+{
+ "name": "kovi",
+ "short_name": "kovi",
+ "description": "Your reading, beautifully understood.",
+ "start_url": "/",
+ "icons": [
+ {
+ "src": "/web-app-manifest-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "any maskable"
+ },
+ {
+ "src": "/web-app-manifest-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "any maskable"
+ }
+ ],
+ "theme_color": "#f6f3ec",
+ "background_color": "#f6f3ec",
+ "display": "standalone"
+}
diff --git a/public/web-app-manifest-192x192.png b/public/web-app-manifest-192x192.png
new file mode 100644
index 0000000..0d15643
Binary files /dev/null and b/public/web-app-manifest-192x192.png differ
diff --git a/public/web-app-manifest-512x512.png b/public/web-app-manifest-512x512.png
new file mode 100644
index 0000000..fc4552a
Binary files /dev/null and b/public/web-app-manifest-512x512.png differ
diff --git a/server.mjs b/server.mjs
index bd8931a..f0199bf 100644
--- a/server.mjs
+++ b/server.mjs
@@ -307,7 +307,7 @@ async function handleApi(req,res,url) {
return false;
}
-const MIME={'.html':'text/html; charset=utf-8','.css':'text/css; charset=utf-8','.js':'text/javascript; charset=utf-8','.svg':'image/svg+xml','.png':'image/png','.jpg':'image/jpeg','.jpeg':'image/jpeg','.webp':'image/webp','.gif':'image/gif','.ico':'image/x-icon','.zip':'application/zip'};
+const MIME={'.html':'text/html; charset=utf-8','.css':'text/css; charset=utf-8','.js':'text/javascript; charset=utf-8','.svg':'image/svg+xml','.png':'image/png','.jpg':'image/jpeg','.jpeg':'image/jpeg','.webp':'image/webp','.gif':'image/gif','.ico':'image/x-icon','.webmanifest':'application/manifest+json','.zip':'application/zip'};
async function serveFile(res,filePath,{cache=false}={}) {
try {
const st=await fsp.stat(filePath); if(!st.isFile()) return false;
diff --git a/tests/frontend.test.mjs b/tests/frontend.test.mjs
index 045011c..2f33cb1 100644
--- a/tests/frontend.test.mjs
+++ b/tests/frontend.test.mjs
@@ -5,6 +5,7 @@ import fs from 'node:fs';
const source=fs.readFileSync(new URL('../public/app.js', import.meta.url),'utf8');
const html=fs.readFileSync(new URL('../public/index.html', import.meta.url),'utf8');
const css=fs.readFileSync(new URL('../public/styles.css', import.meta.url),'utf8');
+const server=fs.readFileSync(new URL('../server.mjs', import.meta.url),'utf8');
test('manual SQLite upload attaches the browser CSRF token',()=>{
assert.match(source,/setRequestHeader\('X-Kovi-CSRF',csrf\)/);
@@ -159,6 +160,23 @@ test('sidebar and empty state use the kovi logo assets instead of split legacy b
assert.match(html,/id="themeColor"/);assert.match(source,/themeColor/);
});
-test('the favicon uses the kovi mascot instead of the old svg mark',()=>{
- assert.match(html,/rel="icon" href="\/favicon\.png"/);assert.doesNotMatch(html,/logo\.svg/);
+test('the favicon set uses the kovi mascot assets instead of the old svg mark',()=>{
+ assert.match(html,/rel="icon" type="image\/png" sizes="96x96" href="\/favicon-96x96\.png"/);
+ assert.match(html,/rel="icon" type="image\/svg\+xml" href="\/favicon\.svg"/);
+ assert.match(html,/rel="shortcut icon" href="\/favicon\.ico"/);
+ assert.match(html,/rel="apple-touch-icon" sizes="180x180" href="\/apple-touch-icon\.png"/);
+ assert.match(html,/rel="manifest" href="\/site\.webmanifest"/);
+ assert.doesNotMatch(html,/favicon\.png/);assert.doesNotMatch(html,/logo\.svg/);
+});
+
+test('the web app manifest brands kovi and points at the installed icons',()=>{
+ const manifest=JSON.parse(fs.readFileSync(new URL('../public/site.webmanifest', import.meta.url),'utf8'));
+ assert.equal(manifest.name,'kovi');assert.equal(manifest.short_name,'kovi');
+ assert.deepEqual(manifest.icons.map(icon=>icon.src),['/web-app-manifest-192x192.png','/web-app-manifest-512x512.png']);
+ for(const icon of manifest.icons){assert.equal(icon.type,'image/png');assert.match(icon.sizes,/^\d+x\d+$/);}
+ assert.match(manifest.theme_color,/^#[0-9a-f]{6}$/i);
+});
+
+test('the static server serves the web app manifest with its manifest MIME type',()=>{
+ assert.match(server,/\.webmanifest':'application\/manifest\+json'/);
});