From 837b53724f4d73f1c18d69832ad5b47d4a7c0e22 Mon Sep 17 00:00:00 2001 From: baixiangcpp Date: Sat, 27 Jun 2026 05:29:25 +0800 Subject: [PATCH 1/5] Fix comparison page acceptance coverage --- src/app/[lang]/about/page.tsx | 27 +++++++-- src/app/[lang]/page.tsx | 26 +++++--- src/app/[lang]/trust-center/page.tsx | 12 ++++ src/core/growth/growth-pages.ts | 91 +++++++++++++++++++++++++--- tests/guards/growth-pages.test.tsx | 58 +++++++++++++++++- 5 files changed, 193 insertions(+), 21 deletions(-) diff --git a/src/app/[lang]/about/page.tsx b/src/app/[lang]/about/page.tsx index d048f0af..4e1c22d6 100644 --- a/src/app/[lang]/about/page.tsx +++ b/src/app/[lang]/about/page.tsx @@ -1,12 +1,22 @@ -"use client" - import Link from "next/link" -import { useLang } from "@/core/i18n/lang-provider" +import { notFound } from "next/navigation" import { Github, Globe, Code2, Zap, Shield, ArrowUpRight } from "lucide-react" +import { getGrowthIndex } from "@/core/growth/growth-pages" +import { isValidLocale } from "@/core/i18n/i18n" +import { getTranslation } from "@/core/i18n/translations/catalog" + +export default async function AboutPage({ params }: { params: Promise<{ lang: string }> }) { + const { lang } = await params + if (!isValidLocale(lang)) { + notFound() + } -export default function AboutPage() { - const { t, lang } = useLang() + const t = getTranslation(lang) const p = t.pages + const compareIndex = getGrowthIndex("compare") + if (!compareIndex) { + throw new Error("[about] Missing compare growth index") + } const highlights = [ { icon: Shield, title: p.about_privacy_title, desc: p.about_privacy_desc }, @@ -84,6 +94,13 @@ export default function AboutPage() { Self-hosting + + + {compareIndex.title[lang]} + - {installAppLinkLabel} - - - - + > + {installAppLinkLabel} + + + {compareIndex.title[locale]} + + + + {/* Quick Navigation Cards - Compact Grid */}
const externalTools = getExternalRequestToolDisclosures(TOOL_REGISTRY) .map((item) => ({ @@ -181,6 +186,13 @@ export default async function TrustCenterPage({
diff --git a/src/core/growth/growth-pages.ts b/src/core/growth/growth-pages.ts index 19dfad01..3b41315c 100644 --- a/src/core/growth/growth-pages.ts +++ b/src/core/growth/growth-pages.ts @@ -86,6 +86,81 @@ export type GrowthUiCopy = { trustBaselineDescription: string } +const GROWTH_LOCALES: Locale[] = ["en", "zh-CN", "zh-TW", "ja", "ko", "de", "fr"] + +const REQUIRED_COMPARISON_DECISION_ROWS: GrowthComparisonRow[] = [ + { + factor: "Privacy", + byteflow: "Use the Trust Center, tool trust labels, and sample inputs to verify whether sensitive data stays in the browser.", + other: "Review each alternative's runtime behavior, storage policy, analytics, and deployment owner before using production data.", + note: "No comparison page should be treated as permission to paste secrets without verification.", + }, + { + factor: "Local execution", + byteflow: "Prefer browser-local tools for parsing, formatting, encoding, hashing, redaction, and snippet generation when a network call is unnecessary.", + other: "Some hosted tools may proxy, store, or execute work on a server even when the UI feels instant.", + note: "Use DevTools Network with sample data when the processing boundary matters.", + }, + { + factor: "Offline use", + byteflow: "Use installable PWA flows and cached tool shells for workflows that should remain available without a live connection.", + other: "Many single-purpose online tools require a fresh network request for the page, scripts, or processing endpoint.", + note: "Offline availability still depends on the route and assets having been cached first.", + }, + { + factor: "Open source", + byteflow: "Review the public repository, issues, and implementation when a workflow needs inspectable behavior.", + other: "Closed or opaque tools require more vendor trust because runtime and storage behavior may not be independently reviewable.", + note: "Open source does not replace verification, but it makes claims easier to audit.", + }, + { + factor: "Workflow composition", + byteflow: "Combine focused tools, related links, and Pipeline Builder when a task needs repeatable multi-step handling.", + other: "Recipe workbenches or specialized sites may be faster when their composition model already matches the task.", + note: "Choose the model your team can document and repeat with the least ambiguity.", + }, + { + factor: "Platform coverage", + byteflow: "Run the web app in modern desktop and mobile browsers, with installable app behavior where supported.", + other: "Native apps, extensions, CLI tools, and hosted sites can cover different device or automation needs.", + note: "Check the target platform before standardizing a team workflow.", + }, + { + factor: "Pricing", + byteflow: "Use the public site and source without an account for the comparison workflows described here.", + other: "Some alternatives may add paid tiers, account requirements, usage limits, or hosted-team features.", + note: "Verify current pricing and limits before making a procurement decision.", + }, +] + +function localizeRequiredComparisonDecisionRows(locale: Locale): GrowthComparisonRow[] { + if (locale === "en") return REQUIRED_COMPARISON_DECISION_ROWS + const copy = getComparisonLocaleCopy()[locale] + + return REQUIRED_COMPARISON_DECISION_ROWS.map((row) => ({ + factor: `${localizeComparisonTerms(row.factor, locale)}${copy.factorSuffix}`, + byteflow: `${copy.byteflowPrefix}${localizeComparisonTerms(row.byteflow, locale)}`, + other: `${copy.otherPrefix}${localizeComparisonTerms(row.other, locale)}`, + note: `${copy.notePrefix}${localizeComparisonTerms(row.note, locale)}`, + })) +} + +function withRequiredComparisonDecisionRows(copy: GrowthPageCopy, locale: Locale): GrowthPageCopy { + return { + ...copy, + comparisonRows: [ + ...(copy.comparisonRows ?? []), + ...localizeRequiredComparisonDecisionRows(locale), + ], + } +} + +function withRequiredComparisonCopy(copyByLocale: Record): Record { + return Object.fromEntries( + GROWTH_LOCALES.map((locale) => [locale, withRequiredComparisonDecisionRows(copyByLocale[locale], locale)]), + ) as Record +} + export const GROWTH_UI_COPY: Record = { en: { keyTakeaways: "Key takeaways", @@ -333,7 +408,7 @@ export const GROWTH_PAGES: GrowthPage[] = [ slug: "compare/byteflow-vs-cyberchef", kind: "comparison", relatedToolKeys: ["json_formatter", "base64_encode_decode", "hash_generator", "url_encode_decode"], - copy: { + copy: withRequiredComparisonCopy({ en: { eyebrow: "Comparison", title: "Byteflow vs CyberChef", @@ -768,13 +843,13 @@ export const GROWTH_PAGES: GrowthPage[] = [ }, ], }, - }, + }), }, { slug: "compare/byteflow-vs-jwt-io", kind: "comparison", relatedToolKeys: ["jwt_decoder", "jwt_workbench", "jwt_verifier", "base64_encode_decode"], - copy: { + copy: withRequiredComparisonCopy({ en: jwtPageCopy("Comparison", "Byteflow vs jwt.io"), "zh-CN": jwtPageCopyZhCN(), "zh-TW": jwtPageCopyZhTW(), @@ -782,7 +857,7 @@ export const GROWTH_PAGES: GrowthPage[] = [ ko: jwtPageCopyKo(), de: jwtPageCopyDe(), fr: jwtPageCopyFr(), - }, + }), }, { slug: "alternatives/json-formatter-privacy-first", @@ -802,7 +877,7 @@ export const GROWTH_PAGES: GrowthPage[] = [ slug: "compare/md5-vs-sha256", kind: "comparison", relatedToolKeys: ["hash_generator", "md5_generator", "base64_encode_decode", "jwt_decoder"], - copy: { + copy: withRequiredComparisonCopy({ en: hashComparisonCopyEn(), "zh-CN": hashComparisonCopyZhCN(), "zh-TW": hashComparisonCopyZhTW(), @@ -810,7 +885,7 @@ export const GROWTH_PAGES: GrowthPage[] = [ ko: hashComparisonCopyKo(), de: hashComparisonCopyDe(), fr: hashComparisonCopyFr(), - }, + }), }, { slug: "compare/json-formatter-vs-json-validator", @@ -1650,7 +1725,7 @@ function localizeComparisonSeed(seed: ComparisonSeed, locale: Locale): GrowthPag } function comparisonPageCopy(seed: ComparisonSeed): Record { - return { + return withRequiredComparisonCopy({ en: seed, "zh-CN": localizeComparisonSeed(seed, "zh-CN"), "zh-TW": localizeComparisonSeed(seed, "zh-TW"), @@ -1658,7 +1733,7 @@ function comparisonPageCopy(seed: ComparisonSeed): Record> = { ko: /[\uAC00-\uD7AF]/, } +const REQUIRED_COMPARISON_DECISION_FACTORS = [ + "privacy", + "local execution", + "offline use", + "open source", + "workflow composition", + "platform coverage", + "pricing", +] + function flattenCopy(copy: GrowthPageCopy): string[] { return [ copy.eyebrow, @@ -91,7 +102,7 @@ describe("BF-028 growth content pages", () => { for (const page of comparisonPages) { const copy = page.copy.en - expect(copy.comparisonRows?.length, page.slug).toBeGreaterThanOrEqual(3) + expect(copy.comparisonRows?.length, page.slug).toBeGreaterThanOrEqual(7) expect(copy.sections.length, page.slug).toBeGreaterThanOrEqual(2) expect(copy.faq.length, page.slug).toBeGreaterThanOrEqual(2) expect(page.relatedToolKeys.length, page.slug).toBeGreaterThanOrEqual(2) @@ -99,6 +110,18 @@ describe("BF-028 growth content pages", () => { } }) + it("keeps every comparison decision table aligned with issue 242 criteria", () => { + const comparisonPages = GROWTH_PAGES.filter((page) => page.kind === "comparison") + + for (const page of comparisonPages) { + const factors = (page.copy.en.comparisonRows ?? []).map((row) => row.factor.toLowerCase()) + + for (const factor of REQUIRED_COMPARISON_DECISION_FACTORS) { + expect(factors, `${page.slug} missing ${factor}`).toContain(factor) + } + } + }) + it("requires complete localized copy for every growth page and locale", () => { for (const page of GROWTH_PAGES) { const english = flattenCopy(page.copy.en) @@ -176,6 +199,39 @@ describe("BF-028 growth content pages", () => { } }) + it("links homepage, About, and Trust Center source to comparison content", () => { + const sources = [ + "src/app/[lang]/page.tsx", + "src/app/[lang]/about/page.tsx", + "src/app/[lang]/trust-center/page.tsx", + ] + + for (const source of sources) { + const code = fs.readFileSync(source, "utf8") + expect(code, source).toContain("getGrowthIndex(\"compare\")") + expect(code, source).toMatch(/href=\{`\/\$\{(?:locale|lang)\}\/compare`\}/) + } + }) + + it("generates unique comparison metadata with canonical and social previews", async () => { + const metadata = await generateCyberChefComparisonMetadata({ params: Promise.resolve({ lang: "en" }) }) + + expect(metadata.title).toEqual("Byteflow vs CyberChef") + expect(metadata.description).toContain("Compare Byteflow and CyberChef") + expect(metadata.alternates?.canonical).toBe("https://byteflow.tools/en/compare/byteflow-vs-cyberchef") + expect(metadata.openGraph).toMatchObject({ + title: "Byteflow vs CyberChef | byteflow.tools", + url: "https://byteflow.tools/en/compare/byteflow-vs-cyberchef", + type: "article", + }) + expect(metadata.openGraph?.images).toEqual(["https://byteflow.tools/og/pages/en/compare/byteflow-vs-cyberchef.jpg"]) + expect(metadata.twitter).toMatchObject({ + card: "summary_large_image", + title: "Byteflow vs CyberChef | byteflow.tools", + }) + expect(metadata.twitter?.images).toEqual(["https://byteflow.tools/og/pages/en/compare/byteflow-vs-cyberchef.jpg"]) + }) + it("renders matching Article or HowTo, BreadcrumbList, and FAQPage JSON-LD", () => { for (const page of GROWTH_PAGES) { const markup = renderToStaticMarkup() From 7a494e75338e516a9ad0047c51a0eae93748fe10 Mon Sep 17 00:00:00 2001 From: baixiangcpp Date: Sat, 27 Jun 2026 05:39:55 +0800 Subject: [PATCH 2/5] Update sitemap lastmod for comparison links --- src/lib/sitemap-lastmod.json | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/lib/sitemap-lastmod.json b/src/lib/sitemap-lastmod.json index f887abb3..7193f108 100644 --- a/src/lib/sitemap-lastmod.json +++ b/src/lib/sitemap-lastmod.json @@ -10,13 +10,13 @@ "fr" ], "home": { - "en": "2026-06-24T00:00:00.000Z", - "zh-CN": "2026-06-24T00:00:00.000Z", - "zh-TW": "2026-06-24T00:00:00.000Z", - "ja": "2026-06-24T00:00:00.000Z", - "ko": "2026-06-24T00:00:00.000Z", - "de": "2026-06-24T00:00:00.000Z", - "fr": "2026-06-24T00:00:00.000Z" + "en": "2026-06-26T00:00:00.000Z", + "zh-CN": "2026-06-26T00:00:00.000Z", + "zh-TW": "2026-06-26T00:00:00.000Z", + "ja": "2026-06-26T00:00:00.000Z", + "ko": "2026-06-26T00:00:00.000Z", + "de": "2026-06-26T00:00:00.000Z", + "fr": "2026-06-26T00:00:00.000Z" }, "hubs": { "formatters": { @@ -193,13 +193,13 @@ "fr": "2026-06-25T00:00:00.000Z" }, "about": { - "en": "2026-06-24T00:00:00.000Z", - "zh-CN": "2026-06-24T00:00:00.000Z", - "zh-TW": "2026-06-24T00:00:00.000Z", - "ja": "2026-06-24T00:00:00.000Z", - "ko": "2026-06-24T00:00:00.000Z", - "de": "2026-06-24T00:00:00.000Z", - "fr": "2026-06-24T00:00:00.000Z" + "en": "2026-06-26T00:00:00.000Z", + "zh-CN": "2026-06-26T00:00:00.000Z", + "zh-TW": "2026-06-26T00:00:00.000Z", + "ja": "2026-06-26T00:00:00.000Z", + "ko": "2026-06-26T00:00:00.000Z", + "de": "2026-06-26T00:00:00.000Z", + "fr": "2026-06-26T00:00:00.000Z" }, "pricing": { "en": "2026-06-25T00:00:00.000Z", From 95da33907ad4b92cb57ee2217d58c36507d25691 Mon Sep 17 00:00:00 2001 From: baixiangcpp Date: Sat, 27 Jun 2026 05:57:35 +0800 Subject: [PATCH 3/5] Keep homepage comparison link within budget --- src/app/[lang]/page.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/app/[lang]/page.tsx b/src/app/[lang]/page.tsx index a2242796..b1d71959 100644 --- a/src/app/[lang]/page.tsx +++ b/src/app/[lang]/page.tsx @@ -204,24 +204,18 @@ export default async function Home({ params }: { params: Promise<{ lang: string {getLocalizedToolTitle("pipeline_builder")} {installAppLinkLabel} - - {compareIndex.title[locale]} - + {compareIndex.eyebrow[locale]} From bddf36204c3f93714130fdf28da19ad6c5bc4b16 Mon Sep 17 00:00:00 2001 From: baixiangcpp Date: Sat, 27 Jun 2026 06:13:01 +0800 Subject: [PATCH 4/5] Localize comparison decision terms --- src/core/growth/growth-pages.ts | 54 +++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/core/growth/growth-pages.ts b/src/core/growth/growth-pages.ts index 3b41315c..95efbef9 100644 --- a/src/core/growth/growth-pages.ts +++ b/src/core/growth/growth-pages.ts @@ -1476,6 +1476,15 @@ function getComparisonTermLocalization(): Record> "SVG Converter": "SVG 转换器", "Related tools": "相关工具", "Related Tools": "相关工具", + "Trust Center": "隐私与信任中心", + "Open Source": "开源", + "Open source": "开源", + "open source": "开源", + "Pipeline Builder": "流水线构建器", + "Trust labels": "信任标签", + "trust labels": "信任标签", + "Browser-local tools": "浏览器本地工具", + "browser-local tools": "浏览器本地工具", }, "zh-TW": { "JSON Formatter": "JSON 格式化工具", @@ -1493,6 +1502,15 @@ function getComparisonTermLocalization(): Record> "SVG Converter": "SVG 轉換器", "Related tools": "相關工具", "Related Tools": "相關工具", + "Trust Center": "隱私與信任中心", + "Open Source": "開源", + "Open source": "開源", + "open source": "開源", + "Pipeline Builder": "流程建構器", + "Trust labels": "信任標籤", + "trust labels": "信任標籤", + "Browser-local tools": "瀏覽器本地工具", + "browser-local tools": "瀏覽器本地工具", }, ja: { "JSON Formatter": "JSON フォーマッター", @@ -1510,6 +1528,15 @@ function getComparisonTermLocalization(): Record> "SVG Converter": "SVG コンバーター", "Related tools": "関連ツール", "Related Tools": "関連ツール", + "Trust Center": "プライバシーと信頼センター", + "Open Source": "オープンソース", + "Open source": "オープンソース", + "open source": "オープンソース", + "Pipeline Builder": "パイプラインビルダー", + "Trust labels": "信頼ラベル", + "trust labels": "信頼ラベル", + "Browser-local tools": "ブラウザーローカルツール", + "browser-local tools": "ブラウザーローカルツール", }, ko: { "JSON Formatter": "JSON 포매터", @@ -1527,6 +1554,15 @@ function getComparisonTermLocalization(): Record> "SVG Converter": "SVG 변환기", "Related tools": "관련 도구", "Related Tools": "관련 도구", + "Trust Center": "개인정보 및 신뢰 센터", + "Open Source": "오픈 소스", + "Open source": "오픈 소스", + "open source": "오픈 소스", + "Pipeline Builder": "파이프라인 빌더", + "Trust labels": "신뢰 라벨", + "trust labels": "신뢰 라벨", + "Browser-local tools": "브라우저 로컬 도구", + "browser-local tools": "브라우저 로컬 도구", }, de: { "JSON Formatter": "JSON-Formatter", @@ -1544,6 +1580,15 @@ function getComparisonTermLocalization(): Record> "SVG Converter": "SVG-Konverter", "Related tools": "Verwandte Tools", "Related Tools": "Verwandte Tools", + "Trust Center": "Datenschutz- und Vertrauenszentrum", + "Open Source": "quelloffen", + "Open source": "quelloffen", + "open source": "quelloffen", + "Pipeline Builder": "Pipeline-Builder", + "Trust labels": "Vertrauenslabels", + "trust labels": "Vertrauenslabels", + "Browser-local tools": "browserlokale Tools", + "browser-local tools": "browserlokale Tools", }, fr: { "JSON Formatter": "Formateur JSON", @@ -1561,6 +1606,15 @@ function getComparisonTermLocalization(): Record> "SVG Converter": "Convertisseur SVG", "Related tools": "Outils connexes", "Related Tools": "Outils connexes", + "Trust Center": "Centre de confiance", + "Open Source": "open source", + "Open source": "open source", + "open source": "open source", + "Pipeline Builder": "Constructeur de pipeline", + "Trust labels": "labels de confiance", + "trust labels": "labels de confiance", + "Browser-local tools": "outils locaux dans le navigateur", + "browser-local tools": "outils locaux dans le navigateur", }, } } From 0b0c42dec953065129f82712793ad9c1e4e4a481 Mon Sep 17 00:00:00 2001 From: baixiangcpp Date: Sat, 27 Jun 2026 06:24:53 +0800 Subject: [PATCH 5/5] Speed up log scrubber position tracking --- src/core/utils/log-scrubber-utils.ts | 44 +++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/src/core/utils/log-scrubber-utils.ts b/src/core/utils/log-scrubber-utils.ts index 538955ad..a58a411b 100644 --- a/src/core/utils/log-scrubber-utils.ts +++ b/src/core/utils/log-scrubber-utils.ts @@ -180,17 +180,52 @@ const RULES: ScrubRule[] = [ }, ] +function getLineColumns(text: string, indexes: number[]): { line: number; column: number }[] { + const positions: { line: number; column: number }[] = [] + let line = 1 + let column = 1 + let cursor = 0 + + for (const index of indexes) { + while (cursor < index) { + const codePoint = text.codePointAt(cursor) + if (codePoint === undefined) break + + if (codePoint === 10) { + line += 1 + column = 1 + } else { + column += 1 + } + + cursor += codePoint > 0xffff ? 2 : 1 + } + + positions.push({ line, column }) + } + + return positions +} + function getLineColumn(text: string, index: number): { line: number; column: number } { let line = 1 let column = 1 - for (const char of Array.from(text.slice(0, index))) { - if (char === "\n") { + let cursor = 0 + + while (cursor < index) { + const codePoint = text.codePointAt(cursor) + if (codePoint === undefined) break + + if (codePoint === 10) { line += 1 column = 1 } else { column += 1 } + + cursor += codePoint > 0xffff ? 2 : 1 } + return { line, column } } @@ -239,8 +274,9 @@ function collectMatches(input: string, options: ScrubOptions): ScrubMatch[] { export function scrubLogs(input: string, options: ScrubOptions = DEFAULT_SCRUB_OPTIONS): ScrubResult { const matches = collectMatches(input, options) - const findings: ScrubFinding[] = matches.map((match) => { - const position = getLineColumn(input, match.start) + const positions = getLineColumns(input, matches.map((match) => match.start)) + const findings: ScrubFinding[] = matches.map((match, index) => { + const position = positions[index] ?? getLineColumn(input, match.start) return { type: match.rule.type, label: match.rule.label,