feat: add a Keep it free sponsor button to the top nav (#427)

Adds a prominent Keep it free sponsor button to the top nav, linking to https://github.com/sponsors/snapotter-hq. Solid orange pill on desktop (left of the avatar), orange heart icon on mobile. Opens in a new tab with rel=noopener noreferrer, so no referrer or user data leaks, and it adds no passive network activity (offline-mode compatible). Fires an opt-in, property-less sponsor_clicked analytics event. Adds sidebar.sponsor and a11y.sponsorLink across all 21 locales.

Claude-Session: https://claude.ai/code/session_01DnYLLA5z4Uf1GDeEPENVgr
This commit is contained in:
SnapOtter
2026-07-04 08:48:16 +00:00
committed by GitHub
parent 3ae48cc76a
commit 7b04317ed2
25 changed files with 115 additions and 2 deletions
+19
View File
@@ -0,0 +1,19 @@
import { expect, test } from "./helpers";
const SPONSOR_URL = "https://github.com/sponsors/snapotter-hq";
test.describe("Sponsor button", () => {
// Pin a desktop viewport so the full pill (with visible text) renders,
// not the mobile icon-only form.
test.use({ viewport: { width: 1280, height: 720 } });
test("top nav links to GitHub Sponsors with safe rel", async ({ page }) => {
await page.goto("/");
const link = page.locator(`a[href="${SPONSOR_URL}"]`);
await expect(link).toBeVisible();
await expect(link).toHaveAttribute("target", "_blank");
await expect(link).toHaveAttribute("rel", "noopener noreferrer");
await expect(link).toContainText(/keep it free/i);
});
});
+3 -2
View File
@@ -2,8 +2,8 @@ import { ANALYTICS_EVENTS } from "@snapotter/shared";
import { describe, expect, it } from "vitest";
describe("ANALYTICS_EVENTS", () => {
it("has exactly 13 event keys", () => {
expect(Object.keys(ANALYTICS_EVENTS)).toHaveLength(13);
it("has exactly 14 event keys", () => {
expect(Object.keys(ANALYTICS_EVENTS)).toHaveLength(14);
});
it("contains the expected keys", () => {
@@ -20,6 +20,7 @@ describe("ANALYTICS_EVENTS", () => {
expect(ANALYTICS_EVENTS).toHaveProperty("AI_BUNDLE_PROMPTED");
expect(ANALYTICS_EVENTS).toHaveProperty("BATCH_PROCESSED");
expect(ANALYTICS_EVENTS).toHaveProperty("FEEDBACK_SUBMITTED");
expect(ANALYTICS_EVENTS).toHaveProperty("SPONSOR_CLICKED");
});
it("all event values are strings", () => {