fix(web): remove Google Analytics and point site links at the fork (#35)
CI / security (push) Successful in 1m12s
CI / lint (push) Successful in 22s
CI / ai-review (push) Skipped

Drops the gtag scripts and the upstream measurement ID, rewrites the privacy copy to match, adds a test that no analytics ID appears under web/, and repoints footer links at the fork with one upstream credit link.
This commit was merged in pull request #35.
This commit is contained in:
2026-11-06 14:27:59 +00:00
parent 180cd4d512
commit f0a50fcff6
9 changed files with 90 additions and 59 deletions
@@ -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([]);
});
});
-13
View File
@@ -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({
/>
</head>
<body>
<Script
src="https://www.googletagmanager.com/gtag/js?id=G-7NBBNJNNQ7"
strategy="lazyOnload"
/>
<Script id="google-analytics" strategy="lazyOnload">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-7NBBNJNNQ7');
`}
</Script>
{children}
</body>
</html>
+4 -11
View File
@@ -141,17 +141,10 @@ export default function OpenSourcePage() {
Free & Open Source Voice Dictation
</h1>
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
Voculinux is 100% open source under the AGPL-3.0 license, a hard fork of{" "}
<a
href="https://github.com/VocaHQ/vocalinux"
target="_blank"
rel="noopener noreferrer"
className="font-medium text-primary hover:underline"
>
Vocalinux (upstream, by VocaHQ)
</a>
. 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.
</p>
</section>
+4 -8
View File
@@ -986,8 +986,8 @@ export default function HomePage() {
<p className="section-lede">
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.
</p>
</div>
<div className="family-grid">
@@ -1010,11 +1010,7 @@ export default function HomePage() {
Linux.
</p>
</div>
<a
className="family-card"
href={UPSTREAM_REPO_URL}
rel="noopener noreferrer"
>
<div className="family-card">
<div className="family-card-top">
<span className="family-platform">
<img
@@ -1032,7 +1028,7 @@ export default function HomePage() {
The upstream Linux dictation app this fork is ported from. Fixes
are taken by hand, one at a time.
</p>
</a>
</div>
<a
className="family-card"
href="https://bongbetic.com"
+8 -9
View File
@@ -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() {
</h2>
<div className="space-y-4 text-muted-foreground">
<p>
voculinux.com loads Google Analytics via the googletagmanager gtag
script for measurement ID{" "}
<code className="rounded bg-muted px-1.5 py-0.5">G-7NBBNJNNQ7</code>{" "}
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.
</p>
<p>
The Bongbetic logos in the header and footer are images served from
bongbetic.com. Loading any page therefore makes a request to that
host.
</p>
</div>
</article>