diff --git a/middleware.ts b/middleware.ts
index 97d6926076d..7d9a4aaf929 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -7,6 +7,17 @@ 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)
+ }
+
+ // Handle i18n routing
const response = handleI18nRouting(request)
// Upgrade default-locale strip redirects from 307 to 301 for SEO
diff --git a/next.config.js b/next.config.js
index 07b4140bf75..7a70e377634 100644
--- a/next.config.js
+++ b/next.config.js
@@ -9,6 +9,10 @@ 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()
const LIMIT_CPUS = Number(process.env.LIMIT_CPUS ?? 2)
@@ -135,6 +139,39 @@ 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 = (source, destination, permanent = true) => {
+ // For external URLs, don't modify the destination
+ const isExternal = destination.startsWith("http")
+
+ // English / default-locale: no prefix in source or destination
+ const defaultRedirect = { source, destination, permanent }
+
+ // Locale-prefixed: only match allowed locales (prevents matching arbitrary segments)
+ const localeRedirect = {
+ source: `/:locale(${LOCALE_ALTS})${source}`,
+ destination: isExternal ? destination : `/:locale${destination}`,
+ 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 primary redirects
+ ...redirects.flatMap(([source, destination, permanent]) =>
+ createRedirect(source, destination, permanent)
+ ),
+ ]
+ },
}
nextConfig = {
diff --git a/public/_redirects b/public/_redirects
deleted file mode 100644
index c9770edeefa..00000000000
--- a/public/_redirects
+++ /dev/null
@@ -1,190 +0,0 @@
-/discord https://discord.gg/ethereum-org 301!
-
-/*/discord https://discord.gg/ethereum-org 301!
-
-/pdfs/* / 301!
-
-/brand /assets/ 301!
-
-/ether /what-is-ether/ 301!
-
-/token /developers/ 301!
-
-/crowdsale /developers/ 301!
-
-/cli /developers/ 301!
-
-/greeter /developers/ 301!
-
-/search / 301!
-
-/use /apps/ 301!
-
-/dapps /apps/ 301!
-
-/beginners /what-is-ethereum/ 301!
-
-/eth2/ /roadmap/ 301!
-
-/build/ /developers/learning-tools/ 301!
-
-/nfts/ /nft/ 301!
-
-/payments/ /payments/ 301!
-
-/daos/ /dao/ 301!
-
-/layer2/ /layer-2/ 301!
-
-/*/layer2/ /:splat/layer-2/ 301!
-
-/grants/ /community/grants/ 301!
-
-/no/* /nb/:splat 301!
-
-/ph/* /fil/:splat 301!
-
-/java/ /developers/docs/programming-languages/java/ 301!
-
-/python/ /developers/docs/programming-languages/python/ 301!
-
-/javascript/ /developers/docs/programming-languages/javascript/ 301!
-
-/golang/ /developers/docs/programming-languages/golang/ 301!
-
-/rust/ /developers/docs/programming-languages/rust/ 301!
-
-/dot-net/ /developers/docs/programming-languages/dot-net/ 301!
-
-/delphi/ /developers/docs/programming-languages/delphi/ 301!
-
-/dart/ /developers/docs/programming-languages/dart/ 301!
-
-/developers/docs/mining/ /developers/docs/consensus-mechanisms/pow/mining/ 301!
-
-/*/beginners /:splat/what-is-ethereum/ 301!
-
-/*/build /:splat/developers/learning-tools/ 301!
-
-/*/eth2/beacon-chain/ /:splat/upgrades/beacon-chain/ 301!
-
-/*/eth2/the-beacon-chain/ /:splat/upgrades/beacon-chain/ 301!
-
-/*/upgrades/the-beacon-chain/ /:splat/upgrades/beacon-chain/ 301!
-
-/*/eth2/merge /:splat/upgrades/merge/ 301!
-
-/*/eth2/the-merge /:splat/upgrades/merge/ 301!
-
-/*/upgrades/the-merge /:splat/upgrades/merge/ 301!
-
-/*/eth2/docking /:splat/upgrades/merge/ 301!
-
-/*/upgrades/docking /:splat/upgrades/merge/ 301!
-
-/*/eth2/the-docking /:splat/upgrades/merge/ 301!
-
-/*/upgrades/the-docking /:splat/upgrades/merge/ 301!
-
-/*/eth2/shard-chains/ /:splat/roadmap/danksharding/ 301!
-
-/*/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!
-
-/*/upgrades /:splat/roadmap 301!
-
-/upgrades/get-involved /contributing 301!
-
-/*/upgrades/get-involved /:splat/contributing 301!
-
-/*/eth2/staking/ /:splat/staking/ 301!
-
-/*/eth2/vision/ /:splat/roadmap/vision/ 301!
-
-/*/eth2/get-involved/ /:splat/upgrades/get-involved/ 301!
-
-/*/eth2/get-involved/bug-bounty/ /:splat/bug-bounty/ 301!
-
-/*/upgrades/get-involved/bug-bounty/ /:splat/bug-bounty/ 301!
-
-/*/eth2/deposit-contract/ /:splat/staking/deposit-contract/ 301!
-
-/*/eth2 /:splat/upgrades/ 301!
-
-/*/developers/docs/scaling/layer-2-rollups /:splat/developers/docs/scaling 301!
-
-/*/about/web-developer /:splat/about/#open-jobs 301!
-
-/*/about/product-designer /:splat/about/#open-jobs 301!
-
-/*/use /:splat/apps/ 301!
-
-# Exception: don't redirect developers/docs/dapps paths
-/*/developers/docs/dapps* /:splat/developers/docs/dapps:splat 200!
-
-/*/dapps /:splat/apps/ 301!
-
-/*/contributing/translation-program/translation-guide/ /:splat/contributing/translation-program/faq/ 301!
-
-/*/contributing/translation-program/content-versions/ /:splat/contributing/translation-program/ 301!
-
-/*/contributing/translation-program/content-buckets/ /:splat/contributing/translation-program/ 301!
-
-/*/developers/docs/smart-contracts/source-code-verification/ /:splat/developers/docs/smart-contracts/verifying/ 301!
-
-/*/developers/docs/smart-contracts/upgrading-smart-contracts/ /:splat/developers/docs/smart-contracts/upgrading/ 301!
-
-/staking/withdraws /staking/withdrawals/ 301!
-
-/*/writing-cohort https://ethereumwriterscohort.carrd.co/ 301!
-
-/*/staking/withdraws /:splat/staking/withdrawals/ 301!
-
-/*/guides/how-to-register-an-ethereum-account /:splat/guides/how-to-create-an-ethereum-account/ 301!
-
-/*/deprecated-software /:splat/apps/ 301!
-
-/*/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!
-
-/eth /what-is-ether 301!
-
-/*/eth /:splat/what-is-ether 301!
diff --git a/public/content/ethereum-forks/index.md b/public/content/ethereum-forks/index.md
index 48073f596e5..3dbac7db7dd 100644
--- a/public/content/ethereum-forks/index.md
+++ b/public/content/ethereum-forks/index.md
@@ -73,7 +73,7 @@ Looking for future protocol upgrades? [Learn about upcoming upgrades on the Ethe
## 2025 {#2025}
-### Fulu-Osaka ("Fusaka", _in progress_) {#fusaka}
+### Fulu-Osaka ("Fusaka") {#fusaka}
{liveNowText}
+ return ( ++ {liveNowText} +
+ ) } return ( -+
{String(timeUnits.days).padStart(2, "0")}
@@ -144,13 +148,13 @@ const FusakaCountdown = ({ liveNowText }: { liveNowText: string }) => {
+
{String(timeUnits.hours).padStart(2, "0")}
{labels.hours}
+
{String(timeUnits.minutes).padStart(2, "0")}
@@ -159,7 +163,7 @@ const FusakaCountdown = ({ liveNowText }: { liveNowText: string }) => {
+
{String(timeUnits.seconds).padStart(2, "0")}
diff --git a/src/components/Banners/FusakaBanner/index.tsx b/src/components/Banners/FusakaBanner/index.tsx
index 0171b498392..17d1b1f0cad 100644
--- a/src/components/Banners/FusakaBanner/index.tsx
+++ b/src/components/Banners/FusakaBanner/index.tsx
@@ -9,28 +9,27 @@ const FusakaBanner = async () => {
const t = await getTranslations({ locale, namespace: "page-index" })
return (
-
+
FUSAKA
+
{t("page-index-fusaka-network-upgrade")}
+
{t("page-index-fusaka-description")}{" "}
+
{t.rich("page-index-fusaka-going-live-in", {
br: () =>
,
})}
diff --git a/src/data/networkUpgradeSummaryData.ts b/src/data/networkUpgradeSummaryData.ts
index 8b39b45d27c..d72cbb91de3 100644
--- a/src/data/networkUpgradeSummaryData.ts
+++ b/src/data/networkUpgradeSummaryData.ts
@@ -3,9 +3,12 @@ import type { NetworkUpgradeData } from "@/lib/types"
const networkUpgradeSummaryData: NetworkUpgradeData = {
fusaka: {
dateTimeAsString: "2025-12-03T21:49:11.000Z",
+ ethPriceInUSD: 3149,
+ waybackLink:
+ "https://web.archive.org/web/20251203214416/https://ethereum.org/",
+ blockNumber: 23935694,
epochNumber: 411392,
slotNumber: 411392 * 32,
- isPending: true,
},
pectra: {
dateTimeAsString: "2025-05-07T10:05:11.000Z",