Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions public/_redirects
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions scripts/e2e/run-playwright-smoke.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
10 changes: 9 additions & 1 deletion scripts/gates/check-canonical.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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}`
}
Expand Down
10 changes: 9 additions & 1 deletion scripts/gates/check-hreflang.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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(
Expand Down
24 changes: 21 additions & 3 deletions scripts/gates/check-legacy-routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@ function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf8"))
}

function sitemapContainsSlugUrl(sitemap, slug) {
return new RegExp(`<loc>https://byteflow\\.tools/(?:${["en", "zh-CN", "zh-TW", "ja", "ko", "de", "fr"].join("|")})/${slug}</loc>`).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([
Expand All @@ -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) {
Expand All @@ -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()
52 changes: 44 additions & 8 deletions scripts/generators/generate-legacy-routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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) {
Expand All @@ -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)}`)
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/[lang]/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default function AboutPage() {
<p className="mt-3 text-base leading-relaxed text-muted-foreground">{p.about_intro}</p>
</section>

<section className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<section id="privacy" className="grid grid-cols-1 gap-4 sm:grid-cols-3">
{highlights.map((item) => (
<article
key={item.title}
Expand Down
11 changes: 9 additions & 2 deletions src/app/[lang]/all-tools/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { notFound } from "next/navigation"
import { isValidLocale } from "@/core/i18n/i18n"
import { getTranslation } from "@/core/i18n/translations/catalog"
import { buildStaticPageMetadata } from "@/core/seo/seo"
import { buildCanonicalUrl, buildLocalizedAlternates } from "@/core/seo/urls"

export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }): Promise<Metadata> {
const { lang } = await params
Expand All @@ -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 }) {
Expand Down
2 changes: 2 additions & 0 deletions src/app/[lang]/all-tools/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -92,6 +93,7 @@ export default async function AllToolsPage({ params }: { params: Promise<{ lang:
}))
return (
<div className="mx-auto flex w-full max-w-6xl flex-col gap-7 pb-8">
<AllToolsQueryRobots />
<header className="rounded-2xl border border-border/70 bg-card/55 p-5 backdrop-blur-sm sm:p-6">
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-primary/80">
{t.common.all_tools}
Expand Down
16 changes: 14 additions & 2 deletions src/app/[lang]/convert-encode/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Metadata> {
const { lang } = await params
Expand All @@ -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 }) {
Expand Down
16 changes: 14 additions & 2 deletions src/app/[lang]/design-media/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Metadata> {
const { lang } = await params
Expand All @@ -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 }) {
Expand Down
16 changes: 14 additions & 2 deletions src/app/[lang]/web-api/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Metadata> {
const { lang } = await params
Expand All @@ -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 }) {
Expand Down
Loading