fix(web): remove Google Analytics and repoint footer links to the fork #35
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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({
|
||||
</div>
|
||||
<div className="footer-base">
|
||||
<span>
|
||||
AGPL-3.0. Built in public by{" "}
|
||||
AGPL-3.0. Based on{" "}
|
||||
<a
|
||||
href="https://github.com/jatinkrmalik"
|
||||
href={UPSTREAM_REPO_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Jatin K Malik
|
||||
Vocalinux (upstream, by VocaHQ)
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
|
||||
@@ -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)", () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[] }[] = [
|
||||
|
||||
Reference in New Issue
Block a user