diff --git a/web/src/app/__tests__/no-analytics.test.ts b/web/src/app/__tests__/no-analytics.test.ts new file mode 100644 index 0000000..ba89312 --- /dev/null +++ b/web/src/app/__tests__/no-analytics.test.ts @@ -0,0 +1,51 @@ +import fs from 'fs'; +import path from 'path'; + +// Ticket #16: the site loads no analytics. Everything the static export is +// built from (web/src, web/public and the next.config files) is scanned byte +// for byte, so a measurement tag cannot return through a component, an asset +// or a config file. Binary assets are read as latin1 so every byte is covered. + +const WEB_ROOT = process.cwd(); +const THIS_FILE = path.join(WEB_ROOT, 'src', 'app', '__tests__', 'no-analytics.test.ts'); +const TREE_ROOTS = [path.join(WEB_ROOT, 'src'), path.join(WEB_ROOT, 'public')]; + +const MARKERS: [string, RegExp][] = [ + ['googletagmanager', /googletagmanager/], + ['google-analytics', /google-analytics/], + ['gtag(', /gtag\(/], + ['G- measurement ID', /G-[A-Z0-9]{8,}/], +]; + +function scanTargets(): string[] { + const targets: string[] = []; + for (const root of TREE_ROOTS) { + for (const rel of fs.readdirSync(root, { encoding: 'utf8', recursive: true })) { + const full = path.join(root, rel); + if (fs.statSync(full).isFile()) targets.push(full); + } + } + for (const name of fs.readdirSync(WEB_ROOT)) { + if (/^next\.config\./.test(name)) targets.push(path.join(WEB_ROOT, name)); + } + return targets.filter((file) => file !== THIS_FILE); +} + +const sources = scanTargets().map((file) => ({ + file: path.relative(WEB_ROOT, file), + text: fs.readFileSync(file, 'latin1'), +})); + +describe('no analytics on the site (ticket #16)', () => { + it('scans the source, public assets and next.config files', () => { + const names = sources.map((source) => source.file); + expect(names).toEqual( + expect.arrayContaining(['src/app/layout.tsx', 'next.config.js', 'public/_headers']), + ); + }); + + it.each(MARKERS)('contains no %s marker', (_label, marker) => { + const offenders = sources.filter((source) => marker.test(source.text)).map((source) => source.file); + expect(offenders).toEqual([]); + }); +}); diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index cc4a807..0d69637 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -1,7 +1,6 @@ import "@/styles/globals.css"; import React from "react"; import { type Metadata } from "next"; -import Script from "next/script"; import { absoluteUrl, DEFAULT_OG_IMAGE_ALT, @@ -173,18 +172,6 @@ export default function RootLayout({ /> - {children} diff --git a/web/src/app/open-source/page.tsx b/web/src/app/open-source/page.tsx index 40fa856..18db190 100644 --- a/web/src/app/open-source/page.tsx +++ b/web/src/app/open-source/page.tsx @@ -141,17 +141,10 @@ export default function OpenSourcePage() { Free & Open Source Voice Dictation

- Voculinux is 100% open source under the AGPL-3.0 license, a hard fork of{" "} - - Vocalinux (upstream, by VocaHQ) - - . No subscriptions, no vendor lock-in, no hidden costs. Local engines keep - audio on the machine. Remote API is optional. + Voculinux is 100% open source under the AGPL-3.0 license, a hard fork of + Vocalinux (upstream, by VocaHQ). No subscriptions, no vendor lock-in, no + hidden costs. Local engines keep audio on the machine. Remote API is + optional.

diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index b3ee3c6..6c52bb1 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -986,8 +986,8 @@ export default function HomePage() {

Voculinux is a hard fork of Vocalinux for Void Linux (glibc, Wayland-primary under niri). The internal Python module keeps the - upstream name so fixes stay cheap to port by hand; upstream links - below are retained as provenance. + upstream name so fixes stay cheap to port by hand. Upstream is + credited in the footer.

@@ -1010,11 +1010,7 @@ export default function HomePage() { Linux.

- +
- +

- voculinux.com loads Google Analytics via the googletagmanager gtag - script for measurement ID{" "} - G-7NBBNJNNQ7{" "} - and runs gtag config with no IP anonymization and no cookie flags. - That sends page views and a Google Analytics client identifier (a - cookie) to Google. Google processes IP addresses and device and - browser data. The installed Voculinux app does not send usage - telemetry. + voculinux.com has no analytics, measurement or tracking scripts, + and its code sets no cookies. +

+

+ The Bongbetic logos in the header and footer are images served from + bongbetic.com. Loading any page therefore makes a request to that + host.

diff --git a/web/src/components/site-chrome.tsx b/web/src/components/site-chrome.tsx index 0e4ba5b..b51f91d 100644 --- a/web/src/components/site-chrome.tsx +++ b/web/src/components/site-chrome.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useId, useState } from "react"; import Link from "next/link"; import { Code } from "lucide-react"; import { VoculinuxLogo } from "@/components/optimized-image"; -import { SOURCE_REPO_URL } from "@/lib/seo"; +import { SOURCE_REPO_URL, UPSTREAM_REPO_URL } from "@/lib/seo"; import { footerGroups, homeNav, @@ -250,12 +250,13 @@ export function SiteChrome({
- AGPL-3.0. Built in public by{" "} + AGPL-3.0. Based on{" "} - Jatin K Malik + Vocalinux (upstream, by VocaHQ) . diff --git a/web/src/lib/__tests__/voculinux-rebrand.test.ts b/web/src/lib/__tests__/voculinux-rebrand.test.ts index bcb7b05..ba25d07 100644 --- a/web/src/lib/__tests__/voculinux-rebrand.test.ts +++ b/web/src/lib/__tests__/voculinux-rebrand.test.ts @@ -134,12 +134,25 @@ describe("fork source and star count (spec16 review)", () => { expect(chrome).toContain("SOURCE_REPO_URL"); }); - it("uses the upstream constant only on labelled upstream surfaces", () => { + it("uses the upstream constant only for the one labelled footer credit", () => { const chrome = read("src", "components", "site-chrome.tsx"); const nav = read("src", "lib", "site-nav.ts"); - expect(chrome).not.toContain("UPSTREAM_REPO_URL"); + expect(chrome.match(/href=\{UPSTREAM_REPO_URL\}/g)).toHaveLength(1); + expect(chrome).toContain("Vocalinux (upstream, by VocaHQ)"); expect(nav).not.toContain("UPSTREAM_REPO_URL"); }); + + it("keeps VocaHQ channels out of the footer social and nav links", () => { + const nav = read("src", "lib", "site-nav.ts"); + expect(nav).not.toMatch(/x\.com|discord\.gg|mailto:|vocahq\.com/); + }); + + it("keeps a single upstream credit link on the about copy and home", () => { + const openSource = readPage("open-source", "page.tsx"); + const home = readPage("page.tsx"); + expect(openSource).not.toContain("github.com/VocaHQ/vocalinux"); + expect(home).not.toContain("href={UPSTREAM_REPO_URL}"); + }); }); describe("AUR and privacy honesty (spec16 review)", () => { diff --git a/web/src/lib/seo.ts b/web/src/lib/seo.ts index cfceb05..d692c13 100644 --- a/web/src/lib/seo.ts +++ b/web/src/lib/seo.ts @@ -14,9 +14,6 @@ export const SOURCE_REPO_URL = "https://git.bongbetic.com/xavierk/voculinux"; // upstream install script, the AppImage and packages published there, and the // upstream release history behind the changelog). export const UPSTREAM_REPO_URL = "https://github.com/VocaHQ/vocalinux"; -export const X_URL = "https://x.com/vocahq"; -export const DISCORD_URL = "https://discord.gg/t6muquAJbm"; -export const EMAIL_URL = "mailto:hello@vocahq.com"; interface PageMetadataOptions { title: string; diff --git a/web/src/lib/site-nav.ts b/web/src/lib/site-nav.ts index 197d65e..451c296 100644 --- a/web/src/lib/site-nav.ts +++ b/web/src/lib/site-nav.ts @@ -1,9 +1,4 @@ -import { - DISCORD_URL, - EMAIL_URL, - SOURCE_REPO_URL, - X_URL, -} from "@/lib/seo"; +import { SOURCE_REPO_URL } from "@/lib/seo"; export type NavLink = { href: string; @@ -32,15 +27,14 @@ export const mobileExtraNav: NavLink[] = [ { href: "/troubleshooting/", label: "Troubleshooting" }, ]; +// The fork's own channels only. The VocaHQ X account, Discord and mailbox +// belong to upstream, and the fork has none of them, so none are listed. export const socialLinks: { href: string; label: string; icon: string }[] = [ - { href: X_URL, label: "X", icon: "/brand/social/x.svg" }, - { href: DISCORD_URL, label: "Discord", icon: "/brand/social/discord.svg" }, { href: SOURCE_REPO_URL, label: "Voculinux source repository", icon: "/brand/social/github.svg", }, - { href: EMAIL_URL, label: "Email hello@vocahq.com", icon: "/brand/social/mail.svg" }, ]; export const footerGroups: { title: string; links: NavLink[] }[] = [