From 3182ac414c45fa16ddb261d2c667018da3fd5e05 Mon Sep 17 00:00:00 2001 From: Paul Wackerow <54227730+wackerow@users.noreply.github.com> Date: Mon, 1 Dec 2025 13:25:26 -0300 Subject: [PATCH 01/15] fix: _redircts to use /*/ to /:splat/ prefix --- public/_redirects | 92 +++++++++++++++++++---------------------------- 1 file changed, 36 insertions(+), 56 deletions(-) diff --git a/public/_redirects b/public/_redirects index 57fb1ef8525..4cbd10131e9 100644 --- a/public/_redirects +++ b/public/_redirects @@ -1,66 +1,64 @@ -/discord https://discord.gg/ethereum-org 301! +## Canonical redirects (intl-prefixed) /*/discord https://discord.gg/ethereum-org 301! -/pdfs/* / 301! +/*/pdfs/* /:splat/ 301! -/brand /assets/ 301! +/*/brand /:splat/assets/ 301! -/ether /eth/ 301! +/*/ethereum.html /:splat/what-is-ethereum/ 301! -/token /developers/ 301! +/*/ether /:splat/eth/ 301! -/crowdsale /developers/ 301! +/*/token /:splat/developers/ 301! -/cli /developers/ 301! +/*/crowdsale /:splat/developers/ 301! -/greeter /developers/ 301! +/*/cli /:splat/developers/ 301! -/search / 301! +/*/greeter /:splat/developers/ 301! -/use /apps/ 301! +/*/roadmap/vision/ /:splat/roadmap/ 301! -/dapps /apps/ 301! +/*/search /:splat/ 301! -/beginners /what-is-ethereum/ 301! +/*/wallets/ /:splat/wallets/find-wallet/ 301! +/*/wallets/find-wallet/* /:splat/wallets/find-wallet/:splat 200! +/*/wallets/* /:splat/wallets/ 301! -/eth2/ /roadmap/ 301! +/*/nfts/ /:splat/nft/ 301! -/build/ /developers/learning-tools/ 301! - -/nfts/ /nft/ 301! - -/payments/ /payments/ 301! - -/daos/ /dao/ 301! - -/layer2/ /layer-2/ 301! +/*/daos/ /:splat/dao/ 301! /*/layer2/ /:splat/layer-2/ 301! -/grants/ /community/grants/ 301! +/*/grants/ /:splat/community/grants/ 301! /no/* /nb/:splat 301! /ph/* /fil/:splat 301! -/java/ /developers/docs/programming-languages/java/ 301! +/*/java/ /:splat/developers/docs/programming-languages/java/ 301! + +/*/python/ /:splat/developers/docs/programming-languages/python/ 301! -/python/ /developers/docs/programming-languages/python/ 301! +/*/javascript/ /:splat/developers/docs/programming-languages/javascript/ 301! -/javascript/ /developers/docs/programming-languages/javascript/ 301! +/*/golang/ /:splat/developers/docs/programming-languages/golang/ 301! -/golang/ /developers/docs/programming-languages/golang/ 301! +/*/rust/ /:splat/developers/docs/programming-languages/rust/ 301! -/rust/ /developers/docs/programming-languages/rust/ 301! +/*/dot-net/ /:splat/developers/docs/programming-languages/dot-net/ 301! -/dot-net/ /developers/docs/programming-languages/dot-net/ 301! +/*/delphi/ /:splat/developers/docs/programming-languages/delphi/ 301! -/delphi/ /developers/docs/programming-languages/delphi/ 301! +/*/dart/ /:splat/developers/docs/programming-languages/dart/ 301! -/dart/ /developers/docs/programming-languages/dart/ 301! +## handled below with intl-prefixed rule -/developers/docs/mining/ /developers/docs/consensus-mechanisms/pow/mining/ 301! +/*/languages/ /:splat/community/language-resources/ 301! + +/*/developers/docs/mining/ /:splat/developers/docs/consensus-mechanisms/pow/mining/ 301! /*/beginners /:splat/what-is-ethereum/ 301! @@ -90,34 +88,20 @@ /*/upgrades/shard-chains/ /:splat/roadmap/danksharding/ 301! -/upgrades/sharding/ /roadmap/danksharding/ 301! - /*/upgrades/sharding/ /:splat/roadmap/danksharding/ 301! -/upgrades/shard-chains/ /roadmap/danksharding/ 301! - -/upgrades/merge /roadmap/merge/ 301! - /*/upgrades/merge /:splat/roadmap/merge/ 301! -/upgrades/merge/issuance /roadmap/merge/issuance 301! - /*/upgrades/merge/issuance /:splat/roadmap/merge/issuance 301! -/upgrades/beacon-chain /roadmap/beacon-chain 301! - /*/upgrades/beacon-chain /:splat/roadmap/beacon-chain 301! -/upgrades/vision/ /roadmap/ 301! - /*/upgrades/vision/ /:splat/roadmap/ 301! -/upgrades /roadmap 301! +/*/roadmap/vision/ /:splat/roadmap/ 301! /*/upgrades /:splat/roadmap 301! -/upgrades/get-involved /contributing 301! - /*/upgrades/get-involved /:splat/contributing 301! /*/eth2/staking/ /:splat/staking/ 301! @@ -136,6 +120,8 @@ /*/developers/docs/scaling/layer-2-rollups /:splat/developers/docs/scaling 301! +/*/developers/docs/layer-2-scaling/ /:splat/layer-2/ 301! + /*/about/web-developer /:splat/about/#open-jobs 301! /*/about/product-designer /:splat/about/#open-jobs 301! @@ -147,6 +133,8 @@ /*/dapps /:splat/apps/ 301! +## duplicates removed above; keep only one canonical rule per path + /*/contributing/translation-program/translation-guide/ /:splat/contributing/translation-program/faq/ 301! /*/contributing/translation-program/content-versions/ /:splat/contributing/translation-program/ 301! @@ -157,7 +145,7 @@ /*/developers/docs/smart-contracts/upgrading-smart-contracts/ /:splat/developers/docs/smart-contracts/upgrading/ 301! -/staking/withdraws /staking/withdrawals/ 301! +## handled below with intl-prefixed rule /*/writing-cohort https://ethereumwriterscohort.carrd.co/ 301! @@ -169,18 +157,10 @@ /*/enterprise/private-ethereum/ /:splat/enterprise/ 301! -/dashboards /resources 301! - /*/dashboards /:splat/resources 301! -/tds /trillion-dollar-security 301! - /*/tds /:splat/trillion-dollar-security 301! -/10-years /10years 301! - /*/10-years /:splat/10years 301! -/history /ethereum-forks 301! - /*/history /:splat/ethereum-forks 301! From a9d0310295808953e182a6f0e235c6e745c2a027 Mon Sep 17 00:00:00 2001 From: Paul Wackerow <54227730+wackerow@users.noreply.github.com> Date: Mon, 1 Dec 2025 13:33:47 -0300 Subject: [PATCH 02/15] feat: add redirects for ext 404s --- public/_redirects | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/public/_redirects b/public/_redirects index 4cbd10131e9..2024de658d7 100644 --- a/public/_redirects +++ b/public/_redirects @@ -26,6 +26,18 @@ /*/wallets/find-wallet/* /:splat/wallets/find-wallet/:splat 200! /*/wallets/* /:splat/wallets/ 301! +/*/garden /:splat/roadmap/ 301! + +/*/download /:splat/developers/docs/nodes-and-clients/ 301! + +/*/how /:splat/guides/ 301! + +# Remove /content prefix (legacy URLs) +/:locale/content/* /:locale/:splat 301! + +# Invalid nested apps paths → apps hub +/:locale/apps/:app/* /:locale/apps/ 301! + /*/nfts/ /:splat/nft/ 301! /*/daos/ /:splat/dao/ 301! From 0965f70376d71afac4654f7ae118393125223535 Mon Sep 17 00:00:00 2001 From: Paul Wackerow <54227730+wackerow@users.noreply.github.com> Date: Mon, 1 Dec 2025 18:16:38 -0300 Subject: [PATCH 03/15] fix(i18n): use named single-segment `/:locale` prefix --- public/_redirects | 173 ++++++++++++++++++++++------------------------ 1 file changed, 83 insertions(+), 90 deletions(-) diff --git a/public/_redirects b/public/_redirects index 2024de658d7..36a398facb4 100644 --- a/public/_redirects +++ b/public/_redirects @@ -1,178 +1,171 @@ ## Canonical redirects (intl-prefixed) -/*/discord https://discord.gg/ethereum-org 301! +/:locale/discord https://discord.gg/ethereum-org 301! -/*/pdfs/* /:splat/ 301! +/:locale/pdfs/* /:locale/ 301! -/*/brand /:splat/assets/ 301! +/:locale/brand /:locale/assets/ 301! -/*/ethereum.html /:splat/what-is-ethereum/ 301! +/:locale/ethereum.html /:locale/what-is-ethereum/ 301! -/*/ether /:splat/eth/ 301! +/:locale/ether /:locale/eth/ 301! -/*/token /:splat/developers/ 301! +/:locale/token /:locale/developers/ 301! -/*/crowdsale /:splat/developers/ 301! +/:locale/crowdsale /:locale/developers/ 301! -/*/cli /:splat/developers/ 301! +/:locale/cli /:locale/developers/ 301! -/*/greeter /:splat/developers/ 301! +/:locale/greeter /:locale/developers/ 301! -/*/roadmap/vision/ /:splat/roadmap/ 301! +/:locale/roadmap/vision/ /:locale/roadmap/ 301! -/*/search /:splat/ 301! +/:locale/search /:locale/ 301! -/*/wallets/ /:splat/wallets/find-wallet/ 301! -/*/wallets/find-wallet/* /:splat/wallets/find-wallet/:splat 200! -/*/wallets/* /:splat/wallets/ 301! +/:locale/wallets/ /:locale/wallets/find-wallet/ 301! -/*/garden /:splat/roadmap/ 301! +/:locale/wallets/find-wallet/* /:locale/wallets/find-wallet/:splat 200! -/*/download /:splat/developers/docs/nodes-and-clients/ 301! +/:locale/wallets/* /:locale/wallets/ 301! -/*/how /:splat/guides/ 301! +/:locale/garden /:locale/roadmap/ 301! -# Remove /content prefix (legacy URLs) +/:locale/download /:locale/developers/docs/nodes-and-clients/ 301! + +/:locale/how /:locale/guides/ 301! + +# Remove /content any usage of prefix /:locale/content/* /:locale/:splat 301! # Invalid nested apps paths → apps hub /:locale/apps/:app/* /:locale/apps/ 301! -/*/nfts/ /:splat/nft/ 301! +/:locale/nfts/ /:locale/nft/ 301! -/*/daos/ /:splat/dao/ 301! +/:locale/daos/ /:locale/dao/ 301! -/*/layer2/ /:splat/layer-2/ 301! +/:locale/layer2/ /:locale/layer-2/ 301! -/*/grants/ /:splat/community/grants/ 301! +/:locale/grants/ /:locale/community/grants/ 301! /no/* /nb/:splat 301! /ph/* /fil/:splat 301! -/*/java/ /:splat/developers/docs/programming-languages/java/ 301! - -/*/python/ /:splat/developers/docs/programming-languages/python/ 301! - -/*/javascript/ /:splat/developers/docs/programming-languages/javascript/ 301! - -/*/golang/ /:splat/developers/docs/programming-languages/golang/ 301! - -/*/rust/ /:splat/developers/docs/programming-languages/rust/ 301! +/:locale/java/ /:locale/developers/docs/programming-languages/java/ 301! -/*/dot-net/ /:splat/developers/docs/programming-languages/dot-net/ 301! +/:locale/python/ /:locale/developers/docs/programming-languages/python/ 301! -/*/delphi/ /:splat/developers/docs/programming-languages/delphi/ 301! +/:locale/javascript/ /:locale/developers/docs/programming-languages/javascript/ 301! -/*/dart/ /:splat/developers/docs/programming-languages/dart/ 301! +/:locale/golang/ /:locale/developers/docs/programming-languages/golang/ 301! -## handled below with intl-prefixed rule +/:locale/rust/ /:locale/developers/docs/programming-languages/rust/ 301! -/*/languages/ /:splat/community/language-resources/ 301! +/:locale/dot-net/ /:locale/developers/docs/programming-languages/dot-net/ 301! -/*/developers/docs/mining/ /:splat/developers/docs/consensus-mechanisms/pow/mining/ 301! +/:locale/delphi/ /:locale/developers/docs/programming-languages/delphi/ 301! -/*/beginners /:splat/what-is-ethereum/ 301! +/:locale/dart/ /:locale/developers/docs/programming-languages/dart/ 301! -/*/build /:splat/developers/learning-tools/ 301! +/:locale/languages/ /:locale/community/language-resources/ 301! -/*/eth2/beacon-chain/ /:splat/upgrades/beacon-chain/ 301! +/:locale/developers/docs/mining/ /:locale/developers/docs/consensus-mechanisms/pow/mining/ 301! -/*/eth2/the-beacon-chain/ /:splat/upgrades/beacon-chain/ 301! +/:locale/beginners /:locale/what-is-ethereum/ 301! -/*/upgrades/the-beacon-chain/ /:splat/upgrades/beacon-chain/ 301! +/:locale/build /:locale/developers/learning-tools/ 301! -/*/eth2/merge /:splat/upgrades/merge/ 301! +/:locale/eth2/beacon-chain/ /:locale/upgrades/beacon-chain/ 301! -/*/eth2/the-merge /:splat/upgrades/merge/ 301! +/:locale/eth2/the-beacon-chain/ /:locale/upgrades/beacon-chain/ 301! -/*/upgrades/the-merge /:splat/upgrades/merge/ 301! +/:locale/upgrades/the-beacon-chain/ /:locale/upgrades/beacon-chain/ 301! -/*/eth2/docking /:splat/upgrades/merge/ 301! +/:locale/eth2/merge /:locale/upgrades/merge/ 301! -/*/upgrades/docking /:splat/upgrades/merge/ 301! +/:locale/eth2/the-merge /:locale/upgrades/merge/ 301! -/*/eth2/the-docking /:splat/upgrades/merge/ 301! +/:locale/upgrades/the-merge /:locale/upgrades/merge/ 301! -/*/upgrades/the-docking /:splat/upgrades/merge/ 301! +/:locale/eth2/docking /:locale/upgrades/merge/ 301! -/*/eth2/shard-chains/ /:splat/roadmap/danksharding/ 301! +/:locale/upgrades/docking /:locale/upgrades/merge/ 301! -/*/upgrades/shard-chains/ /:splat/roadmap/danksharding/ 301! +/:locale/eth2/the-docking /:locale/upgrades/merge/ 301! -/*/upgrades/sharding/ /:splat/roadmap/danksharding/ 301! +/:locale/upgrades/the-docking /:locale/upgrades/merge/ 301! -/*/upgrades/merge /:splat/roadmap/merge/ 301! +/:locale/eth2/shard-chains/ /:locale/roadmap/danksharding/ 301! -/*/upgrades/merge/issuance /:splat/roadmap/merge/issuance 301! +/:locale/upgrades/shard-chains/ /:locale/roadmap/danksharding/ 301! -/*/upgrades/beacon-chain /:splat/roadmap/beacon-chain 301! +/:locale/upgrades/sharding/ /:locale/roadmap/danksharding/ 301! -/*/upgrades/vision/ /:splat/roadmap/ 301! +/:locale/upgrades/merge /:locale/roadmap/merge/ 301! -/*/roadmap/vision/ /:splat/roadmap/ 301! +/:locale/upgrades/merge/issuance /:locale/roadmap/merge/issuance 301! -/*/upgrades /:splat/roadmap 301! +/:locale/upgrades/beacon-chain /:locale/roadmap/beacon-chain 301! -/*/upgrades/get-involved /:splat/contributing 301! +/:locale/upgrades/vision/ /:locale/roadmap/ 301! -/*/eth2/staking/ /:splat/staking/ 301! +/:locale/roadmap/vision/ /:locale/roadmap/ 301! -/*/eth2/vision/ /:splat/roadmap/vision/ 301! +/:locale/upgrades /:locale/roadmap 301! -/*/eth2/get-involved/ /:splat/upgrades/get-involved/ 301! +/:locale/upgrades/get-involved /:locale/contributing 301! -/*/eth2/get-involved/bug-bounty/ /:splat/bug-bounty/ 301! +/:locale/eth2/staking/ /:locale/staking/ 301! -/*/upgrades/get-involved/bug-bounty/ /:splat/bug-bounty/ 301! +/:locale/eth2/vision/ /:locale/roadmap/vision/ 301! -/*/eth2/deposit-contract/ /:splat/staking/deposit-contract/ 301! +/:locale/eth2/get-involved/ /:locale/upgrades/get-involved/ 301! -/*/eth2 /:splat/upgrades/ 301! +/:locale/eth2/get-involved/bug-bounty/ /:locale/bug-bounty/ 301! -/*/developers/docs/scaling/layer-2-rollups /:splat/developers/docs/scaling 301! +/:locale/upgrades/get-involved/bug-bounty/ /:locale/bug-bounty/ 301! -/*/developers/docs/layer-2-scaling/ /:splat/layer-2/ 301! +/:locale/eth2/deposit-contract/ /:locale/staking/deposit-contract/ 301! -/*/about/web-developer /:splat/about/#open-jobs 301! +/:locale/eth2 /:locale/upgrades/ 301! -/*/about/product-designer /:splat/about/#open-jobs 301! +/:locale/developers/docs/scaling/layer-2-rollups /:locale/developers/docs/scaling 301! -/*/use /:splat/apps/ 301! +/:locale/developers/docs/layer-2-scaling/ /:locale/layer-2/ 301! -# Exception: don't redirect developers/docs/dapps paths -/*/developers/docs/dapps* /:splat/developers/docs/dapps:splat 200! +/:locale/about/web-developer /:locale/about/#open-jobs 301! -/*/dapps /:splat/apps/ 301! +/:locale/about/product-designer /:locale/about/#open-jobs 301! -## duplicates removed above; keep only one canonical rule per path +/:locale/use /:locale/apps/ 301! -/*/contributing/translation-program/translation-guide/ /:splat/contributing/translation-program/faq/ 301! +/:locale/dapps /:locale/apps/ 301! -/*/contributing/translation-program/content-versions/ /:splat/contributing/translation-program/ 301! +/:locale/contributing/translation-program/translation-guide/ /:locale/contributing/translation-program/faq/ 301! -/*/contributing/translation-program/content-buckets/ /:splat/contributing/translation-program/ 301! +/:locale/contributing/translation-program/content-versions/ /:locale/contributing/translation-program/ 301! -/*/developers/docs/smart-contracts/source-code-verification/ /:splat/developers/docs/smart-contracts/verifying/ 301! +/:locale/contributing/translation-program/content-buckets/ /:locale/contributing/translation-program/ 301! -/*/developers/docs/smart-contracts/upgrading-smart-contracts/ /:splat/developers/docs/smart-contracts/upgrading/ 301! +/:locale/developers/docs/smart-contracts/source-code-verification/ /:locale/developers/docs/smart-contracts/verifying/ 301! -## handled below with intl-prefixed rule +/:locale/developers/docs/smart-contracts/upgrading-smart-contracts/ /:locale/developers/docs/smart-contracts/upgrading/ 301! -/*/writing-cohort https://ethereumwriterscohort.carrd.co/ 301! +/:locale/writing-cohort https://ethereumwriterscohort.carrd.co/ 301! -/*/staking/withdraws /:splat/staking/withdrawals/ 301! +/:locale/staking/withdraws /:locale/staking/withdrawals/ 301! -/*/guides/how-to-register-an-ethereum-account /:splat/guides/how-to-create-an-ethereum-account/ 301! +/:locale/guides/how-to-register-an-ethereum-account /:locale/guides/how-to-create-an-ethereum-account/ 301! -/*/deprecated-software /:splat/apps/ 301! +/:locale/deprecated-software /:locale/apps/ 301! -/*/enterprise/private-ethereum/ /:splat/enterprise/ 301! +/:locale/enterprise/private-ethereum/ /:locale/enterprise/ 301! -/*/dashboards /:splat/resources 301! +/:locale/dashboards /:locale/resources 301! -/*/tds /:splat/trillion-dollar-security 301! +/:locale/tds /:locale/trillion-dollar-security 301! -/*/10-years /:splat/10years 301! +/:locale/10-years /:locale/10years 301! -/*/history /:splat/ethereum-forks 301! +/:locale/history /:locale/ethereum-forks 301! From ecba9a440a4fc0932adee865316f7ca9e3b7160a Mon Sep 17 00:00:00 2001 From: Paul Wackerow <54227730+wackerow@users.noreply.github.com> Date: Mon, 1 Dec 2025 21:37:23 -0300 Subject: [PATCH 04/15] patch: nesting redirects Should 404 if not available --- public/_redirects | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/public/_redirects b/public/_redirects index 36a398facb4..b26c6def5d9 100644 --- a/public/_redirects +++ b/public/_redirects @@ -24,7 +24,7 @@ /:locale/wallets/ /:locale/wallets/find-wallet/ 301! -/:locale/wallets/find-wallet/* /:locale/wallets/find-wallet/:splat 200! + /:locale/wallets/* /:locale/wallets/ 301! @@ -34,11 +34,8 @@ /:locale/how /:locale/guides/ 301! -# Remove /content any usage of prefix -/:locale/content/* /:locale/:splat 301! - -# Invalid nested apps paths → apps hub -/:locale/apps/:app/* /:locale/apps/ 301! +# Remove /content prefix from legacy URLs (only if destination exists; invalid paths will 404) +/:locale/content/* /:locale/:splat 301 /:locale/nfts/ /:locale/nft/ 301! From 3b68b3a50615e764a98baf54d5a75a1f7925ded8 Mon Sep 17 00:00:00 2001 From: Paul Wackerow <54227730+wackerow@users.noreply.github.com> Date: Mon, 1 Dec 2025 21:38:15 -0300 Subject: [PATCH 05/15] patch: no and ph handlers to top --- public/_redirects | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/public/_redirects b/public/_redirects index b26c6def5d9..78da102e1ce 100644 --- a/public/_redirects +++ b/public/_redirects @@ -1,3 +1,9 @@ +# Locale alias redirects (legacy typos) + +/no/* /nb/:splat 301! + +/ph/* /fil/:splat 301! + ## Canonical redirects (intl-prefixed) /:locale/discord https://discord.gg/ethereum-org 301! From 0b528e8f53d7c2ea3a256de9c2900b1cc72ba5de Mon Sep 17 00:00:00 2001 From: Paul Wackerow <54227730+wackerow@users.noreply.github.com> Date: Mon, 1 Dec 2025 21:39:47 -0300 Subject: [PATCH 06/15] patch: lowercase all incoming paths --- middleware.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/middleware.ts b/middleware.ts index 97d6926076d..3aa0c13d98d 100644 --- a/middleware.ts +++ b/middleware.ts @@ -7,6 +7,16 @@ import { DEFAULT_LOCALE } from "./src/lib/constants" const handleI18nRouting = createMiddleware(routing) export default function middleware(request: NextRequest) { + // Normalize to lowercase paths site-wide (URLs are case-insensitive by spec, + // but our routes are defined in lowercase). Do this BEFORE i18n routing. + const originalPath = request.nextUrl.pathname + const lowerPath = originalPath.toLowerCase() + if (originalPath !== lowerPath) { + const url = request.nextUrl.clone() + url.pathname = lowerPath + return NextResponse.redirect(url, 301) + } + const response = handleI18nRouting(request) // Upgrade default-locale strip redirects from 307 to 301 for SEO From a8829c3c5cf3c17734ac41110503ea926a97181d Mon Sep 17 00:00:00 2001 From: Paul Wackerow <54227730+wackerow@users.noreply.github.com> Date: Tue, 2 Dec 2025 18:55:09 -0300 Subject: [PATCH 07/15] refactor: use next.js middleware for all redirect logic --- middleware.ts | 157 ++++++++++++++++++++++++++++++++++++++++- public/_redirects | 176 +--------------------------------------------- 2 files changed, 158 insertions(+), 175 deletions(-) diff --git a/middleware.ts b/middleware.ts index 3aa0c13d98d..481ecaf2ddb 100644 --- a/middleware.ts +++ b/middleware.ts @@ -6,6 +6,144 @@ import { DEFAULT_LOCALE } from "./src/lib/constants" const handleI18nRouting = createMiddleware(routing) +// Build a strict locale matcher from configured locales +const LOCALE_ALTS = routing.locales.join("|") + +// Define redirect patterns - these will work for both /path and /:locale/path +const redirectPatterns: { from: string; to: string }[] = [ + // Locale aliases + { from: "/no/(.*)", to: "/nb/$1" }, + { from: "/ph/(.*)", to: "/fil/$1" }, + // Legacy redirects + { from: "/discord", to: "https://discord.gg/ethereum-org" }, + { from: "/pdfs/(.*)", to: "/" }, + { from: "/brand", to: "/assets/" }, + { from: "/ethereum\\.html", to: "/what-is-ethereum/" }, + { from: "/ether", to: "/eth/" }, + { from: "/token", to: "/developers/" }, + { from: "/crowdsale", to: "/developers/" }, + { from: "/cli", to: "/developers/" }, + { from: "/greeter", to: "/developers/" }, + { from: "/roadmap/vision", to: "/roadmap/" }, + { from: "/search", to: "/" }, + { from: "/garden", to: "/roadmap/" }, + { from: "/download", to: "/developers/docs/nodes-and-clients/" }, + { from: "/how", to: "/guides/" }, + { from: "/content/(.*)", to: "/$1/" }, + { from: "/nfts", to: "/nft/" }, + { from: "/daos", to: "/dao/" }, + { from: "/layer2", to: "/layer-2/" }, + { from: "/grants", to: "/community/grants/" }, + { from: "/java", to: "/developers/docs/programming-languages/java/" }, + { from: "/python", to: "/developers/docs/programming-languages/python/" }, + { + from: "/javascript", + to: "/developers/docs/programming-languages/javascript/", + }, + { from: "/golang", to: "/developers/docs/programming-languages/golang/" }, + { from: "/rust", to: "/developers/docs/programming-languages/rust/" }, + { from: "/dot-net", to: "/developers/docs/programming-languages/dot-net/" }, + { from: "/delphi", to: "/developers/docs/programming-languages/delphi/" }, + { from: "/dart", to: "/developers/docs/programming-languages/dart/" }, + { from: "/languages", to: "/community/language-resources/" }, + { + from: "/developers/docs/mining", + to: "/developers/docs/consensus-mechanisms/pow/mining/", + }, + { from: "/beginners", to: "/what-is-ethereum/" }, + { from: "/build", to: "/developers/learning-tools/" }, + { from: "/eth2/beacon-chain", to: "/roadmap/beacon-chain/" }, + { from: "/eth2/the-beacon-chain", to: "/roadmap/beacon-chain/" }, + { from: "/upgrades/the-beacon-chain", to: "/roadmap/beacon-chain/" }, + { from: "/eth2/merge", to: "/roadmap/merge/" }, + { from: "/eth2/the-merge", to: "/roadmap/merge/" }, + { from: "/upgrades/the-merge", to: "/roadmap/merge/" }, + { from: "/eth2/docking", to: "/roadmap/merge/" }, + { from: "/upgrades/docking", to: "/roadmap/merge/" }, + { from: "/eth2/the-docking", to: "/roadmap/merge/" }, + { from: "/upgrades/the-docking", to: "/roadmap/merge/" }, + { from: "/eth2/shard-chains", to: "/roadmap/danksharding/" }, + { from: "/upgrades/shard-chains", to: "/roadmap/danksharding/" }, + { from: "/upgrades/sharding", to: "/roadmap/danksharding/" }, + { from: "/upgrades/merge", to: "/roadmap/merge/" }, + { from: "/upgrades/merge/issuance", to: "/roadmap/merge/issuance" }, + { from: "/upgrades/beacon-chain", to: "/roadmap/beacon-chain" }, + { from: "/upgrades/vision", to: "/roadmap/" }, + { from: "/upgrades", to: "/roadmap" }, + { from: "/upgrades/get-involved", to: "/contributing" }, + { from: "/eth2/staking", to: "/staking/" }, + { from: "/eth2/vision", to: "/roadmap/vision/" }, + { from: "/eth2/get-involved", to: "/upgrades/get-involved/" }, + { from: "/eth2/get-involved/bug-bounty", to: "/bug-bounty/" }, + { from: "/upgrades/get-involved/bug-bounty", to: "/bug-bounty/" }, + { from: "/eth2/deposit-contract", to: "/staking/deposit-contract/" }, + { from: "/eth2", to: "/upgrades/" }, + { + from: "/developers/docs/scaling/layer-2-rollups", + to: "/developers/docs/scaling", + }, + { from: "/developers/docs/layer-2-scaling", to: "/layer-2/" }, + { from: "/about/web-developer", to: "/about/#open-jobs" }, + { from: "/about/product-designer", to: "/about/#open-jobs" }, + { from: "/use", to: "/apps/" }, + { from: "/dapps", to: "/apps/" }, + { + from: "/contributing/translation-program/translation-guide", + to: "/contributing/translation-program/faq/", + }, + { + from: "/contributing/translation-program/content-versions", + to: "/contributing/translation-program/", + }, + { + from: "/contributing/translation-program/content-buckets", + to: "/contributing/translation-program/", + }, + { + from: "/developers/docs/smart-contracts/source-code-verification", + to: "/developers/docs/smart-contracts/verifying/", + }, + { + from: "/developers/docs/smart-contracts/upgrading-smart-contracts", + to: "/developers/docs/smart-contracts/upgrading/", + }, + { from: "/writing-cohort", to: "https://ethereumwriterscohort.carrd.co/" }, + { from: "/staking/withdraws", to: "/staking/withdrawals/" }, + { + from: "/guides/how-to-register-an-ethereum-account", + to: "/guides/how-to-create-an-ethereum-account/", + }, + { from: "/deprecated-software", to: "/apps/" }, + { from: "/enterprise/private-ethereum", to: "/enterprise/" }, + { from: "/dashboards", to: "/resources" }, + { from: "/tds", to: "/trillion-dollar-security" }, + { from: "/10-years", to: "/10years" }, + { from: "/history", to: "/ethereum-forks" }, +] + +// Build regex patterns for both non-prefixed and locale-prefixed paths +const redirects: { + from: RegExp + to: string + type: number +}[] = redirectPatterns.flatMap(({ from, to }) => { + return [ + // Non-prefixed version (e.g., /cli) + { + from: new RegExp(`^${from}/?$`), + to, + type: 301, + }, + // Locale-prefixed version (e.g., /es/cli) + { + // from: new RegExp(`^/(\\w+)${from}/?$`), + from: new RegExp(`^/(${LOCALE_ALTS})${from}/?$`), + to: `/$1${to}`, + type: 301, + }, + ] +}) + export default function middleware(request: NextRequest) { // Normalize to lowercase paths site-wide (URLs are case-insensitive by spec, // but our routes are defined in lowercase). Do this BEFORE i18n routing. @@ -17,6 +155,23 @@ export default function middleware(request: NextRequest) { return NextResponse.redirect(url, 301) } + // Apply custom redirects BEFORE i18n routing + for (const rule of redirects) { + if (rule.from.test(lowerPath)) { + const replaced = lowerPath.replace(rule.from, rule.to) + + // External targets (e.g., discord invite) + if (replaced.startsWith("http://") || replaced.startsWith("https://")) { + return NextResponse.redirect(replaced, rule.type) + } + + // Internal targets + const url = request.nextUrl.clone() + url.pathname = replaced + return NextResponse.redirect(url, rule.type) + } + } + const response = handleI18nRouting(request) // Upgrade default-locale strip redirects from 307 to 301 for SEO @@ -36,5 +191,5 @@ export default function middleware(request: NextRequest) { // Simplified matcher pattern export const config = { - matcher: ["/((?!api|_next|_vercel|.well-known|.*\\.[^/]*$).*)"], + matcher: ["/((?!api|_next|_vercel|.well-known|.*\\.(?!html$)[^/]+$).*)"], } diff --git a/public/_redirects b/public/_redirects index 78da102e1ce..4c780ac1382 100644 --- a/public/_redirects +++ b/public/_redirects @@ -1,174 +1,2 @@ -# Locale alias redirects (legacy typos) - -/no/* /nb/:splat 301! - -/ph/* /fil/:splat 301! - -## Canonical redirects (intl-prefixed) - -/:locale/discord https://discord.gg/ethereum-org 301! - -/:locale/pdfs/* /:locale/ 301! - -/:locale/brand /:locale/assets/ 301! - -/:locale/ethereum.html /:locale/what-is-ethereum/ 301! - -/:locale/ether /:locale/eth/ 301! - -/:locale/token /:locale/developers/ 301! - -/:locale/crowdsale /:locale/developers/ 301! - -/:locale/cli /:locale/developers/ 301! - -/:locale/greeter /:locale/developers/ 301! - -/:locale/roadmap/vision/ /:locale/roadmap/ 301! - -/:locale/search /:locale/ 301! - -/:locale/wallets/ /:locale/wallets/find-wallet/ 301! - - - -/:locale/wallets/* /:locale/wallets/ 301! - -/:locale/garden /:locale/roadmap/ 301! - -/:locale/download /:locale/developers/docs/nodes-and-clients/ 301! - -/:locale/how /:locale/guides/ 301! - -# Remove /content prefix from legacy URLs (only if destination exists; invalid paths will 404) -/:locale/content/* /:locale/:splat 301 - -/:locale/nfts/ /:locale/nft/ 301! - -/:locale/daos/ /:locale/dao/ 301! - -/:locale/layer2/ /:locale/layer-2/ 301! - -/:locale/grants/ /:locale/community/grants/ 301! - -/no/* /nb/:splat 301! - -/ph/* /fil/:splat 301! - -/:locale/java/ /:locale/developers/docs/programming-languages/java/ 301! - -/:locale/python/ /:locale/developers/docs/programming-languages/python/ 301! - -/:locale/javascript/ /:locale/developers/docs/programming-languages/javascript/ 301! - -/:locale/golang/ /:locale/developers/docs/programming-languages/golang/ 301! - -/:locale/rust/ /:locale/developers/docs/programming-languages/rust/ 301! - -/:locale/dot-net/ /:locale/developers/docs/programming-languages/dot-net/ 301! - -/:locale/delphi/ /:locale/developers/docs/programming-languages/delphi/ 301! - -/:locale/dart/ /:locale/developers/docs/programming-languages/dart/ 301! - -/:locale/languages/ /:locale/community/language-resources/ 301! - -/:locale/developers/docs/mining/ /:locale/developers/docs/consensus-mechanisms/pow/mining/ 301! - -/:locale/beginners /:locale/what-is-ethereum/ 301! - -/:locale/build /:locale/developers/learning-tools/ 301! - -/:locale/eth2/beacon-chain/ /:locale/upgrades/beacon-chain/ 301! - -/:locale/eth2/the-beacon-chain/ /:locale/upgrades/beacon-chain/ 301! - -/:locale/upgrades/the-beacon-chain/ /:locale/upgrades/beacon-chain/ 301! - -/:locale/eth2/merge /:locale/upgrades/merge/ 301! - -/:locale/eth2/the-merge /:locale/upgrades/merge/ 301! - -/:locale/upgrades/the-merge /:locale/upgrades/merge/ 301! - -/:locale/eth2/docking /:locale/upgrades/merge/ 301! - -/:locale/upgrades/docking /:locale/upgrades/merge/ 301! - -/:locale/eth2/the-docking /:locale/upgrades/merge/ 301! - -/:locale/upgrades/the-docking /:locale/upgrades/merge/ 301! - -/:locale/eth2/shard-chains/ /:locale/roadmap/danksharding/ 301! - -/:locale/upgrades/shard-chains/ /:locale/roadmap/danksharding/ 301! - -/:locale/upgrades/sharding/ /:locale/roadmap/danksharding/ 301! - -/:locale/upgrades/merge /:locale/roadmap/merge/ 301! - -/:locale/upgrades/merge/issuance /:locale/roadmap/merge/issuance 301! - -/:locale/upgrades/beacon-chain /:locale/roadmap/beacon-chain 301! - -/:locale/upgrades/vision/ /:locale/roadmap/ 301! - -/:locale/roadmap/vision/ /:locale/roadmap/ 301! - -/:locale/upgrades /:locale/roadmap 301! - -/:locale/upgrades/get-involved /:locale/contributing 301! - -/:locale/eth2/staking/ /:locale/staking/ 301! - -/:locale/eth2/vision/ /:locale/roadmap/vision/ 301! - -/:locale/eth2/get-involved/ /:locale/upgrades/get-involved/ 301! - -/:locale/eth2/get-involved/bug-bounty/ /:locale/bug-bounty/ 301! - -/:locale/upgrades/get-involved/bug-bounty/ /:locale/bug-bounty/ 301! - -/:locale/eth2/deposit-contract/ /:locale/staking/deposit-contract/ 301! - -/:locale/eth2 /:locale/upgrades/ 301! - -/:locale/developers/docs/scaling/layer-2-rollups /:locale/developers/docs/scaling 301! - -/:locale/developers/docs/layer-2-scaling/ /:locale/layer-2/ 301! - -/:locale/about/web-developer /:locale/about/#open-jobs 301! - -/:locale/about/product-designer /:locale/about/#open-jobs 301! - -/:locale/use /:locale/apps/ 301! - -/:locale/dapps /:locale/apps/ 301! - -/:locale/contributing/translation-program/translation-guide/ /:locale/contributing/translation-program/faq/ 301! - -/:locale/contributing/translation-program/content-versions/ /:locale/contributing/translation-program/ 301! - -/:locale/contributing/translation-program/content-buckets/ /:locale/contributing/translation-program/ 301! - -/:locale/developers/docs/smart-contracts/source-code-verification/ /:locale/developers/docs/smart-contracts/verifying/ 301! - -/:locale/developers/docs/smart-contracts/upgrading-smart-contracts/ /:locale/developers/docs/smart-contracts/upgrading/ 301! - -/:locale/writing-cohort https://ethereumwriterscohort.carrd.co/ 301! - -/:locale/staking/withdraws /:locale/staking/withdrawals/ 301! - -/:locale/guides/how-to-register-an-ethereum-account /:locale/guides/how-to-create-an-ethereum-account/ 301! - -/:locale/deprecated-software /:locale/apps/ 301! - -/:locale/enterprise/private-ethereum/ /:locale/enterprise/ 301! - -/:locale/dashboards /:locale/resources 301! - -/:locale/tds /:locale/trillion-dollar-security 301! - -/:locale/10-years /:locale/10years 301! - -/:locale/history /:locale/ethereum-forks 301! +# All redirects now handled in Next.js middleware +# See middleware.ts for redirect logic From 8e7ddd2473a73a3d250c8d861256b54aa8780f07 Mon Sep 17 00:00:00 2001 From: Paul Wackerow <54227730+wackerow@users.noreply.github.com> Date: Tue, 2 Dec 2025 19:23:16 -0300 Subject: [PATCH 08/15] remove: _redirects --- public/_redirects | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 public/_redirects diff --git a/public/_redirects b/public/_redirects deleted file mode 100644 index 4c780ac1382..00000000000 --- a/public/_redirects +++ /dev/null @@ -1,2 +0,0 @@ -# All redirects now handled in Next.js middleware -# See middleware.ts for redirect logic From b061e60660aab3be55b35d779c22d0774d2b9fe7 Mon Sep 17 00:00:00 2001 From: Paul Wackerow <54227730+wackerow@users.noreply.github.com> Date: Wed, 3 Dec 2025 08:21:28 -0300 Subject: [PATCH 09/15] patch: /content/ redirect trailing slash --- middleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/middleware.ts b/middleware.ts index 481ecaf2ddb..2b84c608837 100644 --- a/middleware.ts +++ b/middleware.ts @@ -29,7 +29,7 @@ const redirectPatterns: { from: string; to: string }[] = [ { from: "/garden", to: "/roadmap/" }, { from: "/download", to: "/developers/docs/nodes-and-clients/" }, { from: "/how", to: "/guides/" }, - { from: "/content/(.*)", to: "/$1/" }, + { from: "/content/(.*)", to: "/$1" }, { from: "/nfts", to: "/nft/" }, { from: "/daos", to: "/dao/" }, { from: "/layer2", to: "/layer-2/" }, From 6531cd743f9d8fd99d8289d61ad9ca682dcf91a9 Mon Sep 17 00:00:00 2001 From: Paul Wackerow <54227730+wackerow@users.noreply.github.com> Date: Wed, 3 Dec 2025 08:22:01 -0300 Subject: [PATCH 10/15] refactor: pass optional type --- middleware.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/middleware.ts b/middleware.ts index 2b84c608837..ccfe9d7c894 100644 --- a/middleware.ts +++ b/middleware.ts @@ -9,8 +9,10 @@ const handleI18nRouting = createMiddleware(routing) // Build a strict locale matcher from configured locales const LOCALE_ALTS = routing.locales.join("|") +type Redirect = { from: string; to: string; type?: number } + // Define redirect patterns - these will work for both /path and /:locale/path -const redirectPatterns: { from: string; to: string }[] = [ +const redirectPatterns: Redirect[] = [ // Locale aliases { from: "/no/(.*)", to: "/nb/$1" }, { from: "/ph/(.*)", to: "/fil/$1" }, @@ -122,27 +124,22 @@ const redirectPatterns: { from: string; to: string }[] = [ ] // Build regex patterns for both non-prefixed and locale-prefixed paths -const redirects: { - from: RegExp - to: string - type: number -}[] = redirectPatterns.flatMap(({ from, to }) => { - return [ +const redirects = redirectPatterns.flatMap( + ({ from, to, type = 301 }: Redirect) => [ // Non-prefixed version (e.g., /cli) { from: new RegExp(`^${from}/?$`), to, - type: 301, + type, }, // Locale-prefixed version (e.g., /es/cli) { - // from: new RegExp(`^/(\\w+)${from}/?$`), from: new RegExp(`^/(${LOCALE_ALTS})${from}/?$`), to: `/$1${to}`, - type: 301, + type, }, ] -}) +) export default function middleware(request: NextRequest) { // Normalize to lowercase paths site-wide (URLs are case-insensitive by spec, From 568c9c852440b284e35808ff698e15572247f8df Mon Sep 17 00:00:00 2001 From: Paul Wackerow <54227730+wackerow@users.noreply.github.com> Date: Wed, 3 Dec 2025 10:48:40 -0300 Subject: [PATCH 11/15] refactor: use next.config redirects revert middleware updates --- middleware.ts | 155 +------------------------------------------------ next.config.js | 142 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 153 deletions(-) diff --git a/middleware.ts b/middleware.ts index ccfe9d7c894..7d9a4aaf929 100644 --- a/middleware.ts +++ b/middleware.ts @@ -6,141 +6,6 @@ import { DEFAULT_LOCALE } from "./src/lib/constants" const handleI18nRouting = createMiddleware(routing) -// Build a strict locale matcher from configured locales -const LOCALE_ALTS = routing.locales.join("|") - -type Redirect = { from: string; to: string; type?: number } - -// Define redirect patterns - these will work for both /path and /:locale/path -const redirectPatterns: Redirect[] = [ - // Locale aliases - { from: "/no/(.*)", to: "/nb/$1" }, - { from: "/ph/(.*)", to: "/fil/$1" }, - // Legacy redirects - { from: "/discord", to: "https://discord.gg/ethereum-org" }, - { from: "/pdfs/(.*)", to: "/" }, - { from: "/brand", to: "/assets/" }, - { from: "/ethereum\\.html", to: "/what-is-ethereum/" }, - { from: "/ether", to: "/eth/" }, - { from: "/token", to: "/developers/" }, - { from: "/crowdsale", to: "/developers/" }, - { from: "/cli", to: "/developers/" }, - { from: "/greeter", to: "/developers/" }, - { from: "/roadmap/vision", to: "/roadmap/" }, - { from: "/search", to: "/" }, - { from: "/garden", to: "/roadmap/" }, - { from: "/download", to: "/developers/docs/nodes-and-clients/" }, - { from: "/how", to: "/guides/" }, - { from: "/content/(.*)", to: "/$1" }, - { from: "/nfts", to: "/nft/" }, - { from: "/daos", to: "/dao/" }, - { from: "/layer2", to: "/layer-2/" }, - { from: "/grants", to: "/community/grants/" }, - { from: "/java", to: "/developers/docs/programming-languages/java/" }, - { from: "/python", to: "/developers/docs/programming-languages/python/" }, - { - from: "/javascript", - to: "/developers/docs/programming-languages/javascript/", - }, - { from: "/golang", to: "/developers/docs/programming-languages/golang/" }, - { from: "/rust", to: "/developers/docs/programming-languages/rust/" }, - { from: "/dot-net", to: "/developers/docs/programming-languages/dot-net/" }, - { from: "/delphi", to: "/developers/docs/programming-languages/delphi/" }, - { from: "/dart", to: "/developers/docs/programming-languages/dart/" }, - { from: "/languages", to: "/community/language-resources/" }, - { - from: "/developers/docs/mining", - to: "/developers/docs/consensus-mechanisms/pow/mining/", - }, - { from: "/beginners", to: "/what-is-ethereum/" }, - { from: "/build", to: "/developers/learning-tools/" }, - { from: "/eth2/beacon-chain", to: "/roadmap/beacon-chain/" }, - { from: "/eth2/the-beacon-chain", to: "/roadmap/beacon-chain/" }, - { from: "/upgrades/the-beacon-chain", to: "/roadmap/beacon-chain/" }, - { from: "/eth2/merge", to: "/roadmap/merge/" }, - { from: "/eth2/the-merge", to: "/roadmap/merge/" }, - { from: "/upgrades/the-merge", to: "/roadmap/merge/" }, - { from: "/eth2/docking", to: "/roadmap/merge/" }, - { from: "/upgrades/docking", to: "/roadmap/merge/" }, - { from: "/eth2/the-docking", to: "/roadmap/merge/" }, - { from: "/upgrades/the-docking", to: "/roadmap/merge/" }, - { from: "/eth2/shard-chains", to: "/roadmap/danksharding/" }, - { from: "/upgrades/shard-chains", to: "/roadmap/danksharding/" }, - { from: "/upgrades/sharding", to: "/roadmap/danksharding/" }, - { from: "/upgrades/merge", to: "/roadmap/merge/" }, - { from: "/upgrades/merge/issuance", to: "/roadmap/merge/issuance" }, - { from: "/upgrades/beacon-chain", to: "/roadmap/beacon-chain" }, - { from: "/upgrades/vision", to: "/roadmap/" }, - { from: "/upgrades", to: "/roadmap" }, - { from: "/upgrades/get-involved", to: "/contributing" }, - { from: "/eth2/staking", to: "/staking/" }, - { from: "/eth2/vision", to: "/roadmap/vision/" }, - { from: "/eth2/get-involved", to: "/upgrades/get-involved/" }, - { from: "/eth2/get-involved/bug-bounty", to: "/bug-bounty/" }, - { from: "/upgrades/get-involved/bug-bounty", to: "/bug-bounty/" }, - { from: "/eth2/deposit-contract", to: "/staking/deposit-contract/" }, - { from: "/eth2", to: "/upgrades/" }, - { - from: "/developers/docs/scaling/layer-2-rollups", - to: "/developers/docs/scaling", - }, - { from: "/developers/docs/layer-2-scaling", to: "/layer-2/" }, - { from: "/about/web-developer", to: "/about/#open-jobs" }, - { from: "/about/product-designer", to: "/about/#open-jobs" }, - { from: "/use", to: "/apps/" }, - { from: "/dapps", to: "/apps/" }, - { - from: "/contributing/translation-program/translation-guide", - to: "/contributing/translation-program/faq/", - }, - { - from: "/contributing/translation-program/content-versions", - to: "/contributing/translation-program/", - }, - { - from: "/contributing/translation-program/content-buckets", - to: "/contributing/translation-program/", - }, - { - from: "/developers/docs/smart-contracts/source-code-verification", - to: "/developers/docs/smart-contracts/verifying/", - }, - { - from: "/developers/docs/smart-contracts/upgrading-smart-contracts", - to: "/developers/docs/smart-contracts/upgrading/", - }, - { from: "/writing-cohort", to: "https://ethereumwriterscohort.carrd.co/" }, - { from: "/staking/withdraws", to: "/staking/withdrawals/" }, - { - from: "/guides/how-to-register-an-ethereum-account", - to: "/guides/how-to-create-an-ethereum-account/", - }, - { from: "/deprecated-software", to: "/apps/" }, - { from: "/enterprise/private-ethereum", to: "/enterprise/" }, - { from: "/dashboards", to: "/resources" }, - { from: "/tds", to: "/trillion-dollar-security" }, - { from: "/10-years", to: "/10years" }, - { from: "/history", to: "/ethereum-forks" }, -] - -// Build regex patterns for both non-prefixed and locale-prefixed paths -const redirects = redirectPatterns.flatMap( - ({ from, to, type = 301 }: Redirect) => [ - // Non-prefixed version (e.g., /cli) - { - from: new RegExp(`^${from}/?$`), - to, - type, - }, - // Locale-prefixed version (e.g., /es/cli) - { - from: new RegExp(`^/(${LOCALE_ALTS})${from}/?$`), - to: `/$1${to}`, - type, - }, - ] -) - export default function middleware(request: NextRequest) { // Normalize to lowercase paths site-wide (URLs are case-insensitive by spec, // but our routes are defined in lowercase). Do this BEFORE i18n routing. @@ -152,23 +17,7 @@ export default function middleware(request: NextRequest) { return NextResponse.redirect(url, 301) } - // Apply custom redirects BEFORE i18n routing - for (const rule of redirects) { - if (rule.from.test(lowerPath)) { - const replaced = lowerPath.replace(rule.from, rule.to) - - // External targets (e.g., discord invite) - if (replaced.startsWith("http://") || replaced.startsWith("https://")) { - return NextResponse.redirect(replaced, rule.type) - } - - // Internal targets - const url = request.nextUrl.clone() - url.pathname = replaced - return NextResponse.redirect(url, rule.type) - } - } - + // Handle i18n routing const response = handleI18nRouting(request) // Upgrade default-locale strip redirects from 307 to 301 for SEO @@ -188,5 +37,5 @@ export default function middleware(request: NextRequest) { // Simplified matcher pattern export const config = { - matcher: ["/((?!api|_next|_vercel|.well-known|.*\\.(?!html$)[^/]+$).*)"], + matcher: ["/((?!api|_next|_vercel|.well-known|.*\\.[^/]*$).*)"], } diff --git a/next.config.js b/next.config.js index 07b4140bf75..2b2bee3a299 100644 --- a/next.config.js +++ b/next.config.js @@ -9,6 +9,8 @@ const createNextIntlPlugin = require("next-intl/plugin") const { withSentryConfig } = require("@sentry/nextjs") +const i18nConfigJson = require("./i18n.config.json") + const withNextIntl = createNextIntlPlugin() const LIMIT_CPUS = Number(process.env.LIMIT_CPUS ?? 2) @@ -135,6 +137,146 @@ module.exports = (phase, { defaultConfig }) => { }, ] }, + async redirects() { + // Build a strict locale matcher from configured locales + const LOCALE_ALTS = i18nConfigJson.map(({ code }) => code).join("|") // e.g. "en|es|fr|..." + + // Helper function to generate both English (no prefix) and locale-prefixed redirects + const createRedirect = (from, to, permanent = true) => { + // For external URLs, don't modify the destination + const isExternal = to.startsWith("http") + + // English / default-locale: no prefix in source or destination + const defaultRedirect = { + source: `${from}`, + destination: to, + permanent, + } + + // Locale-prefixed: only match allowed locales (prevents matching arbitrary segments) + const localeRedirect = { + source: `/:locale(${LOCALE_ALTS})${from}`, + destination: isExternal ? to : `/:locale${to}`, + permanent, + } + + return [defaultRedirect, localeRedirect] + } + + return [ + // Custom locale aliases redirects + { source: "/no/:path*", destination: "/nb/:path*", permanent: true }, + { source: "/ph/:path*", destination: "/fil/:path*", permanent: true }, + + // All redirects ([from, to, permanent? (default true)]) + ...[ + ["/discord", "https://discord.gg/ethereum-org"], + ["/writing-cohort", "https://ethereumwriterscohort.carrd.co/"], + ["/pdfs/:path*", "/"], + ["/brand", "/assets/"], + ["/ethereum.html", "/what-is-ethereum/"], + ["/ether", "/what-is-ether/"], + ["/eth", "/what-is-ether/"], + ["/token", "/developers/"], + ["/crowdsale", "/developers/"], + ["/cli", "/developers/"], + ["/greeter", "/developers/"], + ["/roadmap/vision", "/roadmap/"], + ["/search", "/"], + ["/garden", "/roadmap/"], + ["/download", "/wallets/find-wallet/"], + ["/how", "/guides/"], + ["/content/:path*", "/:path*"], + ["/nfts", "/nft/"], + ["/daos", "/dao/"], + ["/layer2", "/layer-2/"], + ["/grants", "/community/grants/"], + ["/java", "/developers/docs/programming-languages/java/"], + ["/python", "/developers/docs/programming-languages/python/"], + ["/javascript", "/developers/docs/programming-languages/javascript/"], + ["/golang", "/developers/docs/programming-languages/golang/"], + ["/rust", "/developers/docs/programming-languages/rust/"], + ["/dot-net", "/developers/docs/programming-languages/dot-net/"], + ["/delphi", "/developers/docs/programming-languages/delphi/"], + ["/dart", "/developers/docs/programming-languages/dart/"], + ["/languages", "/community/language-resources/"], + [ + "/developers/docs/mining", + "/developers/docs/consensus-mechanisms/pow/mining/", + ], + ["/beginners", "/what-is-ethereum/"], + ["/build", "/developers/learning-tools/"], + ["/eth2/beacon-chain", "/roadmap/beacon-chain/"], + ["/eth2/the-beacon-chain", "/roadmap/beacon-chain/"], + ["/upgrades/the-beacon-chain", "/roadmap/beacon-chain/"], + ["/eth2/merge", "/roadmap/merge/"], + ["/eth2/the-merge", "/roadmap/merge/"], + ["/upgrades/the-merge", "/roadmap/merge/"], + ["/eth2/docking", "/roadmap/merge/"], + ["/upgrades/docking", "/roadmap/merge/"], + ["/eth2/the-docking", "/roadmap/merge/"], + ["/upgrades/the-docking", "/roadmap/merge/"], + ["/eth2/shard-chains", "/roadmap/danksharding/"], + ["/upgrades/shard-chains", "/roadmap/danksharding/"], + ["/upgrades/sharding", "/roadmap/danksharding/"], + ["/upgrades/merge", "/roadmap/merge/"], + ["/upgrades/merge/issuance", "/roadmap/merge/issuance"], + ["/upgrades/beacon-chain", "/roadmap/beacon-chain"], + ["/upgrades/vision", "/roadmap/"], + ["/upgrades", "/roadmap"], + ["/upgrades/get-involved", "/contributing"], + ["/eth2/staking", "/staking/"], + ["/eth2/vision", "/roadmap/vision/"], + ["/eth2/get-involved", "/upgrades/get-involved/"], + ["/eth2/get-involved/bug-bounty", "/bug-bounty/"], + ["/upgrades/get-involved/bug-bounty", "/bug-bounty/"], + ["/eth2/deposit-contract", "/staking/deposit-contract/"], + ["/eth2", "/upgrades/"], + [ + "/developers/docs/scaling/layer-2-rollups", + "/developers/docs/scaling", + ], + ["/developers/docs/layer-2-scaling", "/layer-2/"], + ["/about/web-developer", "/about/#open-jobs"], + ["/about/product-designer", "/about/#open-jobs"], + ["/use", "/apps/"], + ["/dapps", "/apps/"], + [ + "/contributing/translation-program/translation-guide", + "/contributing/translation-program/faq/", + ], + [ + "/contributing/translation-program/content-versions", + "/contributing/translation-program/", + ], + [ + "/contributing/translation-program/content-buckets", + "/contributing/translation-program/", + ], + [ + "/developers/docs/smart-contracts/source-code-verification", + "/developers/docs/smart-contracts/verifying/", + ], + [ + "/developers/docs/smart-contracts/upgrading-smart-contracts", + "/developers/docs/smart-contracts/upgrading/", + ], + ["/staking/withdraws", "/staking/withdrawals/"], + [ + "/guides/how-to-register-an-ethereum-account", + "/guides/how-to-create-an-ethereum-account/", + ], + ["/deprecated-software", "/apps/"], + ["/enterprise/private-ethereum", "/enterprise/"], + ["/dashboards", "/resources"], + ["/tds", "/trillion-dollar-security"], + ["/10-years", "/10years"], + ["/history", "/ethereum-forks"], + ].flatMap(([from, to, permanent]) => + createRedirect(from, to, permanent) + ), + ] + }, } nextConfig = { From 923f76e87496b3715f2f6cc65b60004d77c1447f Mon Sep 17 00:00:00 2001 From: Paul Wackerow <54227730+wackerow@users.noreply.github.com> Date: Wed, 3 Dec 2025 13:16:56 -0300 Subject: [PATCH 12/15] patch: redirect destinations --- next.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/next.config.js b/next.config.js index 2b2bee3a299..635b0d90cf2 100644 --- a/next.config.js +++ b/next.config.js @@ -227,11 +227,11 @@ module.exports = (phase, { defaultConfig }) => { ["/upgrades/get-involved", "/contributing"], ["/eth2/staking", "/staking/"], ["/eth2/vision", "/roadmap/vision/"], - ["/eth2/get-involved", "/upgrades/get-involved/"], + ["/eth2/get-involved", "/contributing/"], ["/eth2/get-involved/bug-bounty", "/bug-bounty/"], ["/upgrades/get-involved/bug-bounty", "/bug-bounty/"], ["/eth2/deposit-contract", "/staking/deposit-contract/"], - ["/eth2", "/upgrades/"], + ["/eth2", "/roadmap/"], [ "/developers/docs/scaling/layer-2-rollups", "/developers/docs/scaling", From 9f8dc518e95364a40c05b61d3869941401d211bd Mon Sep 17 00:00:00 2001 From: Paul Wackerow <54227730+wackerow@users.noreply.github.com> Date: Wed, 3 Dec 2025 13:30:41 -0300 Subject: [PATCH 13/15] patch: redundant string interpolation --- next.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/next.config.js b/next.config.js index 635b0d90cf2..7d810561c46 100644 --- a/next.config.js +++ b/next.config.js @@ -148,7 +148,7 @@ module.exports = (phase, { defaultConfig }) => { // English / default-locale: no prefix in source or destination const defaultRedirect = { - source: `${from}`, + source: from, destination: to, permanent, } From ebc820841ebade74939328f743a86f5b24aa81c6 Mon Sep 17 00:00:00 2001 From: Paul Wackerow <54227730+wackerow@users.noreply.github.com> Date: Wed, 3 Dec 2025 13:52:22 -0300 Subject: [PATCH 14/15] refactor: variable naming --- next.config.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/next.config.js b/next.config.js index 7d810561c46..cd7ced3d50c 100644 --- a/next.config.js +++ b/next.config.js @@ -142,21 +142,17 @@ module.exports = (phase, { defaultConfig }) => { const LOCALE_ALTS = i18nConfigJson.map(({ code }) => code).join("|") // e.g. "en|es|fr|..." // Helper function to generate both English (no prefix) and locale-prefixed redirects - const createRedirect = (from, to, permanent = true) => { + const createRedirect = (source, destination, permanent = true) => { // For external URLs, don't modify the destination - const isExternal = to.startsWith("http") + const isExternal = destination.startsWith("http") // English / default-locale: no prefix in source or destination - const defaultRedirect = { - source: from, - destination: to, - permanent, - } + const defaultRedirect = { source, destination, permanent } // Locale-prefixed: only match allowed locales (prevents matching arbitrary segments) const localeRedirect = { - source: `/:locale(${LOCALE_ALTS})${from}`, - destination: isExternal ? to : `/:locale${to}`, + source: `/:locale(${LOCALE_ALTS})${source}`, + destination: isExternal ? destination : `/:locale${destination}`, permanent, } From 97f545de902199c687e5654cad6677af96d87035 Mon Sep 17 00:00:00 2001 From: Paul Wackerow <54227730+wackerow@users.noreply.github.com> Date: Wed, 3 Dec 2025 13:57:31 -0300 Subject: [PATCH 15/15] refactor: move redirects to redirects.config.js --- next.config.js | 111 ++------------------------------------------ redirects.config.js | 104 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 106 deletions(-) create mode 100644 redirects.config.js diff --git a/next.config.js b/next.config.js index cd7ced3d50c..7a70e377634 100644 --- a/next.config.js +++ b/next.config.js @@ -9,6 +9,8 @@ const createNextIntlPlugin = require("next-intl/plugin") const { withSentryConfig } = require("@sentry/nextjs") +const redirects = require("./redirects.config") + const i18nConfigJson = require("./i18n.config.json") const withNextIntl = createNextIntlPlugin() @@ -164,112 +166,9 @@ module.exports = (phase, { defaultConfig }) => { { source: "/no/:path*", destination: "/nb/:path*", permanent: true }, { source: "/ph/:path*", destination: "/fil/:path*", permanent: true }, - // All redirects ([from, to, permanent? (default true)]) - ...[ - ["/discord", "https://discord.gg/ethereum-org"], - ["/writing-cohort", "https://ethereumwriterscohort.carrd.co/"], - ["/pdfs/:path*", "/"], - ["/brand", "/assets/"], - ["/ethereum.html", "/what-is-ethereum/"], - ["/ether", "/what-is-ether/"], - ["/eth", "/what-is-ether/"], - ["/token", "/developers/"], - ["/crowdsale", "/developers/"], - ["/cli", "/developers/"], - ["/greeter", "/developers/"], - ["/roadmap/vision", "/roadmap/"], - ["/search", "/"], - ["/garden", "/roadmap/"], - ["/download", "/wallets/find-wallet/"], - ["/how", "/guides/"], - ["/content/:path*", "/:path*"], - ["/nfts", "/nft/"], - ["/daos", "/dao/"], - ["/layer2", "/layer-2/"], - ["/grants", "/community/grants/"], - ["/java", "/developers/docs/programming-languages/java/"], - ["/python", "/developers/docs/programming-languages/python/"], - ["/javascript", "/developers/docs/programming-languages/javascript/"], - ["/golang", "/developers/docs/programming-languages/golang/"], - ["/rust", "/developers/docs/programming-languages/rust/"], - ["/dot-net", "/developers/docs/programming-languages/dot-net/"], - ["/delphi", "/developers/docs/programming-languages/delphi/"], - ["/dart", "/developers/docs/programming-languages/dart/"], - ["/languages", "/community/language-resources/"], - [ - "/developers/docs/mining", - "/developers/docs/consensus-mechanisms/pow/mining/", - ], - ["/beginners", "/what-is-ethereum/"], - ["/build", "/developers/learning-tools/"], - ["/eth2/beacon-chain", "/roadmap/beacon-chain/"], - ["/eth2/the-beacon-chain", "/roadmap/beacon-chain/"], - ["/upgrades/the-beacon-chain", "/roadmap/beacon-chain/"], - ["/eth2/merge", "/roadmap/merge/"], - ["/eth2/the-merge", "/roadmap/merge/"], - ["/upgrades/the-merge", "/roadmap/merge/"], - ["/eth2/docking", "/roadmap/merge/"], - ["/upgrades/docking", "/roadmap/merge/"], - ["/eth2/the-docking", "/roadmap/merge/"], - ["/upgrades/the-docking", "/roadmap/merge/"], - ["/eth2/shard-chains", "/roadmap/danksharding/"], - ["/upgrades/shard-chains", "/roadmap/danksharding/"], - ["/upgrades/sharding", "/roadmap/danksharding/"], - ["/upgrades/merge", "/roadmap/merge/"], - ["/upgrades/merge/issuance", "/roadmap/merge/issuance"], - ["/upgrades/beacon-chain", "/roadmap/beacon-chain"], - ["/upgrades/vision", "/roadmap/"], - ["/upgrades", "/roadmap"], - ["/upgrades/get-involved", "/contributing"], - ["/eth2/staking", "/staking/"], - ["/eth2/vision", "/roadmap/vision/"], - ["/eth2/get-involved", "/contributing/"], - ["/eth2/get-involved/bug-bounty", "/bug-bounty/"], - ["/upgrades/get-involved/bug-bounty", "/bug-bounty/"], - ["/eth2/deposit-contract", "/staking/deposit-contract/"], - ["/eth2", "/roadmap/"], - [ - "/developers/docs/scaling/layer-2-rollups", - "/developers/docs/scaling", - ], - ["/developers/docs/layer-2-scaling", "/layer-2/"], - ["/about/web-developer", "/about/#open-jobs"], - ["/about/product-designer", "/about/#open-jobs"], - ["/use", "/apps/"], - ["/dapps", "/apps/"], - [ - "/contributing/translation-program/translation-guide", - "/contributing/translation-program/faq/", - ], - [ - "/contributing/translation-program/content-versions", - "/contributing/translation-program/", - ], - [ - "/contributing/translation-program/content-buckets", - "/contributing/translation-program/", - ], - [ - "/developers/docs/smart-contracts/source-code-verification", - "/developers/docs/smart-contracts/verifying/", - ], - [ - "/developers/docs/smart-contracts/upgrading-smart-contracts", - "/developers/docs/smart-contracts/upgrading/", - ], - ["/staking/withdraws", "/staking/withdrawals/"], - [ - "/guides/how-to-register-an-ethereum-account", - "/guides/how-to-create-an-ethereum-account/", - ], - ["/deprecated-software", "/apps/"], - ["/enterprise/private-ethereum", "/enterprise/"], - ["/dashboards", "/resources"], - ["/tds", "/trillion-dollar-security"], - ["/10-years", "/10years"], - ["/history", "/ethereum-forks"], - ].flatMap(([from, to, permanent]) => - createRedirect(from, to, permanent) + // All primary redirects + ...redirects.flatMap(([source, destination, permanent]) => + createRedirect(source, destination, permanent) ), ] }, diff --git a/redirects.config.js b/redirects.config.js new file mode 100644 index 00000000000..9b971f002df --- /dev/null +++ b/redirects.config.js @@ -0,0 +1,104 @@ +// All primary redirects ([source, destination, permanent? (default true)]) + +/** @type { [string, string, boolean | undefined][] } */ +module.exports = [ + ["/discord", "https://discord.gg/ethereum-org"], + ["/writing-cohort", "https://ethereumwriterscohort.carrd.co/"], + ["/pdfs/:path*", "/"], + ["/brand", "/assets/"], + ["/ethereum.html", "/what-is-ethereum/"], + ["/ether", "/what-is-ether/"], + ["/eth", "/what-is-ether/"], + ["/token", "/developers/"], + ["/crowdsale", "/developers/"], + ["/cli", "/developers/"], + ["/greeter", "/developers/"], + ["/roadmap/vision", "/roadmap/"], + ["/search", "/"], + ["/garden", "/roadmap/"], + ["/download", "/wallets/find-wallet/"], + ["/how", "/guides/"], + ["/content/:path*", "/:path*"], + ["/nfts", "/nft/"], + ["/daos", "/dao/"], + ["/layer2", "/layer-2/"], + ["/grants", "/community/grants/"], + ["/java", "/developers/docs/programming-languages/java/"], + ["/python", "/developers/docs/programming-languages/python/"], + ["/javascript", "/developers/docs/programming-languages/javascript/"], + ["/golang", "/developers/docs/programming-languages/golang/"], + ["/rust", "/developers/docs/programming-languages/rust/"], + ["/dot-net", "/developers/docs/programming-languages/dot-net/"], + ["/delphi", "/developers/docs/programming-languages/delphi/"], + ["/dart", "/developers/docs/programming-languages/dart/"], + ["/languages", "/community/language-resources/"], + [ + "/developers/docs/mining", + "/developers/docs/consensus-mechanisms/pow/mining/", + ], + ["/beginners", "/what-is-ethereum/"], + ["/build", "/developers/learning-tools/"], + ["/eth2/beacon-chain", "/roadmap/beacon-chain/"], + ["/eth2/the-beacon-chain", "/roadmap/beacon-chain/"], + ["/upgrades/the-beacon-chain", "/roadmap/beacon-chain/"], + ["/eth2/merge", "/roadmap/merge/"], + ["/eth2/the-merge", "/roadmap/merge/"], + ["/upgrades/the-merge", "/roadmap/merge/"], + ["/eth2/docking", "/roadmap/merge/"], + ["/upgrades/docking", "/roadmap/merge/"], + ["/eth2/the-docking", "/roadmap/merge/"], + ["/upgrades/the-docking", "/roadmap/merge/"], + ["/eth2/shard-chains", "/roadmap/danksharding/"], + ["/upgrades/shard-chains", "/roadmap/danksharding/"], + ["/upgrades/sharding", "/roadmap/danksharding/"], + ["/upgrades/merge", "/roadmap/merge/"], + ["/upgrades/merge/issuance", "/roadmap/merge/issuance"], + ["/upgrades/beacon-chain", "/roadmap/beacon-chain"], + ["/upgrades/vision", "/roadmap/"], + ["/upgrades", "/roadmap"], + ["/upgrades/get-involved", "/contributing"], + ["/eth2/staking", "/staking/"], + ["/eth2/vision", "/roadmap/vision/"], + ["/eth2/get-involved", "/contributing/"], + ["/eth2/get-involved/bug-bounty", "/bug-bounty/"], + ["/upgrades/get-involved/bug-bounty", "/bug-bounty/"], + ["/eth2/deposit-contract", "/staking/deposit-contract/"], + ["/eth2", "/roadmap/"], + ["/developers/docs/scaling/layer-2-rollups", "/developers/docs/scaling"], + ["/developers/docs/layer-2-scaling", "/layer-2/"], + ["/about/web-developer", "/about/#open-jobs"], + ["/about/product-designer", "/about/#open-jobs"], + ["/use", "/apps/"], + ["/dapps", "/apps/"], + [ + "/contributing/translation-program/translation-guide", + "/contributing/translation-program/faq/", + ], + [ + "/contributing/translation-program/content-versions", + "/contributing/translation-program/", + ], + [ + "/contributing/translation-program/content-buckets", + "/contributing/translation-program/", + ], + [ + "/developers/docs/smart-contracts/source-code-verification", + "/developers/docs/smart-contracts/verifying/", + ], + [ + "/developers/docs/smart-contracts/upgrading-smart-contracts", + "/developers/docs/smart-contracts/upgrading/", + ], + ["/staking/withdraws", "/staking/withdrawals/"], + [ + "/guides/how-to-register-an-ethereum-account", + "/guides/how-to-create-an-ethereum-account/", + ], + ["/deprecated-software", "/apps/"], + ["/enterprise/private-ethereum", "/enterprise/"], + ["/dashboards", "/resources"], + ["/tds", "/trillion-dollar-security"], + ["/10-years", "/10years"], + ["/history", "/ethereum-forks"], +]