From 8e8a451ba2db9836017af7de668223ecf88688ae Mon Sep 17 00:00:00 2001 From: Xavier Karma Date: Fri, 6 Nov 2026 19:54:30 +0530 Subject: [PATCH 1/2] fix(web): remove Google Analytics from the site The root layout loaded the gtag script for measurement ID G-7NBBNJNNQ7, and the privacy page described it. Remove both. The privacy page now says the site runs no analytics or tracking scripts, sets no cookies, and loads its header and footer logos from bongbetic.com. A jest test scans web/src, web/public and the next.config files for googletagmanager, google-analytics, gtag( and G- measurement IDs, so the tag cannot return unnoticed. Refs #16 --- web/src/app/__tests__/no-analytics.test.ts | 51 ++++++++++++++++++++++ web/src/app/layout.tsx | 13 ------ web/src/app/privacy/page.tsx | 17 ++++---- 3 files changed, 59 insertions(+), 22 deletions(-) create mode 100644 web/src/app/__tests__/no-analytics.test.ts 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/privacy/page.tsx b/web/src/app/privacy/page.tsx index 6083a49..9902a92 100644 --- a/web/src/app/privacy/page.tsx +++ b/web/src/app/privacy/page.tsx @@ -24,7 +24,7 @@ export default function PrivacyPage() { headline: "Voculinux Privacy Policy", description: "How Voculinux handles voice data: local engines on your machine, optional Remote API to a server you configure.", - dateModified: "2026-09-23", + dateModified: "2026-11-06", author: { "@type": "Person", name: "Jatin K Malik", @@ -158,14 +158,13 @@ export default function PrivacyPage() {

- 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.

-- 2.54.0 From 6afb9218e0b8c2e197b85616c9babb432785a200 Mon Sep 17 00:00:00 2001 From: Xavier Karma Date: Fri, 6 Nov 2026 19:54:30 +0530 Subject: [PATCH 2/2] fix(web): point footer and site links at the fork The footer's X, Discord and mailbox links went to VocaHQ channels the fork does not have, so they are removed and the footer keeps the fork source link. The footer's personal credit to Jatin K Malik becomes the one upstream credit, "Vocalinux (upstream, by VocaHQ)". The about copy and the home provenance card no longer link to upstream. The Twitter handle, the JSON-LD author fields and the functional upstream install and release links are unchanged. Refs #16 --- web/src/app/open-source/page.tsx | 15 ++++----------- web/src/app/page.tsx | 12 ++++-------- web/src/components/site-chrome.tsx | 9 +++++---- web/src/lib/__tests__/voculinux-rebrand.test.ts | 17 +++++++++++++++-- web/src/lib/seo.ts | 3 --- web/src/lib/site-nav.ts | 12 +++--------- 6 files changed, 31 insertions(+), 37 deletions(-) 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.

- +
- +
- 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[] }[] = [ -- 2.54.0