diff --git a/site/assets/account.html b/site/assets/account.html
index e722408..d738294 100644
--- a/site/assets/account.html
+++ b/site/assets/account.html
@@ -8,6 +8,7 @@
Your account — readm3
+
diff --git a/site/assets/admin.html b/site/assets/admin.html
index a2364f8..7e87810 100644
--- a/site/assets/admin.html
+++ b/site/assets/admin.html
@@ -3,7 +3,7 @@
Your Markdown workspace — readm3
-
+
diff --git a/site/build.ts b/site/build.ts
index bf9d765..a5fefb5 100644
--- a/site/build.ts
+++ b/site/build.ts
@@ -182,6 +182,8 @@ function shell(options: { title: string; description: string; path: string; main
+
+
@@ -725,6 +727,14 @@ write("viewer.webmanifest", JSON.stringify({
launch_type: "single-client",
}],
}));
+// The site-wide manifest: every page but the editor links it, so readm3.com installs from its homepage.
+write("manifest.webmanifest", JSON.stringify({
+ id: "/", name: "readm3", short_name: "readm3",
+ description: "Markdown editing and sharing for people and teams.",
+ start_url: "/", scope: "/", display: "standalone",
+ background_color: "#05070a", theme_color: "#05070a", lang: "en",
+ icons: [192, 512].flatMap((size) => ["any", "maskable"].map((purpose) => ({ src: `/viewer-assets/icon-${size}.png`, sizes: `${size}x${size}`, type: "image/png", purpose }))),
+}));
const precache = ["/viewer", script, ...chunks, viewerCss, sharedCss, adminCss, seed, "/viewer.webmanifest", "/favicon.svg", "/viewer-assets/icon-192.png", "/viewer-assets/icon-512.png"];
const cacheVersion = createHash("sha256").update(viewerHtml + precache.join("|")).digest("hex").slice(0, 16);
write("viewer-sw.js", readFileSync(join(here, "assets/viewer-sw.js"), "utf8")
diff --git a/site/test/viewer.spec.ts b/site/test/viewer.spec.ts
index 20ce26c..24856cf 100644
--- a/site/test/viewer.spec.ts
+++ b/site/test/viewer.spec.ts
@@ -610,6 +610,20 @@ test("still renders a Markdown paste as a document", async ({ page, request }) =
await expect(page.locator("#raw-link")).toBeVisible();
});
+test("the homepage links an installable site manifest", async ({ request }) => {
+ expect(await (await request.get("/")).text()).toContain('');
+ const response = await request.get("/manifest.webmanifest");
+ expect(response.headers()["content-type"]).toContain("manifest+json");
+ const manifest = await response.json();
+ expect(manifest).toMatchObject({ name: "readm3", start_url: "/", scope: "/", display: "standalone" });
+ for (const size of ["192x192", "512x512"]) expect(manifest.icons.some((icon: { sizes: string }) => icon.sizes === size)).toBe(true);
+ for (const icon of manifest.icons) {
+ const image = await request.get(icon.src);
+ expect(image.ok()).toBe(true);
+ expect(image.headers()["content-type"]).toBe("image/png");
+ }
+});
+
test("installs a complete offline shell and restores edited documents after an offline reload", async ({ page, context, request }) => {
const manifestResponse = await request.get("/viewer.webmanifest");
expect(manifestResponse.headers()["content-type"]).toContain("manifest+json");