From 3e18cebc5191c14b9f75edc6d123cff3a1f02cf5 Mon Sep 17 00:00:00 2001 From: baixiangcpp Date: Tue, 23 Jun 2026 09:12:29 -0700 Subject: [PATCH] BF-001-BF-008: complete P0 audit batch --- public/_redirects | 21 ++++ scripts/e2e/run-playwright-smoke.js | 2 + scripts/gates/check-canonical.js | 10 +- scripts/gates/check-hreflang.js | 10 +- scripts/gates/check-legacy-routes.js | 24 +++- scripts/generators/generate-legacy-routes.js | 52 ++++++-- src/app/[lang]/about/page.tsx | 2 +- src/app/[lang]/all-tools/layout.tsx | 11 +- src/app/[lang]/all-tools/page.tsx | 2 + src/app/[lang]/convert-encode/layout.tsx | 16 ++- src/app/[lang]/design-media/layout.tsx | 16 ++- src/app/[lang]/web-api/layout.tsx | 16 ++- src/app/globals.css | 2 + src/app/robots.ts | 10 ++ src/app/sitemap.ts | 4 +- src/core/i18n/translations/de.json | 49 +++++++- src/core/i18n/translations/en.json | 49 +++++++- src/core/i18n/translations/fr.json | 49 +++++++- src/core/i18n/translations/ja.json | 49 +++++++- src/core/i18n/translations/ko.json | 49 +++++++- src/core/i18n/translations/zh-CN.json | 49 +++++++- src/core/i18n/translations/zh-TW.json | 49 +++++++- .../routing/legacy-taxonomy-redirects.json | 5 + src/core/routing/seo-route-policy.ts | 35 ++++++ src/features/pipeline/recipe-codec.ts | 40 +----- src/features/pipeline/recipe-import-export.ts | 4 +- src/features/pipeline/recipe-sanitizer.ts | 65 ++++++++++ src/features/pipeline/recipe-store.ts | 12 +- .../tool-discovery/all-tools-discovery.tsx | 20 +-- .../tool-discovery/all-tools-query-robots.tsx | 26 ++++ src/features/tool-shell/index.ts | 1 + .../tool-shell/sensitive-input-warning.tsx | 59 +++++++++ src/features/tool-shell/tool-action-bar.tsx | 11 +- .../tool-templates/focused-hash-tool-page.tsx | 35 ++++-- .../tools/certificate-decoder/page.tsx | 3 + src/features/tools/env-parser/page.tsx | 5 + .../tools/har-viewer-sanitizer/page.tsx | 3 + src/features/tools/hash-generator/page.tsx | 19 +-- .../tools/http-request-builder/page.tsx | 3 + src/features/tools/image-resizer/page.tsx | 2 +- src/features/tools/json-diff-viewer/page.tsx | 4 +- src/features/tools/json-formatter/page.tsx | 2 +- src/features/tools/jwt-decoder/page.tsx | 3 + src/features/tools/jwt-verifier/page.tsx | 20 ++- .../tools/jwt-workbench/jwt-secret-field.tsx | 50 ++++++++ src/features/tools/jwt-workbench/page.tsx | 23 ++-- src/features/tools/log-scrubber/page.tsx | 3 + src/features/tools/pipeline-builder/page.tsx | 88 +++++++++++-- .../pipeline-builder/pipeline-onboarding.tsx | 77 ++++++++++++ .../pipeline-privacy-preview.tsx | 61 +++++++++ .../pipeline-builder/pipeline-step-list.tsx | 87 +++++++++++-- src/features/tools/regex-tester/page.tsx | 6 +- src/features/tools/saml-decoder/page.tsx | 2 + src/features/tools/text-diff-checker/page.tsx | 2 +- src/lib/sitemap-lastmod.json | 70 +++++------ .../phase3-pipeline-builder-page.test.tsx | 50 +++++++- tests/guards/all-tools-route-guard.test.ts | 1 + .../guards/bf-p0-mobile-layout-guard.test.ts | 34 +++++ tests/guards/bf-p0-seo-routing.test.ts | 116 ++++++++++++++++++ tests/guards/hash-worker-guard.test.ts | 4 +- .../jwt-security-semantics-guard.test.ts | 10 +- ...line-builder-privacy-preview-guard.test.ts | 26 ++++ .../sensitive-input-warning-guard.test.ts | 55 +++++++++ tests/guards/sitemap.test.ts | 13 ++ tests/unit/pipeline-foundation.test.ts | 58 +++++++++ 65 files changed, 1559 insertions(+), 195 deletions(-) create mode 100644 src/core/routing/legacy-taxonomy-redirects.json create mode 100644 src/core/routing/seo-route-policy.ts create mode 100644 src/features/pipeline/recipe-sanitizer.ts create mode 100644 src/features/tool-discovery/all-tools-query-robots.tsx create mode 100644 src/features/tool-shell/sensitive-input-warning.tsx create mode 100644 src/features/tools/jwt-workbench/jwt-secret-field.tsx create mode 100644 src/features/tools/pipeline-builder/pipeline-onboarding.tsx create mode 100644 src/features/tools/pipeline-builder/pipeline-privacy-preview.tsx create mode 100644 tests/guards/bf-p0-mobile-layout-guard.test.ts create mode 100644 tests/guards/bf-p0-seo-routing.test.ts create mode 100644 tests/guards/pipeline-builder-privacy-preview-guard.test.ts create mode 100644 tests/guards/sensitive-input-warning-guard.test.ts diff --git a/public/_redirects b/public/_redirects index eae0fbad..a53cbd1e 100644 --- a/public/_redirects +++ b/public/_redirects @@ -12,3 +12,24 @@ /ko/tls-chain-inspector /ko/certificate-decoder 301 /de/tls-chain-inspector /de/certificate-decoder 301 /fr/tls-chain-inspector /fr/certificate-decoder 301 +/en/web-api /en/web-api-network 301 +/zh-CN/web-api /zh-CN/web-api-network 301 +/zh-TW/web-api /zh-TW/web-api-network 301 +/ja/web-api /ja/web-api-network 301 +/ko/web-api /ko/web-api-network 301 +/de/web-api /de/web-api-network 301 +/fr/web-api /fr/web-api-network 301 +/en/design-media /en/images-svg-css 301 +/zh-CN/design-media /zh-CN/images-svg-css 301 +/zh-TW/design-media /zh-TW/images-svg-css 301 +/ja/design-media /ja/images-svg-css 301 +/ko/design-media /ko/images-svg-css 301 +/de/design-media /de/images-svg-css 301 +/fr/design-media /fr/images-svg-css 301 +/en/convert-encode /en/all-tools 301 +/zh-CN/convert-encode /zh-CN/all-tools 301 +/zh-TW/convert-encode /zh-TW/all-tools 301 +/ja/convert-encode /ja/all-tools 301 +/ko/convert-encode /ko/all-tools 301 +/de/convert-encode /de/all-tools 301 +/fr/convert-encode /fr/all-tools 301 diff --git a/scripts/e2e/run-playwright-smoke.js b/scripts/e2e/run-playwright-smoke.js index c21618ea..73b94fae 100644 --- a/scripts/e2e/run-playwright-smoke.js +++ b/scripts/e2e/run-playwright-smoke.js @@ -727,6 +727,7 @@ async function assertPipelineRecipeJourney(context, baseUrl) { return buttons.some((button) => button.textContent?.trim() === "Save" && !button.disabled); }, null, { timeout: 15_000 }); await saveButton.click(); + await page.getByRole("button", { name: /Save structure only/i }).click(); await page.waitForFunction(() => { const select = document.querySelector("[aria-label='Select saved recipe']"); if (!(select instanceof HTMLSelectElement)) return false; @@ -767,6 +768,7 @@ async function assertPipelineRecipeJourney(context, baseUrl) { const downloadPromise = page.waitForEvent("download"); await page.getByRole("button", { name: /^Export JSON$/ }).first().click(); + await page.getByRole("button", { name: /Export structure only/i }).click(); const download = await downloadPromise; const downloadPath = await download.path(); if (!downloadPath) { diff --git a/scripts/gates/check-canonical.js b/scripts/gates/check-canonical.js index f5a4d4de..7ed7ba03 100644 --- a/scripts/gates/check-canonical.js +++ b/scripts/gates/check-canonical.js @@ -5,9 +5,17 @@ const BASE_URL = process.env.SITE_URL || "https://byteflow.tools" const LOCALES = new Set(["en", "zh-CN", "zh-TW", "ja", "ko", "de", "fr"]) const DEFAULT_SCAN_DIRS = [".next/server/app", "out"] const TOOL_ALIAS_PATH = path.join(process.cwd(), "src", "core", "registry", "tool-aliases.json") +const LEGACY_TAXONOMY_REDIRECT_PATH = path.join(process.cwd(), "src", "core", "routing", "legacy-taxonomy-redirects.json") const TOOL_ALIAS_TO_CANONICAL_SLUG = fs.existsSync(TOOL_ALIAS_PATH) ? JSON.parse(fs.readFileSync(TOOL_ALIAS_PATH, "utf8")) : {} +const LEGACY_TAXONOMY_REDIRECTS = fs.existsSync(LEGACY_TAXONOMY_REDIRECT_PATH) + ? JSON.parse(fs.readFileSync(LEGACY_TAXONOMY_REDIRECT_PATH, "utf8")) + : {} + +function canonicalSlugFor(slug) { + return LEGACY_TAXONOMY_REDIRECTS[slug] || TOOL_ALIAS_TO_CANONICAL_SLUG[slug] || slug +} function resolveScanDir() { if (process.env.CANONICAL_SCAN_DIR) { @@ -69,7 +77,7 @@ function expectedCanonicalFor(relativePath) { const localizedPage = relativePath.match(/^([^/]+)\/(.+)\.html$/) if (localizedPage) { const locale = localizedPage[1] - const slug = TOOL_ALIAS_TO_CANONICAL_SLUG[localizedPage[2]] || localizedPage[2] + const slug = canonicalSlugFor(localizedPage[2]) if (LOCALES.has(locale)) { return `${BASE_URL}/${locale}/${slug}` } diff --git a/scripts/gates/check-hreflang.js b/scripts/gates/check-hreflang.js index cb1e64da..da8e14af 100644 --- a/scripts/gates/check-hreflang.js +++ b/scripts/gates/check-hreflang.js @@ -6,9 +6,17 @@ const LOCALES = ["en", "zh-CN", "zh-TW", "ja", "ko", "de", "fr"] const LOCALE_SET = new Set(LOCALES) const DEFAULT_SCAN_DIRS = [".next/server/app", "out"] const TOOL_ALIAS_PATH = path.join(process.cwd(), "src", "core", "registry", "tool-aliases.json") +const LEGACY_TAXONOMY_REDIRECT_PATH = path.join(process.cwd(), "src", "core", "routing", "legacy-taxonomy-redirects.json") const TOOL_ALIAS_TO_CANONICAL_SLUG = fs.existsSync(TOOL_ALIAS_PATH) ? JSON.parse(fs.readFileSync(TOOL_ALIAS_PATH, "utf8")) : {} +const LEGACY_TAXONOMY_REDIRECTS = fs.existsSync(LEGACY_TAXONOMY_REDIRECT_PATH) + ? JSON.parse(fs.readFileSync(LEGACY_TAXONOMY_REDIRECT_PATH, "utf8")) + : {} + +function canonicalSlugFor(slug) { + return LEGACY_TAXONOMY_REDIRECTS[slug] || TOOL_ALIAS_TO_CANONICAL_SLUG[slug] || slug +} function resolveScanDir() { if (process.env.HREFLANG_SCAN_DIR) { @@ -76,7 +84,7 @@ function parseRoute(relativePath) { } function expectedHreflangs(slug) { - const canonicalSlug = slug ? (TOOL_ALIAS_TO_CANONICAL_SLUG[slug] || slug) : null + const canonicalSlug = slug ? canonicalSlugFor(slug) : null const expected = new Map() for (const locale of LOCALES) { expected.set( diff --git a/scripts/gates/check-legacy-routes.js b/scripts/gates/check-legacy-routes.js index 373a313d..80597cd3 100644 --- a/scripts/gates/check-legacy-routes.js +++ b/scripts/gates/check-legacy-routes.js @@ -19,8 +19,12 @@ function readJson(filePath) { return JSON.parse(fs.readFileSync(filePath, "utf8")) } +function sitemapContainsSlugUrl(sitemap, slug) { + return new RegExp(`https://byteflow\\.tools/(?:${["en", "zh-CN", "zh-TW", "ja", "ko", "de", "fr"].join("|")})/${slug}`).test(sitemap) +} + function main() { - const routes = checkGeneratedLegacyRoutes() + const { routes, taxonomyRedirects } = checkGeneratedLegacyRoutes() const canonicalToolSlugs = new Set(loadOrderedToolManifests().map((tool) => tool.slug)) const routeGroups = readJson(ROUTE_GROUPS_PATH) const sitemapRouteSlugs = new Set([ @@ -47,14 +51,28 @@ function main() { } } + for (const [sourceSlug, targetSlug] of Object.entries(taxonomyRedirects)) { + if (!hasRoutePage(sourceSlug)) { + problems.push(`${sourceSlug}: missing static fallback route page`) + } + if (!hasRoutePage(targetSlug)) { + problems.push(`${sourceSlug}: taxonomy target route does not exist (${targetSlug})`) + } + } + for (const sitemapPath of SITEMAP_CANDIDATES) { if (!fs.existsSync(sitemapPath) || !fs.statSync(sitemapPath).isFile()) continue const sitemap = fs.readFileSync(sitemapPath, "utf8") for (const route of routes) { - if (sitemap.includes(`/${route.sourceSlug}`)) { + if (sitemapContainsSlugUrl(sitemap, route.sourceSlug)) { problems.push(`${path.relative(ROOT, sitemapPath)} contains legacy source ${route.sourceSlug}`) } } + for (const sourceSlug of Object.keys(taxonomyRedirects)) { + if (sitemapContainsSlugUrl(sitemap, sourceSlug)) { + problems.push(`${path.relative(ROOT, sitemapPath)} contains legacy taxonomy source ${sourceSlug}`) + } + } } if (problems.length > 0) { @@ -65,7 +83,7 @@ function main() { process.exit(1) } - console.log(`[check:legacy-routes] OK: ${routes.length} legacy route(s) have valid targets, redirects, and sitemap exclusions`) + console.log(`[check:legacy-routes] OK: ${routes.length} legacy route(s) and ${Object.keys(taxonomyRedirects).length} legacy taxonomy route(s) have valid targets, redirects, and sitemap exclusions`) } main() diff --git a/scripts/generators/generate-legacy-routes.js b/scripts/generators/generate-legacy-routes.js index d0f26658..4b4bb06f 100644 --- a/scripts/generators/generate-legacy-routes.js +++ b/scripts/generators/generate-legacy-routes.js @@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url" const ROOT = process.cwd() const LEGACY_ROUTES_PATH = path.join(ROOT, "src/core/routing/legacy-routes.json") +const LEGACY_TAXONOMY_REDIRECTS_PATH = path.join(ROOT, "src/core/routing/legacy-taxonomy-redirects.json") const TOOL_ALIASES_PATH = path.join(ROOT, "src/core/registry/tool-aliases.json") const REDIRECTS_PATH = path.join(ROOT, "public/_redirects") const LOCALES = ["en", "zh-CN", "zh-TW", "ja", "ko", "de", "fr"] @@ -63,19 +64,53 @@ export function buildToolAliasesSource(routes = loadLegacyRoutes()) { return stableStringify(aliases) } -export function buildRedirectsSource(routes = loadLegacyRoutes()) { - const lines = routes.flatMap((route) => { - if (!REDIRECT_STATUSES.has(route.status)) return [] - return LOCALES.map((locale) => `/${locale}/${route.sourceSlug} /${locale}/${route.targetSlug} ${route.status}`) - }) +export function loadLegacyTaxonomyRedirects() { + if (!fs.existsSync(LEGACY_TAXONOMY_REDIRECTS_PATH)) return {} + const redirects = JSON.parse(readText(LEGACY_TAXONOMY_REDIRECTS_PATH)) + if (!redirects || typeof redirects !== "object" || Array.isArray(redirects)) { + throw new Error("[legacy-routes] legacy-taxonomy-redirects.json must be an object") + } + + const validated = {} + for (const [sourceSlug, targetSlug] of Object.entries(redirects)) { + if (typeof sourceSlug !== "string" || !sourceSlug.trim()) { + throw new Error("[legacy-routes] taxonomy source slug is required") + } + if (typeof targetSlug !== "string" || !targetSlug.trim()) { + throw new Error(`[legacy-routes] ${sourceSlug}: taxonomy target slug is required`) + } + if (sourceSlug === targetSlug) { + throw new Error(`[legacy-routes] ${sourceSlug}: taxonomy targetSlug must differ from sourceSlug`) + } + validated[sourceSlug] = targetSlug + } + + return validated +} + +function buildRedirectLines(routes, taxonomyRedirects = loadLegacyTaxonomyRedirects()) { + return [ + ...routes.flatMap((route) => { + if (!REDIRECT_STATUSES.has(route.status)) return [] + return LOCALES.map((locale) => `/${locale}/${route.sourceSlug} /${locale}/${route.targetSlug} ${route.status}`) + }), + ...Object.entries(taxonomyRedirects).flatMap(([sourceSlug, targetSlug]) => ( + LOCALES.map((locale) => `/${locale}/${sourceSlug} /${locale}/${targetSlug} 301`) + )), + ] +} + +export function buildRedirectsSource(routes = loadLegacyRoutes(), taxonomyRedirects = loadLegacyTaxonomyRedirects()) { + const lines = buildRedirectLines(routes, taxonomyRedirects) return `${lines.join("\n")}\n` } export function checkGeneratedLegacyRoutes() { const routes = loadLegacyRoutes() + const taxonomyRedirects = loadLegacyTaxonomyRedirects() const expectedAliases = buildToolAliasesSource(routes) - const expectedRedirects = buildRedirectsSource(routes) + const expectedRedirects = buildRedirectsSource(routes, taxonomyRedirects) const problems = [] if (!fs.existsSync(TOOL_ALIASES_PATH) || readText(TOOL_ALIASES_PATH) !== expectedAliases) { @@ -91,13 +126,14 @@ export function checkGeneratedLegacyRoutes() { process.exit(1) } - return routes + return { routes, taxonomyRedirects } } function writeGeneratedFiles() { const routes = loadLegacyRoutes() + const taxonomyRedirects = loadLegacyTaxonomyRedirects() fs.writeFileSync(TOOL_ALIASES_PATH, buildToolAliasesSource(routes)) - fs.writeFileSync(REDIRECTS_PATH, buildRedirectsSource(routes)) + fs.writeFileSync(REDIRECTS_PATH, buildRedirectsSource(routes, taxonomyRedirects)) console.log(`[generate:legacy-routes] wrote ${path.relative(ROOT, TOOL_ALIASES_PATH)}`) console.log(`[generate:legacy-routes] wrote ${path.relative(ROOT, REDIRECTS_PATH)}`) } diff --git a/src/app/[lang]/about/page.tsx b/src/app/[lang]/about/page.tsx index 059231d9..d981ac73 100644 --- a/src/app/[lang]/about/page.tsx +++ b/src/app/[lang]/about/page.tsx @@ -21,7 +21,7 @@ export default function AboutPage() {

{p.about_intro}

-
+
{highlights.map((item) => (
}): Promise { const { lang } = await params @@ -13,12 +14,18 @@ export async function generateMetadata({ params }: { params: Promise<{ lang: str const locale = lang const t = getTranslation(locale) - return buildStaticPageMetadata({ + return { + ...buildStaticPageMetadata({ lang: locale, slug: "all-tools", title: t.common.all_tools, description: t.site.explore_by_category_subtitle, - }) + }), + alternates: { + canonical: buildCanonicalUrl(locale, "all-tools"), + languages: buildLocalizedAlternates({ slug: "all-tools" }), + }, + } } export default function Layout({ children }: { children: React.ReactNode }) { diff --git a/src/app/[lang]/all-tools/page.tsx b/src/app/[lang]/all-tools/page.tsx index 36cfe6df..800cf16b 100644 --- a/src/app/[lang]/all-tools/page.tsx +++ b/src/app/[lang]/all-tools/page.tsx @@ -5,6 +5,7 @@ import { getMenuGroups } from "@/core/registry/menu-groups" import { getToolRegistryStats } from "@/core/registry/stats" import { TOOL_CAPABILITY_LABELS, TOOL_FAMILY_LABELS, type ToolCapability, type ToolFamily } from "@/core/registry" import { AllToolsDiscovery } from "@/features/tool-discovery/all-tools-discovery" +import { AllToolsQueryRobots } from "@/features/tool-discovery/all-tools-query-robots" const POPULAR_DISCOVERY_TAGS = [ "json", @@ -92,6 +93,7 @@ export default async function AllToolsPage({ params }: { params: Promise<{ lang: })) return (
+

{t.common.all_tools} diff --git a/src/app/[lang]/convert-encode/layout.tsx b/src/app/[lang]/convert-encode/layout.tsx index fcf0e1a0..7e985548 100644 --- a/src/app/[lang]/convert-encode/layout.tsx +++ b/src/app/[lang]/convert-encode/layout.tsx @@ -3,8 +3,10 @@ import type { Metadata } from "next" import { isValidLocale } from "@/core/i18n/i18n" import { getTranslation } from "@/core/i18n/translations/catalog" import { buildHubMetadata } from "@/core/seo/seo" +import { buildCanonicalUrl, buildLocalizedAlternates } from "@/core/seo/urls" const SLUG = "convert-encode" +const CANONICAL_SLUG = "all-tools" export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }): Promise { const { lang } = await params @@ -14,12 +16,22 @@ export async function generateMetadata({ params }: { params: Promise<{ lang: str const locale = lang const t = getTranslation(locale) - return buildHubMetadata({ + return { + ...buildHubMetadata({ lang: locale, slug: SLUG, title: t.nav.convert_encode, description: t.categories.convert_encode_desc, - }) + }), + robots: { + index: false, + follow: true, + }, + alternates: { + canonical: buildCanonicalUrl(locale, CANONICAL_SLUG), + languages: buildLocalizedAlternates({ slug: CANONICAL_SLUG }), + }, + } } export default function Layout({ children }: { children: React.ReactNode }) { diff --git a/src/app/[lang]/design-media/layout.tsx b/src/app/[lang]/design-media/layout.tsx index c89d8e8b..bc86ecb7 100644 --- a/src/app/[lang]/design-media/layout.tsx +++ b/src/app/[lang]/design-media/layout.tsx @@ -3,8 +3,10 @@ import type { Metadata } from "next" import { isValidLocale } from "@/core/i18n/i18n" import { getTranslation } from "@/core/i18n/translations/catalog" import { buildHubMetadata } from "@/core/seo/seo" +import { buildCanonicalUrl, buildLocalizedAlternates } from "@/core/seo/urls" const SLUG = "design-media" +const CANONICAL_SLUG = "images-svg-css" export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }): Promise { const { lang } = await params @@ -14,12 +16,22 @@ export async function generateMetadata({ params }: { params: Promise<{ lang: str const locale = lang const t = getTranslation(locale) - return buildHubMetadata({ + return { + ...buildHubMetadata({ lang: locale, slug: SLUG, title: t.nav.design_media, description: t.categories.design_media_desc, - }) + }), + robots: { + index: false, + follow: true, + }, + alternates: { + canonical: buildCanonicalUrl(locale, CANONICAL_SLUG), + languages: buildLocalizedAlternates({ slug: CANONICAL_SLUG }), + }, + } } export default function Layout({ children }: { children: React.ReactNode }) { diff --git a/src/app/[lang]/web-api/layout.tsx b/src/app/[lang]/web-api/layout.tsx index 7c4af085..399268c9 100644 --- a/src/app/[lang]/web-api/layout.tsx +++ b/src/app/[lang]/web-api/layout.tsx @@ -3,8 +3,10 @@ import type { Metadata } from "next" import { isValidLocale } from "@/core/i18n/i18n" import { getTranslation } from "@/core/i18n/translations/catalog" import { buildHubMetadata } from "@/core/seo/seo" +import { buildCanonicalUrl, buildLocalizedAlternates } from "@/core/seo/urls" const SLUG = "web-api" +const CANONICAL_SLUG = "web-api-network" export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }): Promise { const { lang } = await params @@ -14,12 +16,22 @@ export async function generateMetadata({ params }: { params: Promise<{ lang: str const locale = lang const t = getTranslation(locale) - return buildHubMetadata({ + return { + ...buildHubMetadata({ lang: locale, slug: SLUG, title: t.nav.web_api, description: t.categories.web_api_desc, - }) + }), + robots: { + index: false, + follow: true, + }, + alternates: { + canonical: buildCanonicalUrl(locale, CANONICAL_SLUG), + languages: buildLocalizedAlternates({ slug: CANONICAL_SLUG }), + }, + } } export default function Layout({ children }: { children: React.ReactNode }) { diff --git a/src/app/globals.css b/src/app/globals.css index 4bff46a6..bee6b181 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -104,6 +104,8 @@ } body { + min-width: 0; + overflow-x: clip; font-family: var(--font-sans); background-color: var(--color-background); color: var(--color-foreground); diff --git a/src/app/robots.ts b/src/app/robots.ts index 46393e9d..cddc7077 100644 --- a/src/app/robots.ts +++ b/src/app/robots.ts @@ -9,6 +9,16 @@ export default function robots(): MetadataRoute.Robots { disallow: [ '*?handoff=', '*?handoff_ref=', + '/*/all-tools?search=', + '/*/all-tools?category=', + '/*/all-tools?execution=', + '/*/all-tools?family=', + '/*/all-tools?input=', + '/*/all-tools?tag=', + '/*/all-tools?tags=', + '/*/all-tools?capability=', + '/*/all-tools?use=', + '/*/all-tools?useCase=', ], }, sitemap: 'https://byteflow.tools/sitemap.xml', diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 91dd3ac7..2bf0360c 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -1,13 +1,15 @@ import type { MetadataRoute } from "next" import { LOCALES } from "@/core/i18n/i18n" import { TOOL_REGISTRY } from "@/core/registry" +import { LEGACY_TAXONOMY_SLUGS } from "@/core/routing/seo-route-policy" import { SITE_URL, buildCanonicalUrl, buildLocalizedAlternates } from "@/core/seo/urls" import routeGroups from "@/lib/sitemap-route-groups.json" import sitemapLastmod from "@/lib/sitemap-lastmod.json" export const dynamic = "force-static" -const HUB_SLUGS = Array.from(new Set(routeGroups.hubSlugs)) +const LEGACY_TAXONOMY_SLUG_SET = new Set(LEGACY_TAXONOMY_SLUGS) +const HUB_SLUGS = Array.from(new Set(routeGroups.hubSlugs)).filter((slug) => !LEGACY_TAXONOMY_SLUG_SET.has(slug)) const STATIC_SLUGS = routeGroups.staticSlugs const NOINDEX_STATIC_SLUGS = new Set(["about", "pricing", "terms"]) const SITEMAP_STATIC_SLUGS = STATIC_SLUGS.filter((slug) => !NOINDEX_STATIC_SLUGS.has(slug)) diff --git a/src/core/i18n/translations/de.json b/src/core/i18n/translations/de.json index 60116020..ccbf8999 100644 --- a/src/core/i18n/translations/de.json +++ b/src/core/i18n/translations/de.json @@ -417,7 +417,20 @@ "use_case_format": "Formatieren/validieren", "use_case_image": "Bild/Design", "use_case_security": "Sicherheit", - "use_case_workflow": "Arbeitsablauf" + "use_case_workflow": "Arbeitsablauf", + "sensitive_warning_label": "Warnung zu sensiblen Eingaben", + "sensitive_warning_title": "Sensible Eingaben bleiben in Ihrem Browser", + "sensitive_warning_base": "byteflow.tools lädt Tool-Nutzdaten standardmäßig nicht hoch, speichert sie nicht, protokolliert sie nicht und analysiert sie nicht auf einem Server.", + "sensitive_warning_default_note": "Fügen Sie nur Daten ein, die Sie prüfen dürfen. Produktionsgeheimnisse sollten vor der Nutzung lokal verifiziert werden.", + "sensitive_warning_token_note": "Dekodieren zeigt Token-Inhalte, beweist aber nicht, dass ein Token gültig, vertrauenswürdig oder sicher nutzbar ist.", + "sensitive_warning_secret_note": "Geheimnisse werden lokal verarbeitet und standardmäßig nicht gespeichert. Verwenden Sie Produktionsschlüssel nur, wenn Sie die Browser-Grenze geprüft haben.", + "sensitive_warning_certificate_note": "Zertifikatsanalyse ist lokal. Sie ersetzt keine Trust-Chain-, Widerrufs- oder Richtlinienprüfung.", + "sensitive_warning_log_note": "Logs und HAR-Dateien enthalten oft Zugangsdaten, Cookies, URLs und personenbezogene Daten. Prüfen Sie die Redaktion, bevor Sie Ausgaben teilen.", + "sensitive_warning_request_note": "Dieser Builder erzeugt nur Code. Er sendet keine Anfrage und leitet keine Zugangsdaten über byteflow.tools weiter.", + "sensitive_warning_privacy_policy": "Datenschutzrichtlinie", + "sensitive_warning_verify_devtools": "In DevTools prüfen", + "reveal_secret": "Geheimnis anzeigen", + "hide_secret": "Geheimnis ausblenden" }, "tools": { "json_formatter": { @@ -2602,7 +2615,39 @@ "template_log_scrub_before_sharing_description": "Normalisiert eingefügte Logs und maskiert Tokens, E-Mails, IPs und Zugangsdaten vor dem Teilen.", "template_step_decode_jwt_payload": "JWT-Payload dekodieren", "step_io_hint": "{input}-Eingabe -> {output}-Ausgabe", - "external_request_step_notice": "Externer Request-Schritt: Netzwerkziel vor dem Ausführen prüfen." + "external_request_step_notice": "Externer Request-Schritt: Netzwerkziel vor dem Ausführen prüfen.", + "no_steps_title": "Mit einem lokalen Schritt starten", + "drag_handle": "Schritt per Drag-and-drop sortieren", + "step_reordered_announcement": "{label} wurde auf Position {position} von {total} verschoben.", + "onboarding_title": "Recipe in drei Schritten bauen", + "onboarding_description": "Ein Recipe führt die Anfangseingabe von oben nach unten durch jeden Schritt und zeigt danach Ausgabe und Laufprotokoll.", + "onboarding_step_sample_title": "Beispiel laden", + "onboarding_step_sample_body": "Starten Sie mit einem eingebauten Recipe, damit Ablauf und Beispieleingabe sofort sichtbar sind.", + "onboarding_step_edit_title": "Schritte bearbeiten", + "onboarding_step_edit_body": "Fügen Sie Schritte hinzu, entfernen oder sortieren Sie sie und passen Sie öffentliche Optionen an.", + "onboarding_step_run_title": "Ausführen und prüfen", + "onboarding_step_run_body": "Lokal ausführen, Ausgabe prüfen und den Status jedes Schritts im Protokoll ansehen.", + "onboarding_run_sample": "Beispiel-Recipe ausführen", + "onboarding_local_only": "Kein Konto, keine Cloud-Historie, keine Payload-Speicherung", + "onboarding_dismiss": "Einführung schließen", + "privacy_preview_title": "Datenschutzvorschau", + "privacy_preview_save": "Speichert nur die Workflow-Struktur lokal in IndexedDB.", + "privacy_preview_export": "Exportiert eine JSON-Recipe-Datei nur mit Struktur.", + "privacy_preview_share": "Kopiert eine Share-URL, die nur Workflow-Struktur enthält.", + "privacy_preview_included": "Enthalten", + "privacy_preview_excluded": "Ausgeschlossen", + "privacy_preview_confirm_save": "Nur Struktur speichern", + "privacy_preview_confirm_export": "Nur Struktur exportieren", + "privacy_preview_confirm_share": "Struktur-URL kopieren", + "privacy_preview_cancel": "Abbrechen", + "privacy_scope_recipe_metadata": "Rezeptname und Beschreibung", + "privacy_scope_steps_options": "Schrittreihenfolge, Tool-IDs, Labels und öffentliche Optionen", + "privacy_scope_settings": "Sicherheitseinstellungen wie Stop-on-Error und lokale Byte-Limits", + "privacy_scope_runtime_input": "Ursprüngliche Laufzeiteingabe", + "privacy_scope_outputs": "Endausgabe und Zwischenoutputs", + "privacy_scope_logs": "Ausführungslogs, Diagnosen und Fehler", + "privacy_scope_files": "Hochgeladene Dateiinhalte", + "privacy_scope_constants": "Konstante Schritteingaben, Tokens, Schlüssel und Nutzdaten" }, "saml_decoder": { "title": "SAML-Dekoder", diff --git a/src/core/i18n/translations/en.json b/src/core/i18n/translations/en.json index bcac578a..6ad71b7f 100644 --- a/src/core/i18n/translations/en.json +++ b/src/core/i18n/translations/en.json @@ -417,7 +417,20 @@ "use_case_format": "Format/validate", "use_case_image": "Image/design", "use_case_security": "Security", - "use_case_workflow": "Workflow" + "use_case_workflow": "Workflow", + "sensitive_warning_label": "Sensitive input warning", + "sensitive_warning_title": "Sensitive input stays in your browser", + "sensitive_warning_base": "byteflow.tools does not upload, store, log, or analyze tool payloads on a server by default.", + "sensitive_warning_default_note": "Paste only data you are allowed to inspect. Production secrets should be verified locally before use.", + "sensitive_warning_token_note": "Decoding shows token contents but is not proof that a token is valid, trusted, or safe to use.", + "sensitive_warning_secret_note": "Secrets are processed locally and are not saved by default. Avoid using production keys unless you have verified the browser boundary.", + "sensitive_warning_certificate_note": "Certificate parsing is local. It does not replace trust-chain, revocation, or policy validation.", + "sensitive_warning_log_note": "Logs and HAR files often contain credentials, cookies, URLs, and personal data. Review redaction before sharing output.", + "sensitive_warning_request_note": "This builder generates code only. It does not send the request or proxy credentials through byteflow.tools.", + "sensitive_warning_privacy_policy": "Privacy policy", + "sensitive_warning_verify_devtools": "Verify in DevTools", + "reveal_secret": "Reveal secret", + "hide_secret": "Hide secret" }, "categories": { "formatters_desc": "Format, beautify, and transform code and data files including JSON, XML, SQL, HTML, CSS, and YAML.", @@ -2622,7 +2635,39 @@ "template_log_scrub_before_sharing_description": "Normalize pasted logs, then redact tokens, emails, IPs, and credentials before sharing.", "template_step_decode_jwt_payload": "Decode JWT payload", "step_io_hint": "{input} input -> {output} output", - "external_request_step_notice": "External request step: confirm the network target before running." + "external_request_step_notice": "External request step: confirm the network target before running.", + "no_steps_title": "Start with one local step", + "drag_handle": "Drag to reorder step", + "step_reordered_announcement": "{label} moved to position {position} of {total}.", + "onboarding_title": "Build a recipe in three moves", + "onboarding_description": "A recipe passes the initial input through each step from top to bottom, then shows the final output and run log.", + "onboarding_step_sample_title": "Load a sample", + "onboarding_step_sample_body": "Start from a built-in recipe so the flow and sample input are visible immediately.", + "onboarding_step_edit_title": "Edit the steps", + "onboarding_step_edit_body": "Add, remove, reorder, or tune public options before running the recipe.", + "onboarding_step_run_title": "Run and inspect", + "onboarding_step_run_body": "Run locally, review the final output, and check per-step status in the log.", + "onboarding_run_sample": "Run sample recipe", + "onboarding_local_only": "No account, no cloud history, no payload storage", + "onboarding_dismiss": "Dismiss onboarding", + "privacy_preview_title": "Privacy preview", + "privacy_preview_save": "Save this recipe as workflow structure only in local IndexedDB.", + "privacy_preview_export": "Export a structure-only JSON recipe file.", + "privacy_preview_share": "Copy a share URL that contains workflow structure only.", + "privacy_preview_included": "Included", + "privacy_preview_excluded": "Excluded", + "privacy_preview_confirm_save": "Save structure only", + "privacy_preview_confirm_export": "Export structure only", + "privacy_preview_confirm_share": "Copy structure-only URL", + "privacy_preview_cancel": "Cancel", + "privacy_scope_recipe_metadata": "Recipe name and description", + "privacy_scope_steps_options": "Step order, tool IDs, labels, and public options", + "privacy_scope_settings": "Safety settings such as stop-on-error and local byte limits", + "privacy_scope_runtime_input": "Initial runtime input", + "privacy_scope_outputs": "Final output and intermediate step outputs", + "privacy_scope_logs": "Run logs, diagnostics, and errors", + "privacy_scope_files": "Uploaded file contents", + "privacy_scope_constants": "Constant step inputs, tokens, keys, and payloads" }, "saml_decoder": { "title": "SAML Decoder", diff --git a/src/core/i18n/translations/fr.json b/src/core/i18n/translations/fr.json index 2307f9a0..85c23621 100644 --- a/src/core/i18n/translations/fr.json +++ b/src/core/i18n/translations/fr.json @@ -417,7 +417,20 @@ "use_case_format": "Formater/valider", "use_case_image": "Image et design", "use_case_security": "Sécurité", - "use_case_workflow": "Flux de travail" + "use_case_workflow": "Flux de travail", + "sensitive_warning_label": "Avertissement sur les saisies sensibles", + "sensitive_warning_title": "Les saisies sensibles restent dans votre navigateur", + "sensitive_warning_base": "byteflow.tools ne téléverse, ne stocke, ne journalise et n'analyse pas les charges utiles des outils sur un serveur par défaut.", + "sensitive_warning_default_note": "Collez uniquement les données que vous êtes autorisé à inspecter. Les secrets de production doivent être vérifiés localement avant usage.", + "sensitive_warning_token_note": "Le décodage affiche le contenu du jeton, mais ne prouve pas qu'il est valide, fiable ou sûr à utiliser.", + "sensitive_warning_secret_note": "Les secrets sont traités localement et ne sont pas enregistrés par défaut. Évitez les clés de production sans avoir vérifié la limite du navigateur.", + "sensitive_warning_certificate_note": "L'analyse de certificat est locale. Elle ne remplace pas la validation de chaîne de confiance, de révocation ou de politique.", + "sensitive_warning_log_note": "Les logs et fichiers HAR contiennent souvent des identifiants, cookies, URL et données personnelles. Vérifiez la rédaction avant de partager la sortie.", + "sensitive_warning_request_note": "Ce constructeur génère seulement du code. Il n'envoie pas la requête et ne relaie pas les identifiants via byteflow.tools.", + "sensitive_warning_privacy_policy": "Politique de confidentialité", + "sensitive_warning_verify_devtools": "Vérifier dans DevTools", + "reveal_secret": "Afficher le secret", + "hide_secret": "Masquer le secret" }, "tools": { "json_formatter": { @@ -2602,7 +2615,39 @@ "template_log_scrub_before_sharing_description": "Normalise les logs collés, puis masque tokens, e-mails, IP et identifiants avant partage.", "template_step_decode_jwt_payload": "Décoder le payload JWT", "step_io_hint": "Entrée {input} -> sortie {output}", - "external_request_step_notice": "Étape de requête externe : vérifiez la cible réseau avant exécution." + "external_request_step_notice": "Étape de requête externe : vérifiez la cible réseau avant exécution.", + "no_steps_title": "Commencer par une étape locale", + "drag_handle": "Glisser pour réordonner l’étape", + "step_reordered_announcement": "{label} déplacé en position {position} sur {total}.", + "onboarding_title": "Construire un recipe en trois gestes", + "onboarding_description": "Un recipe transmet l’entrée initiale à chaque étape de haut en bas, puis affiche la sortie finale et le journal.", + "onboarding_step_sample_title": "Charger un exemple", + "onboarding_step_sample_body": "Commencez avec un recipe intégré pour voir immédiatement le flux et l’entrée exemple.", + "onboarding_step_edit_title": "Modifier les étapes", + "onboarding_step_edit_body": "Ajoutez, retirez, réordonnez ou ajustez les options publiques avant l’exécution.", + "onboarding_step_run_title": "Exécuter et inspecter", + "onboarding_step_run_body": "Exécutez localement, vérifiez la sortie finale et consultez le statut de chaque étape.", + "onboarding_run_sample": "Exécuter le recipe exemple", + "onboarding_local_only": "Sans compte, sans historique cloud, sans stockage de payload", + "onboarding_dismiss": "Fermer l’aide de démarrage", + "privacy_preview_title": "Aperçu confidentialité", + "privacy_preview_save": "Enregistre uniquement la structure du workflow dans IndexedDB local.", + "privacy_preview_export": "Exporte un fichier JSON de recipe contenant uniquement la structure.", + "privacy_preview_share": "Copie une URL de partage contenant uniquement la structure du workflow.", + "privacy_preview_included": "Inclus", + "privacy_preview_excluded": "Exclu", + "privacy_preview_confirm_save": "Enregistrer la structure seule", + "privacy_preview_confirm_export": "Exporter la structure seule", + "privacy_preview_confirm_share": "Copier l’URL structure seule", + "privacy_preview_cancel": "Annuler", + "privacy_scope_recipe_metadata": "Nom et description de la recette", + "privacy_scope_steps_options": "Ordre des étapes, ID des outils, libellés et options publiques", + "privacy_scope_settings": "Paramètres de sécurité comme l'arrêt sur erreur et les limites locales en octets", + "privacy_scope_runtime_input": "Entrée d'exécution initiale", + "privacy_scope_outputs": "Sortie finale et sorties intermédiaires", + "privacy_scope_logs": "Journaux d'exécution, diagnostics et erreurs", + "privacy_scope_files": "Contenu des fichiers téléversés", + "privacy_scope_constants": "Entrées constantes d'étape, jetons, clés et charges utiles" }, "saml_decoder": { "title": "Décodeur SAML", diff --git a/src/core/i18n/translations/ja.json b/src/core/i18n/translations/ja.json index f5c22512..cdafdecc 100644 --- a/src/core/i18n/translations/ja.json +++ b/src/core/i18n/translations/ja.json @@ -417,7 +417,20 @@ "use_case_format": "整形/検証", "use_case_image": "画像/デザイン", "use_case_security": "セキュリティ", - "use_case_workflow": "作業フロー" + "use_case_workflow": "作業フロー", + "sensitive_warning_label": "機密入力の注意", + "sensitive_warning_title": "機密入力はブラウザー内に残ります", + "sensitive_warning_base": "byteflow.tools は既定でツールのペイロードをサーバーへアップロード、保存、記録、分析しません。", + "sensitive_warning_default_note": "検査する権限があるデータだけを貼り付けてください。本番シークレットは使用前にローカル境界を確認してください。", + "sensitive_warning_token_note": "デコードはトークン内容を表示するだけで、有効性、信頼性、安全性を証明するものではありません。", + "sensitive_warning_secret_note": "シークレットはローカルで処理され、既定では保存されません。ブラウザー境界を確認していない場合は本番キーを避けてください。", + "sensitive_warning_certificate_note": "証明書の解析はローカルで行われますが、信頼チェーン、失効状態、ポリシー検証の代替にはなりません。", + "sensitive_warning_log_note": "ログや HAR ファイルには資格情報、Cookie、URL、個人データが含まれることがあります。共有前にマスク結果を確認してください。", + "sensitive_warning_request_note": "このビルダーはコードを生成するだけです。リクエスト送信や byteflow.tools 経由の資格情報プロキシは行いません。", + "sensitive_warning_privacy_policy": "プライバシーポリシー", + "sensitive_warning_verify_devtools": "DevTools で確認", + "reveal_secret": "シークレットを表示", + "hide_secret": "シークレットを隠す" }, "tools": { "json_formatter": { @@ -2602,7 +2615,39 @@ "template_log_scrub_before_sharing_description": "貼り付けたログを正規化し、共有前に token、メール、IP、認証情報をマスクします。", "template_step_decode_jwt_payload": "JWT payload をデコード", "step_io_hint": "{input} 入力 -> {output} 出力", - "external_request_step_notice": "外部リクエストステップ: 実行前にネットワーク先を確認してください。" + "external_request_step_notice": "外部リクエストステップ: 実行前にネットワーク先を確認してください。", + "no_steps_title": "ローカルステップを1つ追加", + "drag_handle": "ドラッグしてステップを並べ替え", + "step_reordered_announcement": "{label} を {total} 件中 {position} 番目へ移動しました。", + "onboarding_title": "3ステップで recipe を作成", + "onboarding_description": "Recipe は初期入力を上から順に各ステップへ渡し、最終出力と実行ログを表示します。", + "onboarding_step_sample_title": "サンプルを読み込む", + "onboarding_step_sample_body": "内蔵 recipe から始めると、流れとサンプル入力をすぐ確認できます。", + "onboarding_step_edit_title": "ステップを編集", + "onboarding_step_edit_body": "実行前にステップの追加、削除、並べ替え、公開オプション調整ができます。", + "onboarding_step_run_title": "実行して確認", + "onboarding_step_run_body": "ローカルで実行し、最終出力と各ステップの状態をログで確認します。", + "onboarding_run_sample": "サンプル recipe を実行", + "onboarding_local_only": "アカウントなし、クラウド履歴なし、payload 保存なし", + "onboarding_dismiss": "オンボーディングを閉じる", + "privacy_preview_title": "プライバシープレビュー", + "privacy_preview_save": "recipe 構造だけをローカル IndexedDB に保存します。", + "privacy_preview_export": "構造だけの JSON recipe ファイルを書き出します。", + "privacy_preview_share": "ワークフロー構造だけを含む共有 URL をコピーします。", + "privacy_preview_included": "含まれるもの", + "privacy_preview_excluded": "除外されるもの", + "privacy_preview_confirm_save": "構造だけを保存", + "privacy_preview_confirm_export": "構造だけを書き出す", + "privacy_preview_confirm_share": "構造のみの URL をコピー", + "privacy_preview_cancel": "キャンセル", + "privacy_scope_recipe_metadata": "レシピ名と説明", + "privacy_scope_steps_options": "ステップ順序、ツール ID、ラベル、公開オプション", + "privacy_scope_settings": "エラー時停止やローカルバイト上限などの安全設定", + "privacy_scope_runtime_input": "初期実行入力", + "privacy_scope_outputs": "最終出力と中間ステップ出力", + "privacy_scope_logs": "実行ログ、診断、エラー", + "privacy_scope_files": "アップロードされたファイル内容", + "privacy_scope_constants": "固定ステップ入力、トークン、キー、ペイロード" }, "saml_decoder": { "title": "SAML デコーダー", diff --git a/src/core/i18n/translations/ko.json b/src/core/i18n/translations/ko.json index b388b72a..a5cd1a5d 100644 --- a/src/core/i18n/translations/ko.json +++ b/src/core/i18n/translations/ko.json @@ -417,7 +417,20 @@ "use_case_format": "포맷/검증", "use_case_image": "이미지/디자인", "use_case_security": "보안", - "use_case_workflow": "작업 흐름" + "use_case_workflow": "작업 흐름", + "sensitive_warning_label": "민감한 입력 경고", + "sensitive_warning_title": "민감한 입력은 브라우저에만 남습니다", + "sensitive_warning_base": "byteflow.tools는 기본적으로 도구 페이로드를 서버에 업로드, 저장, 기록 또는 분석하지 않습니다.", + "sensitive_warning_default_note": "검토할 권한이 있는 데이터만 붙여 넣으세요. 운영 환경 비밀값은 로컬 경계를 확인한 뒤 사용하세요.", + "sensitive_warning_token_note": "디코딩은 토큰 내용을 보여줄 뿐이며 토큰의 유효성, 신뢰성 또는 안전성을 증명하지 않습니다.", + "sensitive_warning_secret_note": "비밀값은 로컬에서 처리되며 기본적으로 저장되지 않습니다. 브라우저 경계를 확인하지 않았다면 운영 키 사용을 피하세요.", + "sensitive_warning_certificate_note": "인증서 파싱은 로컬에서 수행되지만 신뢰 체인, 폐기 상태 또는 정책 검증을 대체하지 않습니다.", + "sensitive_warning_log_note": "로그와 HAR 파일에는 자격 증명, 쿠키, URL, 개인 데이터가 포함될 수 있습니다. 출력을 공유하기 전에 삭제 결과를 확인하세요.", + "sensitive_warning_request_note": "이 빌더는 코드만 생성합니다. 요청을 보내거나 byteflow.tools를 통해 자격 증명을 프록시하지 않습니다.", + "sensitive_warning_privacy_policy": "개인정보 처리방침", + "sensitive_warning_verify_devtools": "DevTools에서 확인", + "reveal_secret": "비밀값 표시", + "hide_secret": "비밀값 숨기기" }, "tools": { "json_formatter": { @@ -2602,7 +2615,39 @@ "template_log_scrub_before_sharing_description": "붙여넣은 로그를 정규화한 뒤 공유 전에 token, 이메일, IP, 자격 증명을 마스킹합니다.", "template_step_decode_jwt_payload": "JWT payload 디코드", "step_io_hint": "{input} 입력 -> {output} 출력", - "external_request_step_notice": "외부 요청 단계: 실행 전에 네트워크 대상을 확인하세요." + "external_request_step_notice": "외부 요청 단계: 실행 전에 네트워크 대상을 확인하세요.", + "no_steps_title": "로컬 단계 하나로 시작", + "drag_handle": "드래그하여 단계 순서 변경", + "step_reordered_announcement": "{label} 단계가 전체 {total}개 중 {position}번째로 이동했습니다.", + "onboarding_title": "세 단계로 recipe 만들기", + "onboarding_description": "Recipe 는 초기 입력을 위에서 아래로 각 단계에 전달한 뒤 최종 출력과 실행 로그를 보여줍니다.", + "onboarding_step_sample_title": "샘플 불러오기", + "onboarding_step_sample_body": "내장 recipe 로 시작하면 흐름과 샘플 입력을 바로 볼 수 있습니다.", + "onboarding_step_edit_title": "단계 편집", + "onboarding_step_edit_body": "실행 전에 단계를 추가, 삭제, 재정렬하거나 공개 옵션을 조정합니다.", + "onboarding_step_run_title": "실행하고 확인", + "onboarding_step_run_body": "로컬에서 실행하고 최종 출력과 단계별 상태를 로그에서 확인합니다.", + "onboarding_run_sample": "샘플 recipe 실행", + "onboarding_local_only": "계정 없음, 클라우드 기록 없음, payload 저장 없음", + "onboarding_dismiss": "온보딩 닫기", + "privacy_preview_title": "개인정보 미리보기", + "privacy_preview_save": "워크플로 구조만 로컬 IndexedDB 에 저장합니다.", + "privacy_preview_export": "구조만 담은 JSON recipe 파일을 내보냅니다.", + "privacy_preview_share": "워크플로 구조만 포함한 공유 URL 을 복사합니다.", + "privacy_preview_included": "포함됨", + "privacy_preview_excluded": "제외됨", + "privacy_preview_confirm_save": "구조만 저장", + "privacy_preview_confirm_export": "구조만 내보내기", + "privacy_preview_confirm_share": "구조 전용 URL 복사", + "privacy_preview_cancel": "취소", + "privacy_scope_recipe_metadata": "레시피 이름과 설명", + "privacy_scope_steps_options": "단계 순서, 도구 ID, 라벨, 공개 옵션", + "privacy_scope_settings": "오류 시 중지와 로컬 바이트 제한 같은 안전 설정", + "privacy_scope_runtime_input": "초기 실행 입력", + "privacy_scope_outputs": "최종 출력과 중간 단계 출력", + "privacy_scope_logs": "실행 로그, 진단, 오류", + "privacy_scope_files": "업로드된 파일 내용", + "privacy_scope_constants": "고정 단계 입력, 토큰, 키, 페이로드" }, "saml_decoder": { "title": "SAML 디코더", diff --git a/src/core/i18n/translations/zh-CN.json b/src/core/i18n/translations/zh-CN.json index 54c99a2f..cb587368 100644 --- a/src/core/i18n/translations/zh-CN.json +++ b/src/core/i18n/translations/zh-CN.json @@ -417,7 +417,20 @@ "use_case_format": "格式化/校验", "use_case_image": "图片/设计", "use_case_security": "安全", - "use_case_workflow": "工作流" + "use_case_workflow": "工作流", + "sensitive_warning_label": "敏感输入提醒", + "sensitive_warning_title": "敏感输入会留在你的浏览器中", + "sensitive_warning_base": "byteflow.tools 默认不会把工具载荷上传、存储、记录或发送到服务器分析。", + "sensitive_warning_default_note": "只粘贴你有权检查的数据。生产环境密钥应先在本地确认边界后再使用。", + "sensitive_warning_token_note": "解码只会显示令牌内容,并不证明令牌有效、可信或可安全使用。", + "sensitive_warning_secret_note": "密钥在本地处理,默认不会保存。除非已确认浏览器本地边界,否则避免使用生产密钥。", + "sensitive_warning_certificate_note": "证书解析在本地完成,但不能替代信任链、吊销状态或策略校验。", + "sensitive_warning_log_note": "日志和 HAR 文件常包含凭证、Cookie、URL 和个人数据。分享输出前请检查脱敏结果。", + "sensitive_warning_request_note": "此构建器只生成代码,不会发送请求,也不会通过 byteflow.tools 代理凭证。", + "sensitive_warning_privacy_policy": "隐私政策", + "sensitive_warning_verify_devtools": "在 DevTools 中验证", + "reveal_secret": "显示密钥", + "hide_secret": "隐藏密钥" }, "tools": { "json_formatter": { @@ -2602,7 +2615,39 @@ "template_log_scrub_before_sharing_description": "先规范化粘贴日志,再分享前脱敏 token、邮箱、IP 和凭据。", "template_step_decode_jwt_payload": "解码 JWT payload", "step_io_hint": "{input} 输入 -> {output} 输出", - "external_request_step_notice": "外部请求步骤:运行前确认网络目标。" + "external_request_step_notice": "外部请求步骤:运行前确认网络目标。", + "no_steps_title": "从一个本地步骤开始", + "drag_handle": "拖动以重新排序步骤", + "step_reordered_announcement": "{label} 已移动到第 {position} 位,共 {total} 步。", + "onboarding_title": "三步构建 recipe", + "onboarding_description": "Recipe 会让初始输入按从上到下的顺序经过每个步骤,然后显示最终输出和运行日志。", + "onboarding_step_sample_title": "载入示例", + "onboarding_step_sample_body": "从内置 recipe 开始,立即看到流程和示例输入。", + "onboarding_step_edit_title": "编辑步骤", + "onboarding_step_edit_body": "运行前可添加、删除、重排步骤,或调整公开选项。", + "onboarding_step_run_title": "运行并检查", + "onboarding_step_run_body": "在本地运行,查看最终输出,并在日志中检查每步状态。", + "onboarding_run_sample": "运行示例 recipe", + "onboarding_local_only": "无账号、无云端历史、无载荷存储", + "onboarding_dismiss": "关闭引导", + "privacy_preview_title": "隐私预览", + "privacy_preview_save": "仅把流程结构保存到本地 IndexedDB。", + "privacy_preview_export": "导出仅含结构的 JSON recipe 文件。", + "privacy_preview_share": "复制仅包含流程结构的分享 URL。", + "privacy_preview_included": "包含", + "privacy_preview_excluded": "不包含", + "privacy_preview_confirm_save": "仅保存结构", + "privacy_preview_confirm_export": "仅导出结构", + "privacy_preview_confirm_share": "复制结构分享 URL", + "privacy_preview_cancel": "取消", + "privacy_scope_recipe_metadata": "配方名称和描述", + "privacy_scope_steps_options": "步骤顺序、工具 ID、标签和公开选项", + "privacy_scope_settings": "停止出错、 本地字节限制等安全设置", + "privacy_scope_runtime_input": "初始运行输入", + "privacy_scope_outputs": "最终输出和中间步骤输出", + "privacy_scope_logs": "运行日志、诊断信息和错误", + "privacy_scope_files": "上传的文件内容", + "privacy_scope_constants": "固定步骤输入、令牌、密钥和载荷" }, "saml_decoder": { "title": "SAML 解码器", diff --git a/src/core/i18n/translations/zh-TW.json b/src/core/i18n/translations/zh-TW.json index 183d540e..ee004939 100644 --- a/src/core/i18n/translations/zh-TW.json +++ b/src/core/i18n/translations/zh-TW.json @@ -417,7 +417,20 @@ "use_case_format": "格式化/驗證", "use_case_image": "圖片/設計", "use_case_security": "安全", - "use_case_workflow": "工作流" + "use_case_workflow": "工作流", + "sensitive_warning_label": "敏感輸入提醒", + "sensitive_warning_title": "敏感輸入會留在你的瀏覽器中", + "sensitive_warning_base": "byteflow.tools 預設不會將工具載荷上傳、儲存、記錄或送到伺服器分析。", + "sensitive_warning_default_note": "只貼上你有權檢查的資料。正式環境密鑰應先在本機確認邊界後再使用。", + "sensitive_warning_token_note": "解碼只會顯示權杖內容,並不證明權杖有效、可信或可安全使用。", + "sensitive_warning_secret_note": "密鑰在本機處理,預設不會儲存。除非已確認瀏覽器本機邊界,否則避免使用正式環境密鑰。", + "sensitive_warning_certificate_note": "憑證解析在本機完成,但不能取代信任鏈、撤銷狀態或政策驗證。", + "sensitive_warning_log_note": "日誌和 HAR 檔常包含憑證、Cookie、URL 和個人資料。分享輸出前請檢查遮蔽結果。", + "sensitive_warning_request_note": "此建構器只產生程式碼,不會送出請求,也不會透過 byteflow.tools 代理憑證。", + "sensitive_warning_privacy_policy": "隱私權政策", + "sensitive_warning_verify_devtools": "在 DevTools 中驗證", + "reveal_secret": "顯示密鑰", + "hide_secret": "隱藏密鑰" }, "tools": { "json_formatter": { @@ -2602,7 +2615,39 @@ "template_log_scrub_before_sharing_description": "先正規化貼上的日誌,再於分享前遮蔽 token、電子郵件、IP 和憑證。", "template_step_decode_jwt_payload": "解碼 JWT payload", "step_io_hint": "{input} 輸入 -> {output} 輸出", - "external_request_step_notice": "外部請求步驟:執行前確認網路目標。" + "external_request_step_notice": "外部請求步驟:執行前確認網路目標。", + "no_steps_title": "從一個本地步驟開始", + "drag_handle": "拖曳以重新排序步驟", + "step_reordered_announcement": "{label} 已移至第 {position} 位,共 {total} 步。", + "onboarding_title": "三步建立 recipe", + "onboarding_description": "Recipe 會讓初始輸入依序通過每個步驟,接著顯示最終輸出與執行記錄。", + "onboarding_step_sample_title": "載入範例", + "onboarding_step_sample_body": "從內建 recipe 開始,立即看見流程與範例輸入。", + "onboarding_step_edit_title": "編輯步驟", + "onboarding_step_edit_body": "執行前可新增、刪除、重排步驟,或調整公開選項。", + "onboarding_step_run_title": "執行並檢查", + "onboarding_step_run_body": "在本地執行,查看最終輸出,並在記錄中檢查每步狀態。", + "onboarding_run_sample": "執行範例 recipe", + "onboarding_local_only": "無帳號、無雲端歷史、無 payload 儲存", + "onboarding_dismiss": "關閉引導", + "privacy_preview_title": "隱私預覽", + "privacy_preview_save": "只將流程結構儲存在本地 IndexedDB。", + "privacy_preview_export": "匯出只含結構的 JSON recipe 檔案。", + "privacy_preview_share": "複製只包含流程結構的分享 URL。", + "privacy_preview_included": "包含", + "privacy_preview_excluded": "不包含", + "privacy_preview_confirm_save": "只儲存結構", + "privacy_preview_confirm_export": "只匯出結構", + "privacy_preview_confirm_share": "複製結構分享 URL", + "privacy_preview_cancel": "取消", + "privacy_scope_recipe_metadata": "配方名稱和描述", + "privacy_scope_steps_options": "步驟順序、工具 ID、標籤和公開選項", + "privacy_scope_settings": "停止於錯誤、本機位元組限制等安全設定", + "privacy_scope_runtime_input": "初始執行輸入", + "privacy_scope_outputs": "最終輸出和中間步驟輸出", + "privacy_scope_logs": "執行日誌、診斷資訊和錯誤", + "privacy_scope_files": "上傳的檔案內容", + "privacy_scope_constants": "固定步驟輸入、權杖、密鑰和載荷" }, "saml_decoder": { "title": "SAML 解碼器", diff --git a/src/core/routing/legacy-taxonomy-redirects.json b/src/core/routing/legacy-taxonomy-redirects.json new file mode 100644 index 00000000..406d0f73 --- /dev/null +++ b/src/core/routing/legacy-taxonomy-redirects.json @@ -0,0 +1,5 @@ +{ + "web-api": "web-api-network", + "design-media": "images-svg-css", + "convert-encode": "all-tools" +} diff --git a/src/core/routing/seo-route-policy.ts b/src/core/routing/seo-route-policy.ts new file mode 100644 index 00000000..e0a7bc0b --- /dev/null +++ b/src/core/routing/seo-route-policy.ts @@ -0,0 +1,35 @@ +import type { Locale } from "@/core/i18n/i18n" +import { SITE_URL, buildCanonicalUrl } from "@/core/seo/urls" +import legacyTaxonomyRedirects from "./legacy-taxonomy-redirects.json" + +export { SITE_URL } + +export const LEGACY_TAXONOMY_REDIRECTS = legacyTaxonomyRedirects as Readonly> + +export const LEGACY_TAXONOMY_SLUGS = Object.keys(LEGACY_TAXONOMY_REDIRECTS) + +const ALL_TOOLS_INDEX_QUERY_KEYS = new Set(["search", "category", "execution", "family", "input", "tag", "tags", "capability", "use", "useCase"]) + +export function getLegacyTaxonomyTarget(slug: string): string | null { + return LEGACY_TAXONOMY_REDIRECTS[slug] ?? null +} + +export function buildLocalizedPath(locale: Locale, slug?: string | null): string { + return new URL(buildCanonicalUrl(locale, slug), SITE_URL).pathname +} + +export function isAllToolsIndexableSearchParams(params: URLSearchParams): boolean { + for (const key of params.keys()) { + if (ALL_TOOLS_INDEX_QUERY_KEYS.has(key)) return false + } + return true +} + +export function shouldNoindexAllToolsUrl(url: string): boolean { + try { + const parsed = new URL(url, SITE_URL) + return !isAllToolsIndexableSearchParams(parsed.searchParams) + } catch { + return false + } +} diff --git a/src/features/pipeline/recipe-codec.ts b/src/features/pipeline/recipe-codec.ts index e9b81e5d..6fa03822 100644 --- a/src/features/pipeline/recipe-codec.ts +++ b/src/features/pipeline/recipe-codec.ts @@ -1,5 +1,5 @@ import type { RecipeDocument } from "./recipe-types" -import { getPipelineAdapter } from "./adapter-registry" +import { recipeHasRuntimePersistenceRisk, sanitizeRecipeForPersistence } from "./recipe-sanitizer" export type RecipeCodecResult = | { ok: true; recipe: RecipeDocument } @@ -49,43 +49,13 @@ export function decodeRecipeFromUrlParam(value: string): RecipeCodecResult { } export function recipeContainsRuntimeInput(recipe: RecipeDocument): boolean { - return recipe.steps.some((step) => step.inputMode === "constant" && typeof step.constantInput === "string" && step.constantInput.length > 0) -} - -function sanitizePortableOptions(toolKey: string, options: Record): Record { - const adapter = getPipelineAdapter(toolKey) - if (!adapter) return {} - - return Object.fromEntries( - adapter.publicOptionKeys - .filter((key) => Object.prototype.hasOwnProperty.call(options, key)) - .map((key) => [key, options[key]]), - ) + return recipeHasRuntimePersistenceRisk(recipe) } export function createPortableRecipe(recipe: RecipeDocument): RecipeDocument { - return { - ...recipe, - steps: recipe.steps.map((step) => { - const base = { - ...step, - options: sanitizePortableOptions(step.toolKey, step.options || {}), - } - - if (step.inputMode === "constant") { - const withoutInput = { ...base } - delete withoutInput.constantInput - return { - ...withoutInput, - inputMode: "previous_output" as const, - } - } - - return base - }), - } + return sanitizeRecipeForPersistence(recipe) } -export function encodeRecipeForShareUrl(recipe: RecipeDocument): string { - return encodeRecipeForUrl(createPortableRecipe(recipe)) +export function encodeRecipeForShareUrl(recipe: RecipeDocument, options: { includeRuntimeInput?: boolean } = {}): string { + return encodeRecipeForUrl(options.includeRuntimeInput ? recipe : createPortableRecipe(recipe)) } diff --git a/src/features/pipeline/recipe-import-export.ts b/src/features/pipeline/recipe-import-export.ts index e7121ae6..02e03f1a 100644 --- a/src/features/pipeline/recipe-import-export.ts +++ b/src/features/pipeline/recipe-import-export.ts @@ -1,5 +1,5 @@ import { validateRecipe } from "./executor" -import { createPortableRecipe } from "./recipe-codec" +import { sanitizeRecipeForPersistence } from "./recipe-sanitizer" import type { RecipeDocument } from "./recipe-types" export type RecipeImportResult = @@ -7,7 +7,7 @@ export type RecipeImportResult = | { ok: false; errors: string[] } export function exportRecipeToJson(recipe: RecipeDocument): string { - return `${JSON.stringify(createPortableRecipe(recipe), null, 2)}\n` + return `${JSON.stringify(sanitizeRecipeForPersistence(recipe), null, 2)}\n` } export function importRecipeFromJson(source: string): RecipeImportResult { diff --git a/src/features/pipeline/recipe-sanitizer.ts b/src/features/pipeline/recipe-sanitizer.ts new file mode 100644 index 00000000..a3527911 --- /dev/null +++ b/src/features/pipeline/recipe-sanitizer.ts @@ -0,0 +1,65 @@ +import { getPipelineAdapter } from "./adapter-registry" +import type { RecipeDocument, RecipeStep } from "./recipe-types" + +export type RecipePrivacyScope = { + included: string[] + excluded: string[] +} + +export const RECIPE_STRUCTURE_PRIVACY_SCOPE: RecipePrivacyScope = { + included: [ + "privacy_scope_recipe_metadata", + "privacy_scope_steps_options", + "privacy_scope_settings", + ], + excluded: [ + "privacy_scope_runtime_input", + "privacy_scope_outputs", + "privacy_scope_logs", + "privacy_scope_files", + "privacy_scope_constants", + ], +} + +function sanitizeOptions(toolKey: string, options: Record): Record { + const adapter = getPipelineAdapter(toolKey) + if (!adapter) return {} + + return Object.fromEntries( + adapter.publicOptionKeys + .filter((key) => Object.prototype.hasOwnProperty.call(options, key)) + .map((key) => [key, options[key]]), + ) +} + +function sanitizeStepForStructure(step: RecipeStep): RecipeStep { + const sanitized: RecipeStep = { + id: step.id, + toolKey: step.toolKey, + label: step.label, + adapterVersion: step.adapterVersion, + inputMode: "previous_output", + options: sanitizeOptions(step.toolKey, step.options || {}), + } + + if (!sanitized.label) delete sanitized.label + return sanitized +} + +export function sanitizeRecipeForPersistence(recipe: RecipeDocument): RecipeDocument { + return { + schemaVersion: recipe.schemaVersion, + id: recipe.id, + name: recipe.name, + description: recipe.description, + createdAt: recipe.createdAt, + updatedAt: recipe.updatedAt, + steps: recipe.steps.map(sanitizeStepForStructure), + edges: recipe.edges.map((edge) => ({ ...edge })), + settings: { ...recipe.settings }, + } +} + +export function recipeHasRuntimePersistenceRisk(recipe: RecipeDocument): boolean { + return recipe.steps.some((step) => step.inputMode === "constant" && typeof step.constantInput === "string" && step.constantInput.length > 0) +} diff --git a/src/features/pipeline/recipe-store.ts b/src/features/pipeline/recipe-store.ts index c70f38f6..bad89e20 100644 --- a/src/features/pipeline/recipe-store.ts +++ b/src/features/pipeline/recipe-store.ts @@ -1,5 +1,5 @@ import type { RecipeDocument } from "./recipe-types" -import { createPortableRecipe } from "./recipe-codec" +import { sanitizeRecipeForPersistence } from "./recipe-sanitizer" const DB_NAME = "byteflow-pipeline-recipes" const DB_VERSION = 1 @@ -28,12 +28,12 @@ export function createSavedRecipeRecord( metadata: Partial = {}, now = new Date().toISOString(), ): SavedRecipeRecord { - const portableRecipe = createPortableRecipe(recipe) + const safeRecipe = sanitizeRecipeForPersistence(recipe) return { - id: portableRecipe.id, - name: portableRecipe.name, - recipe: portableRecipe, - createdAt: metadata.createdAt ?? portableRecipe.createdAt ?? now, + id: safeRecipe.id, + name: safeRecipe.name, + recipe: safeRecipe, + createdAt: metadata.createdAt ?? safeRecipe.createdAt ?? now, updatedAt: now, lastRunAt: metadata.lastRunAt, pinned: metadata.pinned, diff --git a/src/features/tool-discovery/all-tools-discovery.tsx b/src/features/tool-discovery/all-tools-discovery.tsx index 759d217b..bb4f43fd 100644 --- a/src/features/tool-discovery/all-tools-discovery.tsx +++ b/src/features/tool-discovery/all-tools-discovery.tsx @@ -576,10 +576,10 @@ export function AllToolsDiscovery({ ) return ( -

-
-
-
+
+
+
+
-
+
{labels.popularTags} @@ -688,14 +688,14 @@ export function AllToolsDiscovery({
{workflows.length > 0 ? ( -
+

{labels.commonWorkflows}

{workflows.map((workflow) => ( @@ -717,7 +717,7 @@ export function AllToolsDiscovery({ {filteredGroups.length > 0 ? (
{filteredGroups.map((group) => ( -
+

{group.title}

@@ -739,10 +739,10 @@ export function AllToolsDiscovery({ -

+

{tool.title}

diff --git a/src/features/tool-discovery/all-tools-query-robots.tsx b/src/features/tool-discovery/all-tools-query-robots.tsx new file mode 100644 index 00000000..a619dc18 --- /dev/null +++ b/src/features/tool-discovery/all-tools-query-robots.tsx @@ -0,0 +1,26 @@ +"use client" + +import * as React from "react" +import { shouldNoindexAllToolsUrl } from "@/core/routing/seo-route-policy" + +const META_SELECTOR = 'meta[name="robots"][data-byteflow-all-tools-query-robots="true"]' + +export function AllToolsQueryRobots() { + React.useEffect(() => { + const shouldNoindex = shouldNoindexAllToolsUrl(window.location.href) + const existing = document.head.querySelector(META_SELECTOR) + + if (!shouldNoindex) { + existing?.remove() + return + } + + const meta = existing ?? document.createElement("meta") + meta.name = "robots" + meta.content = "noindex,follow" + meta.dataset.byteflowAllToolsQueryRobots = "true" + if (!existing) document.head.append(meta) + }, []) + + return null +} diff --git a/src/features/tool-shell/index.ts b/src/features/tool-shell/index.ts index 5a1e2a58..6386e9b2 100644 --- a/src/features/tool-shell/index.ts +++ b/src/features/tool-shell/index.ts @@ -5,6 +5,7 @@ export * from "./tool-empty-state" export * from "./tool-preview-area" export * from "./monaco-editors" export * from "./privacy-badge" +export * from "./sensitive-input-warning" export * from "./tool-privacy-footer" export * from "./tool-trust-header" export * from "./privacy-faq" diff --git a/src/features/tool-shell/sensitive-input-warning.tsx b/src/features/tool-shell/sensitive-input-warning.tsx new file mode 100644 index 00000000..4a5f3d87 --- /dev/null +++ b/src/features/tool-shell/sensitive-input-warning.tsx @@ -0,0 +1,59 @@ +"use client" + +import Link from "next/link" +import { AlertTriangle, ExternalLink, ShieldCheck } from "lucide-react" +import { useLang } from "@/core/i18n/lang-provider" +import { cn } from "@/core/utils/utils" + +type SensitiveInputWarningProps = { + className?: string + variant?: "default" | "token" | "secret" | "certificate" | "log" | "request" +} + +const VARIANT_NOTE = { + default: "sensitive_warning_default_note", + token: "sensitive_warning_token_note", + secret: "sensitive_warning_secret_note", + certificate: "sensitive_warning_certificate_note", + log: "sensitive_warning_log_note", + request: "sensitive_warning_request_note", +} as const satisfies Record, keyof ReturnType["t"]["common"]> + +export function SensitiveInputWarning({ + className, + variant = "default", +}: SensitiveInputWarningProps) { + const { lang, t } = useLang() + + return ( +

+ ) +} diff --git a/src/features/tool-shell/tool-action-bar.tsx b/src/features/tool-shell/tool-action-bar.tsx index 443cc7cc..9c510589 100644 --- a/src/features/tool-shell/tool-action-bar.tsx +++ b/src/features/tool-shell/tool-action-bar.tsx @@ -39,11 +39,11 @@ export type ToolAction = { type AnalyticsAction = "tool_run" | "copy_output" | "download_output" | null const ACTION_BASE_CLASS = - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50" + "inline-flex min-h-11 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 sm:min-h-9" const ACTION_SIZE_CLASS: Record = { - sm: "h-9 px-3", - icon: "h-9 w-9", + sm: "px-3 py-2 sm:h-9 sm:py-0", + icon: "w-11 sm:h-9 sm:w-9", } const ACTION_VARIANT_CLASS: Record = { @@ -121,7 +121,7 @@ export function ToolActionBar({ ) return ( -
+
{primaryActions.map((action) => { const Icon = action.icon const analyticsAction = classifyAnalyticsAction(action.id) @@ -135,6 +135,7 @@ export function ToolActionBar({ ACTION_BASE_CLASS, ACTION_SIZE_CLASS[action.size ?? "sm"], ACTION_VARIANT_CLASS[action.variant ?? "outline"], + "max-w-full", action.disabled && "pointer-events-none opacity-50", )} aria-disabled={action.disabled || undefined} @@ -163,6 +164,7 @@ export function ToolActionBar({ ACTION_BASE_CLASS, ACTION_SIZE_CLASS[action.size ?? "sm"], ACTION_VARIANT_CLASS[action.variant ?? "outline"], + "max-w-full", )} onClick={() => triggerAction(action)} disabled={action.disabled} @@ -184,6 +186,7 @@ export function ToolActionBar({ ACTION_BASE_CLASS, ACTION_SIZE_CLASS["sm"], ACTION_VARIANT_CLASS["outline"], + "max-w-full", )} disabled={!handoffPayload?.trim()} > diff --git a/src/features/tool-templates/focused-hash-tool-page.tsx b/src/features/tool-templates/focused-hash-tool-page.tsx index b7e5d35b..aa31e030 100644 --- a/src/features/tool-templates/focused-hash-tool-page.tsx +++ b/src/features/tool-templates/focused-hash-tool-page.tsx @@ -1,7 +1,7 @@ "use client" import * as React from "react" -import { Copy, Download, Eraser, Fingerprint, TestTube2 } from "lucide-react" +import { Copy, Download, Eraser, Eye, EyeOff, Fingerprint, TestTube2 } from "lucide-react" import { toast } from "sonner" import { useLang } from "@/core/i18n/lang-provider" import { RelatedTools } from "@/core/seo/components/related-tools" @@ -10,6 +10,7 @@ import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { ToolActionBar, type ToolAction } from "@/features/tool-shell/tool-action-bar" import { ToolEmptyState } from "@/features/tool-shell/tool-empty-state" +import { SensitiveInputWarning } from "@/features/tool-shell/sensitive-input-warning" import { hashBytes, hashHmac, @@ -71,10 +72,13 @@ export function FocusedHashToolPage({ relatedToolKey = "hash_generator", }: FocusedHashToolPageProps) { const { t, lang } = useLang() + const revealSecretLabel = t.common.reveal_secret + const hideSecretLabel = t.common.hide_secret const [mode, setMode] = React.useState("text") const [input, setInput] = React.useState("") const [secret, setSecret] = React.useState("") + const [secretVisible, setSecretVisible] = React.useState(false) const [fileName, setFileName] = React.useState("") const [fileSize, setFileSize] = React.useState(0) const [fileBytes, setFileBytes] = React.useState(null) @@ -264,6 +268,8 @@ export function FocusedHashToolPage({
) : null} + {enableHmac ? : null} +
@@ -320,13 +326,26 @@ export function FocusedHashToolPage({ )} {mode === "hmac" ? ( - setSecret(event.target.value)} - placeholder={t.common.hash_tool.secret_key} - spellCheck={false} - className="font-mono text-sm" - /> +
+ setSecret(event.target.value)} + placeholder={t.common.hash_tool.secret_key} + spellCheck={false} + className="font-mono text-sm" + /> + +
) : null}
diff --git a/src/features/tools/certificate-decoder/page.tsx b/src/features/tools/certificate-decoder/page.tsx index 09961655..7abcb0d3 100644 --- a/src/features/tools/certificate-decoder/page.tsx +++ b/src/features/tools/certificate-decoder/page.tsx @@ -6,6 +6,7 @@ import { Textarea } from "@/components/ui/textarea" import { useLang } from "@/core/i18n/lang-provider" import { RelatedTools } from "@/core/seo/components/related-tools" import { ToolActionBar, type ToolAction } from "@/features/tool-shell/tool-action-bar" +import { SensitiveInputWarning } from "@/features/tool-shell/sensitive-input-warning" // ─── ASN.1 / PEM Parser (Pure JS, no dependencies) ────────────────────────── @@ -332,6 +333,8 @@ export function CertificateDecoderPage() {
{/* Input */} + +