diff --git a/docs/roadmap.md b/docs/roadmap.md index 2050dde..cb48c68 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -315,12 +315,13 @@ should follow the board's trust model. disposable keys, signer apps). Linked from the editor and the board. Aimed at the activist/whistleblower audience the aesthetic invites — being honest about limits matters more than any feature. -- **Link previews / Open Graph — partial, by design.** Static site-wide OG + - Twitter-card meta are on the home, board, and safety pages, so a shared - `voxvera.org` link renders a real title/description instead of nothing. - **Per-flyer previews are a deliberate non-goal:** the poster `naddr` lives in - the URL `#fragment` (invisible to servers), and a static host can't render - per-flyer `` without server-side code — which is out of scope by choice +- **Link previews / Open Graph — shipped (site-wide).** Static OG + + Twitter-card meta are on the home, board, and safety pages, including a + **1200×630 preview image** (`site/og-card.jpg`, `summary_large_image`), so a + shared `voxvera.org` link renders a real title, description, and image card. + **Per-flyer previews remain a deliberate non-goal:** the poster `naddr` lives + in the URL `#fragment` (invisible to servers), and a static host can't render + per-flyer ``/image without server-side code — out of scope by choice (this stays a free, purely static site). Revisit only if that constraint ever changes. - **Publish transparency — ✅ done.** The publish confirmation modal now reports diff --git a/e2e/flyer.spec.js b/e2e/flyer.spec.js index 0aa6740..b181705 100644 --- a/e2e/flyer.spec.js +++ b/e2e/flyer.spec.js @@ -121,6 +121,20 @@ test.describe("VoxVera static client", () => { expect(await res.text()).toContain(" { + for (const path of ["/", "/board.html", "/safety.html"]) { + await page.goto(path); + const ogImage = await page.locator('meta[property="og:image"]').getAttribute("content"); + expect(ogImage, `${path} og:image`).toBe("https://voxvera.org/og-card.jpg"); + const twCard = await page.locator('meta[name="twitter:card"]').getAttribute("content"); + expect(twCard, `${path} twitter:card`).toBe("summary_large_image"); + } + // The card asset itself is served and is a JPEG. + const res = await page.request.get("/og-card.jpg"); + expect(res.status()).toBe(200); + expect(res.headers()["content-type"] || "").toContain("jpeg"); + }); + test("print media hides app chrome and keeps the flyer", async ({ page }, testInfo) => { await page.goto("/"); await page.emulateMedia({ media: "print" }); diff --git a/site/board.html b/site/board.html index 0f928af..bb6efde 100644 --- a/site/board.html +++ b/site/board.html @@ -15,9 +15,14 @@ - + + + + + +
diff --git a/site/index.html b/site/index.html index 9c3c3e7..c95e45b 100644 --- a/site/index.html +++ b/site/index.html @@ -12,9 +12,14 @@ - + + + + + +
diff --git a/site/og-card.jpg b/site/og-card.jpg new file mode 100644 index 0000000..c6dd6b8 Binary files /dev/null and b/site/og-card.jpg differ diff --git a/site/safety.html b/site/safety.html index cf78376..cea9e31 100644 --- a/site/safety.html +++ b/site/safety.html @@ -14,9 +14,14 @@ - + + + + + +