From 98806018cedda8f27fdb887b6676e02ce6d6b2ba Mon Sep 17 00:00:00 2001 From: Kresna Date: Sun, 2 Aug 2026 11:49:55 +0700 Subject: [PATCH 1/2] feat(i18n+about): localize header shell, translate About + spam-safe contact email MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Header shell (ShellIsland) now detects language from the URL and localizes the '⌘K to search' hint, the Contribute (GitHub) modal, and all tooltips/aria-labels — Bahasa on /id/. - About page moved under [...locale] and fully translated (EN + ID). New 'Contact & tool requests' section: contact@goodwebtools.com, and visitors can request a new tool by email. - Email is spam-safe: assembled from data-attributes in JS (via Base), so the plain address never appears in the static HTML. Human-readable 'contact [at] … [dot] com' fallback for no-JS. - LangSwitcher + worker treat /about as localized (switch stays on the page). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Ubfx4XocHcECaL8twp9zsr --- src/components/shell/LangSwitcher.tsx | 2 +- src/components/shell/ShellIsland.tsx | 73 ++++++++++----- src/layouts/Base.astro | 14 +++ src/pages/[...locale]/about.astro | 123 ++++++++++++++++++++++++++ src/pages/about.astro | 81 ----------------- worker/index.js | 2 +- 6 files changed, 191 insertions(+), 104 deletions(-) create mode 100644 src/pages/[...locale]/about.astro delete mode 100644 src/pages/about.astro diff --git a/src/components/shell/LangSwitcher.tsx b/src/components/shell/LangSwitcher.tsx index 0124c6f..9e22093 100644 --- a/src/components/shell/LangSwitcher.tsx +++ b/src/components/shell/LangSwitcher.tsx @@ -3,7 +3,7 @@ import { LOCALES, LOCALE_LABEL, LOCALE_NAME, localizePath, stripLocale, type Lan // Public sections that exist in every locale. Other paths (about, settings…) fall // back to the locale home when switching, so the switcher never lands on a 404. -const LOCALIZED_PREFIXES = ['/tools/', '/category/']; +const LOCALIZED_PREFIXES = ['/tools/', '/category/', '/about']; /** The URL for the current page in `lang` — computed fresh from the live location. */ function targetFor(lang: Lang): string { diff --git a/src/components/shell/ShellIsland.tsx b/src/components/shell/ShellIsland.tsx index ca51219..94ee37d 100644 --- a/src/components/shell/ShellIsland.tsx +++ b/src/components/shell/ShellIsland.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState, type ReactNode } from 'react'; import { Search, Github, Info, ExternalLink, Settings, MoreVertical } from 'lucide-react'; import { ThemeToggle } from './ThemeToggle'; import { LangSwitcher } from './LangSwitcher'; @@ -7,6 +7,7 @@ import { Modal } from '@/components/ui/Modal'; import { initTheme } from '@/stores/theme.store'; import { isTauri } from '@/services/platform'; import { REPO_URL } from '@/config'; +import type { Lang } from '@/i18n/config'; /** Opens the command palette from a click (works without a keyboard). */ export function openSearch() { @@ -18,18 +19,56 @@ const iconBtn = const menuItem = 'flex w-full items-center gap-2.5 px-3 py-2.5 text-left text-sm font-bold hover:bg-accent hover:text-accent-foreground'; +// Header UI strings, per language (the header persists across navigations, so the +// language is detected from the URL client-side rather than passed as a prop). +const S: Record = { + en: { + searchAria: 'Search tools', searchPre: 'Press', searchPost: 'to search', + about: 'About', contribute: 'Contribute on GitHub', contribMenu: 'Contribute', more: 'More', settings: 'Settings', + modalTitle: 'Contribute', + modalBody: ( + <> +

GoodWebTools is open source. Every tool runs entirely in your browser using modern web APIs and WebAssembly — nothing is uploaded.

+

Got an idea for a tool that could work fully client-side? Or found a bug? Please open an issue, or fork the repo and send a pull request — contributions and requests are very welcome.

+ + ), + }, + id: { + searchAria: 'Cari tool', searchPre: 'Tekan', searchPost: 'untuk mencari', + about: 'Tentang', contribute: 'Kontribusi di GitHub', contribMenu: 'Kontribusi', more: 'Lainnya', settings: 'Pengaturan', + modalTitle: 'Kontribusi', + modalBody: ( + <> +

GoodWebTools bersifat sumber terbuka. Setiap tool berjalan sepenuhnya di browser Anda menggunakan API web modern dan WebAssembly — tidak ada yang diunggah.

+

Punya ide tool yang bisa berjalan sepenuhnya di sisi klien? Atau menemukan bug? Silakan buka issue, atau fork repo dan kirim pull request — kontribusi dan permintaan sangat kami hargai.

+ + ), + }, +}; + export function ShellIsland() { const [modal, setModal] = useState(null); const [menuOpen, setMenuOpen] = useState(false); + const [lang, setLang] = useState('en'); // Settings only apply to the desktop app; hide the nav link on the web. // Starts false so SSR and the first client render match, then reveals on // desktop after mount (avoids a hydration mismatch). const [isDesktop, setIsDesktop] = useState(false); const menuRef = useRef(null); + const s = S[lang]; useEffect(() => { initTheme(); setIsDesktop(isTauri()); + // Detect language from the URL, and keep it current across view transitions. + const syncLang = () => setLang(/^\/id(\/|$)/.test(location.pathname) ? 'id' : 'en'); + syncLang(); + document.addEventListener('astro:page-load', syncLang); + return () => document.removeEventListener('astro:page-load', syncLang); }, []); // Close the mobile overflow menu on outside click, Escape, or navigation. @@ -62,43 +101,43 @@ export function ShellIsland() {
{/* Secondary actions — inline on ≥sm, in an overflow menu on mobile. */}
{isDesktop && ( - + )} - + -
- {menuOpen && (
{isDesktop && ( - Settings + {s.settings} )} - About + {s.about}
)} @@ -114,17 +153,9 @@ export function ShellIsland() { {modal === 'github' && ( - setModal(null)}> + setModal(null)}>
-

- GoodWebTools is open source. Every tool runs entirely - in your browser using modern web APIs and WebAssembly — nothing is uploaded. -

-

- Got an idea for a tool that could work fully client-side? Or found a bug? Please{' '} - open an issue, or fork the - repo and send a pull request — contributions and requests are very welcome. -

+ {s.modalBody} diff --git a/src/pages/[...locale]/about.astro b/src/pages/[...locale]/about.astro new file mode 100644 index 0000000..25729a8 --- /dev/null +++ b/src/pages/[...locale]/about.astro @@ -0,0 +1,123 @@ +--- +import { ChevronRight, Github, ExternalLink, Bookmark, Mail } from 'lucide-react'; +import Base from '@/layouts/Base.astro'; +import { REPO_URL } from '@/config'; +import { LOCALES, localeParam, localizePath, type Lang } from '@/i18n/config'; + +export function getStaticPaths() { + return LOCALES.map(lang => ({ params: { locale: localeParam(lang) }, props: { lang } })); +} + +const { lang } = Astro.props as { lang: Lang }; +const homePath = localizePath('/', lang); + +const A = { + en: { + title: 'About', metaDesc: 'Why GoodWebTools exists — modern web capability, privacy-first, and your files never leave your browser.', + crumb: 'About', home: 'Home', h1: 'About GoodWebTools', + whyH: 'Why this exists', + whyP: "Modern browsers are incredibly capable. With WebAssembly and today's web APIs, things that used to need a desktop app or a server — parsing PDFs, encrypting files, hashing gigabytes, extracting archives, editing images — can run right inside a browser tab. I built GoodWebTools to put that power behind simple, focused pages: open a tool, get your result, done.", + privH: 'Privacy first', + privP1: "Everything runs on your device. Your files, text, and passwords never leave your browser — there's no upload and no account. The only data collected is anonymous, consent-based usage analytics (which loads only if you accept the cookie banner) so I can see which tools are worth improving. Your files are never part of it.", + privP2: "To help cover the costs of running the site, I may show a few unobtrusive ads. That's the extent of the monetization — your data is never part of the deal.", + contactH: 'Contact & tool requests', + contactPre: 'Questions, feedback, or a bug to report? Email us at', + contactReq: "Want a tool that doesn't exist yet? Email the same address and tell us what you need — if it can run fully in the browser, it's a great candidate to build.", + foundH: 'Found it useful?', + foundPre: "If a tool saved you time, bookmark GoodWebTools so it's here next time you need it — press", + foundMac: 'on a Mac or', foundEnd: 'elsewhere.', + osH: 'Open source', + osP: 'GoodWebTools is open source. If you have an idea for a tool that could work fully in the browser, or you want to fix or improve something, open an issue or send a pull request — contributions and requests are very welcome.', + }, + id: { + title: 'Tentang', metaDesc: 'Mengapa GoodWebTools ada — kemampuan web modern, mengutamakan privasi, dan berkas Anda tidak pernah meninggalkan browser.', + crumb: 'Tentang', home: 'Beranda', h1: 'Tentang GoodWebTools', + whyH: 'Mengapa ini ada', + whyP: 'Browser modern sangat mumpuni. Dengan WebAssembly dan API web masa kini, hal-hal yang dulu memerlukan aplikasi desktop atau server — mengurai PDF, mengenkripsi berkas, meng-hash gigabita data, mengekstrak arsip, mengedit gambar — kini bisa berjalan langsung di dalam tab browser. Saya membuat GoodWebTools untuk menghadirkan kemampuan itu lewat halaman yang sederhana dan fokus: buka tool, dapatkan hasilnya, selesai.', + privH: 'Privasi yang utama', + privP1: 'Semuanya berjalan di perangkat Anda. Berkas, teks, dan kata sandi Anda tidak pernah meninggalkan browser — tidak ada unggahan dan tidak ada akun. Satu-satunya data yang dikumpulkan adalah analitik penggunaan anonim berbasis persetujuan (yang hanya dimuat jika Anda menerima banner cookie) agar saya tahu tool mana yang layak ditingkatkan. Berkas Anda tidak pernah menjadi bagiannya.', + privP2: 'Untuk membantu menutup biaya menjalankan situs, saya mungkin menampilkan sedikit iklan yang tidak mengganggu. Hanya sebatas itu monetisasinya — data Anda tidak pernah menjadi bagian dari kesepakatan.', + contactH: 'Kontak & permintaan tool', + contactPre: 'Ada pertanyaan, masukan, atau bug untuk dilaporkan? Kirim email ke kami di', + contactReq: 'Ingin tool yang belum ada? Kirim email ke alamat yang sama dan beri tahu kami kebutuhan Anda — jika bisa berjalan sepenuhnya di browser, itu kandidat yang bagus untuk dibuat.', + foundH: 'Merasa terbantu?', + foundPre: 'Jika sebuah tool menghemat waktu Anda, tandai GoodWebTools (bookmark) agar mudah ditemukan lagi — tekan', + foundMac: 'di Mac atau', foundEnd: 'di tempat lain.', + osH: 'Sumber terbuka', + osP: 'GoodWebTools bersifat sumber terbuka. Jika Anda punya ide tool yang bisa berjalan sepenuhnya di browser, atau ingin memperbaiki atau meningkatkan sesuatu, buka issue atau kirim pull request — kontribusi dan permintaan sangat kami hargai.', + }, +}[lang]; +--- + + +
+ + +

{A.h1}

+ +
+
+

{A.whyH}

+

{A.whyP}

+
+ +
+

{A.privH}

+

{A.privP1}

+

{A.privP2}

+
+ +
+

+ {A.contactH} +

+

+ {A.contactPre}{' '} + contact [at] goodwebtools [dot] com. +

+

{A.contactReq}

+
+ +
+

+ {A.foundH} +

+

+ {A.foundPre} + ⌘ D + {A.foundMac} + Ctrl + D + {A.foundEnd} +

+
+ +
+

{A.osH}

+

{A.osP}

+ + + slaveofcode/goodwebtools + + +
+
+
+ diff --git a/src/pages/about.astro b/src/pages/about.astro deleted file mode 100644 index 0b44451..0000000 --- a/src/pages/about.astro +++ /dev/null @@ -1,81 +0,0 @@ ---- -import { ChevronRight, Github, ExternalLink, Bookmark } from 'lucide-react'; -import Base from '@/layouts/Base.astro'; -import { REPO_URL } from '@/config'; ---- - - -
- - -

About GoodWebTools

- -
-
-

Why this exists

-

- Modern browsers are incredibly capable. With WebAssembly and today's web - APIs, things that used to need a desktop app or a server — parsing PDFs, encrypting files, - hashing gigabytes, extracting archives, editing images — can run right inside a browser - tab. I built GoodWebTools to put that power behind simple, focused pages: - open a tool, get your result, done. -

-
- -
-

Privacy first

-

- Everything runs on your device. Your files, text, and passwords never - leave your browser — there's no upload and no account. The only data collected is - anonymous, consent-based usage analytics (which loads only if you accept the - cookie banner) so I can see which tools are worth improving. Your files are never part of it. -

-

- To help cover the costs of running the site, I may show a few unobtrusive ads. That's the - extent of the monetization — your data is never part of the deal. -

-
- -
-

- Found it useful? -

-

- If a tool saved you time, bookmark GoodWebTools so it's here next time you - need it — press - ⌘ D - on a Mac or - Ctrl + D - elsewhere. -

-
- -
-

Open source

-

- GoodWebTools is open source. If you have an idea for a tool that could work fully in the - browser, or you want to fix or improve something, open an issue or - send a pull request — contributions and requests are very welcome. -

- - - slaveofcode/goodwebtools - - -
-
-
- diff --git a/worker/index.js b/worker/index.js index 45438de..b01169d 100644 --- a/worker/index.js +++ b/worker/index.js @@ -75,7 +75,7 @@ export default { if (request.method === 'GET') { const accept = request.headers.get('accept') || ''; const p = url.pathname; - const isLocalized = p === '/' || p.startsWith('/tools/') || p.startsWith('/category/'); + const isLocalized = p === '/' || p.startsWith('/tools/') || p.startsWith('/category/') || p === '/about' || p.startsWith('/about/'); const alreadyId = p === '/id' || p.startsWith('/id/'); const cookie = request.headers.get('cookie') || ''; const hasChoice = /(?:^|;\s*)gwt\.lang=/.test(cookie); From cec78fe8573300b28f0804c90003a71383086d73 Mon Sep 17 00:00:00 2001 From: Kresna Date: Sun, 2 Aug 2026 12:02:50 +0700 Subject: [PATCH 2/2] fix(analytics): hardcode GA4 ID + hostname guard (build var kept getting dropped) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SITE_GA_ID → PUBLIC_GA_ID → import.meta.env plumbing left gaId empty in production (Cloudflare build-var/WORKERS_CI plumbing is unreliable), so Google Analytics never loaded. A GA4 Measurement ID is public anyway (it's in every visitor's page source), so it now lives in config with a hostname allowlist: GA only runs on goodwebtools.com / www — forks, staging, previews and localhost carry the same ID but never report. Cookie-consent gate unchanged; PUBLIC_GA_ID build var still overrides if present. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Ubfx4XocHcECaL8twp9zsr --- src/config.ts | 14 +++++++++----- src/layouts/Base.astro | 7 +++++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/config.ts b/src/config.ts index fd2fc45..727e587 100644 --- a/src/config.ts +++ b/src/config.ts @@ -8,12 +8,16 @@ export const SITE_NAME = 'GoodWebTools'; export const SITE_TAGLINE = 'Privacy-first client-side utilities that run entirely in your browser'; /** - * Google Analytics measurement ID (e.g. "G-XXXXXXXXXX"). - * Set it via the PUBLIC_GA_ID environment variable (see .env.example). - * Empty = analytics disabled. When set, GA still only loads after the visitor - * accepts cookies in the consent banner (GDPR). + * Google Analytics 4 measurement ID. A GA4 ID is not secret — it's visible in + * every visitor's page source — so it lives in the repo for reliability (a + * Cloudflare build var kept getting dropped). Forks/staging/localhost never report + * to it: the loader in Base.astro only runs GA on the production host(s) below, and + * only after cookie consent (GDPR). PUBLIC_GA_ID (build var) still overrides if set. */ -export const GA_ID = import.meta.env.PUBLIC_GA_ID ?? ''; +export const GA_ID = import.meta.env.PUBLIC_GA_ID || 'G-4Q9F8CL7FW'; + +/** Hostnames on which analytics may run (keeps forks/staging/localhost out). */ +export const GA_ALLOWED_HOSTS = ['goodwebtools.com', 'www.goodwebtools.com']; /** * When PUBLIC_NOINDEX is "1"/"true", every page emits robots noindex. Set it on diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro index a9efaa0..5eb5789 100644 --- a/src/layouts/Base.astro +++ b/src/layouts/Base.astro @@ -3,7 +3,7 @@ import { ViewTransitions } from 'astro:transitions'; import { ShellIsland } from '@/components/shell/ShellIsland'; import { GlobalHotkeyInit } from '@/components/shell/GlobalHotkeyInit'; import { ConsentRibbon } from '@/components/shell/ConsentRibbon'; -import { SITE_NAME, SITE_TAGLINE, SITE_URL, GA_ID, NOINDEX } from '@/config'; +import { SITE_NAME, SITE_TAGLINE, SITE_URL, GA_ID, GA_ALLOWED_HOSTS, NOINDEX } from '@/config'; import { LOCALES, DEFAULT_LOCALE, localizePath, stripLocale, type Lang } from '@/i18n/config'; import '../styles/global.css'; @@ -158,9 +158,12 @@ const jsonLdBlocks = [siteJsonLd, ...(Array.isArray(jsonLd) ? jsonLd : jsonLd ? -