diff --git a/src/app.js b/src/app.js index 2eb3c17..4bb9eeb 100644 --- a/src/app.js +++ b/src/app.js @@ -20,6 +20,14 @@ function html(status, markup) { }; } +function text(status, body, contentType) { + return { + status, + headers: { "content-type": contentType || "text/plain; charset=utf-8" }, + body, + }; +} + function parseJSON(rawBody) { if (!rawBody) { return {}; @@ -112,6 +120,30 @@ function buildApp({ config }) { return json(200, { ok: true }); } + if (method === "GET" && path === "/manifest.webmanifest") { + return text( + 200, + JSON.stringify({ + name: "X Article to Audio", + short_name: "XArtAudio", + start_url: "/", + display: "standalone", + background_color: "#1d232a", + theme_color: "#1d232a", + icons: [], + }), + "application/manifest+json; charset=utf-8", + ); + } + + if (method === "GET" && path === "/sw.js") { + return text( + 200, + "self.addEventListener('install', () => self.skipWaiting()); self.addEventListener('activate', () => self.clients.claim());", + "application/javascript; charset=utf-8", + ); + } + if (method === "GET" && path === "/") { return html(200, renderHomePage({ authenticated: Boolean(userId), diff --git a/src/views/pages.js b/src/views/pages.js index 20d31d0..1467d8b 100644 --- a/src/views/pages.js +++ b/src/views/pages.js @@ -16,12 +16,19 @@ function layout({ title, content }) { ${escapeHtml(title)} + +
${content}
+ `; } diff --git a/test/app.test.js b/test/app.test.js index 3b8781c..c60a3cf 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -44,6 +44,21 @@ test("rejects invalid X webhook signature", () => { assert.equal(response.status, 401); }); +test("serves pwa manifest route", () => { + const app = createApp(); + const response = app.handleRequest({ + method: "GET", + path: "/manifest.webmanifest", + headers: {}, + rawBody: "", + }); + + assert.equal(response.status, 200); + assert.match(response.headers["content-type"], /application\/manifest\+json/); + const body = JSON.parse(response.body); + assert.equal(body.short_name, "XArtAudio"); +}); + test("X webhook returns not_article response and no charge", () => { const app = createApp(); postJSON(app, "/api/webhooks/polar", { userId: "u1", credits: 5, eventId: "evt1" }, "polar-secret"); diff --git a/test/views.test.js b/test/views.test.js index a9ac33b..74ab814 100644 --- a/test/views.test.js +++ b/test/views.test.js @@ -8,6 +8,7 @@ test("layout includes daisyui stylesheet and mobile-first wrapper", () => { const html = layout({ title: "t", content: "x" }); assert.match(html, /daisyui@5/); assert.match(html, /max-w-md mx-auto p-4/); + assert.match(html, /manifest.webmanifest/); }); test("home page renders jobs list and wallet credits", () => {