diff --git a/app/[locale]/home-2026/page.tsx b/app/[locale]/home-2026/page.tsx
deleted file mode 100644
index 784a36d83c4..00000000000
--- a/app/[locale]/home-2026/page.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import { notFound } from "next/navigation"
-import { getTranslations, setRequestLocale } from "next-intl/server"
-
-import type { Lang, PageParams } from "@/lib/types"
-
-import Homepage2026 from "@/components/Homepage/Homepage2026"
-
-import { getMetadata } from "@/lib/utils/metadata"
-
-import { LOCALES_CODES } from "@/lib/constants"
-
-import { getAccountHolders, getGrowThePieData } from "@/lib/data"
-
-const Page = async ({ params }: { params: PageParams }) => {
- const { locale } = params
-
- if (!LOCALES_CODES.includes(locale)) return notFound()
-
- setRequestLocale(locale)
-
- const [growThePieData, accountHolders] = await Promise.all([
- getGrowThePieData(),
- getAccountHolders(),
- ])
-
- // Handle null cases - throw error if required data is missing
- if (!growThePieData) {
- throw new Error("Failed to fetch GrowThePie data")
- }
- if (!accountHolders || "error" in accountHolders) {
- throw new Error("Failed to fetch account holders data")
- }
-
- const accountHoldersValue = accountHolders.value
- const transactionsToday =
- "value" in growThePieData.txCount ? growThePieData.txCount.value : 0
-
- return (
-
- )
-}
-
-export default Page
-
-export async function generateMetadata({
- params,
-}: {
- params: { locale: string }
-}) {
- const { locale } = params
-
- const t = await getTranslations({ locale, namespace: "page-index" })
- return getMetadata({
- locale,
- slug: ["home-2026"],
- title: t("page-index-meta-title"),
- description: t("page-index-meta-description"),
- })
-}
diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx
index a48003ed660..8bf9c76f570 100644
--- a/app/[locale]/page.tsx
+++ b/app/[locale]/page.tsx
@@ -7,16 +7,19 @@ import { getTranslations, setRequestLocale } from "next-intl/server"
import type {
AllHomepageActivityData,
CommunityBlog,
+ Lang,
PageParams,
ValuesPairing,
} from "@/lib/types"
import { CodeExample } from "@/lib/interfaces"
+import ABTestWrapper from "@/components/AB/TestWrapper"
import ActivityStats from "@/components/ActivityStats"
import { ChevronNext } from "@/components/Chevron"
import HomeHero from "@/components/Hero/HomeHero"
import BentoCard from "@/components/Homepage/BentoCard"
import CodeExamples from "@/components/Homepage/CodeExamples"
+import Homepage2026 from "@/components/Homepage/Homepage2026"
import HomepageSectionImage from "@/components/Homepage/HomepageSectionImage"
import { getBentoBoxItems } from "@/components/Homepage/utils"
import ValuesMarqueeFallback from "@/components/Homepage/ValuesMarquee/Fallback"
@@ -35,7 +38,9 @@ import { Image } from "@/components/Image"
import CardImage from "@/components/Image/CardImage"
import IntersectionObserverReveal from "@/components/IntersectionObserverReveal"
import MainArticle from "@/components/MainArticle"
+import ScrollDepthTracker from "@/components/ScrollDepthTracker"
import Tooltip from "@/components/Tooltip"
+import { TrackedSection } from "@/components/TrackedSection"
import { ButtonLink } from "@/components/ui/buttons/Button"
import SvgButtonLink, {
type SvgButtonLinkProps,
@@ -79,6 +84,7 @@ import IndexPageJsonLD from "./page-jsonld"
import { getActivity } from "./utils"
import {
+ getAccountHolders,
getAppsData,
getAttestantPosts,
getBeaconchainData,
@@ -90,6 +96,9 @@ import {
} from "@/lib/data"
import EventFallback from "@/public/images/events/event-placeholder.png"
+// Force dynamic rendering to read headers for A/B testing
+export const dynamic = "force-dynamic"
+
const BentoCardSwiper = nextDynamic(
() => import("@/components/Homepage/BentoCardSwiper"),
{
@@ -136,6 +145,8 @@ const Page = async ({ params }: { params: PageParams }) => {
const { direction: dir, isRtl } = getDirection(locale)
// Fetch data using the new data-layer functions (already cached)
+ // Each fetch is wrapped with .catch() to prevent Promise.all from rejecting entirely
+ // when a single API fails - enables graceful degradation
const [
ethPrice,
beaconchainData,
@@ -145,61 +156,102 @@ const Page = async ({ params }: { params: PageParams }) => {
rssData,
appsData,
eventsData,
+ accountHolders,
] = await Promise.all([
- getEthPrice(),
- getBeaconchainData(),
- getTotalValueLockedData(),
- getGrowThePieData(),
- getAttestantPosts(),
- getRSSData(),
- getAppsData(),
- getEventsData(),
+ getEthPrice().catch(() => null),
+ getBeaconchainData().catch(() => null),
+ getTotalValueLockedData().catch(() => null),
+ getGrowThePieData().catch(() => null),
+ getAttestantPosts().catch(() => null),
+ getRSSData().catch(() => null),
+ getAppsData().catch(() => null),
+ getEventsData().catch(() => null),
+ getAccountHolders().catch(() => null),
])
- // Handle null cases - throw error if required data is missing
+ // Graceful degradation: log errors and use fallback values
+ // With force-dynamic, there's no ISR cache to fall back to, so we must handle failures gracefully
+
+ // Error fallback helper
+ const createErrorMetric = (error: string) => ({ error })
+
+ // ETH Price - show "—" on failure
if (!ethPrice) {
- throw new Error("Failed to fetch ETH price data")
+ console.error("[Homepage] Failed to fetch ETH price data")
}
+ const safeEthPrice =
+ ethPrice ?? createErrorMetric("Failed to fetch ETH price")
+
+ // Beaconchain data - show "—" on failure
if (!beaconchainData) {
- throw new Error("Failed to fetch Beaconchain data")
+ console.error("[Homepage] Failed to fetch Beaconchain data")
}
+ const totalEthStaked =
+ beaconchainData?.totalEthStaked ??
+ createErrorMetric("Failed to fetch staked ETH")
+
+ // Total Value Locked - show "—" on failure
if (!totalValueLocked) {
- throw new Error("Failed to fetch total value locked data")
+ console.error("[Homepage] Failed to fetch TVL data")
}
+ const safeTotalValueLocked =
+ totalValueLocked ?? createErrorMetric("Failed to fetch TVL")
+
+ // GrowThePie data - show "—" on failure
if (!growThePieData) {
- throw new Error("Failed to fetch GrowThePie data")
+ console.error("[Homepage] Failed to fetch GrowThePie data")
+ }
+ const safeTxCount =
+ growThePieData?.txCount ?? createErrorMetric("Failed to fetch tx count")
+ const safeTxCostsMedianUsd =
+ growThePieData?.txCostsMedianUsd ??
+ createErrorMetric("Failed to fetch tx costs")
+
+ // Account holders - show "—" on failure (only used by redesign variants)
+ if (!accountHolders || "error" in accountHolders) {
+ console.error("[Homepage] Failed to fetch account holders data")
}
+ const accountHoldersValue =
+ accountHolders && "value" in accountHolders ? accountHolders.value : null
+
+ // Transactions today for KPIs (redesign variants) - show "—" on failure
+ const transactionsToday =
+ growThePieData && "value" in growThePieData.txCount
+ ? growThePieData.txCount.value
+ : null
+
+ // Apps data - hide section on failure
if (!appsData) {
- throw new Error("Failed to fetch apps data")
+ console.error("[Homepage] Failed to fetch apps data")
}
+ const hasAppsData = !!appsData
- // RSS feeds - graceful degradation: use what's available if we have enough items
+ // RSS feeds - hide section if insufficient items
const rssFeeds = rssData ?? []
const attestantFeed = attestantPosts ?? []
const totalRssItems =
rssFeeds.reduce((sum, feed) => sum + feed.length, 0) + attestantFeed.length
if (totalRssItems < RSS_DISPLAY_COUNT) {
- throw new Error(
- `Insufficient RSS data: need at least ${RSS_DISPLAY_COUNT} items`
+ console.error(
+ `[Homepage] Insufficient RSS data: have ${totalRssItems}, need ${RSS_DISPLAY_COUNT}`
)
}
-
- // Extract totalEthStaked from beaconchainData
- const { totalEthStaked } = beaconchainData
+ const hasEnoughRssItems = totalRssItems >= RSS_DISPLAY_COUNT
// Events - use empty array as fallback
const upcomingEvents = (eventsData ?? []).slice(0, 3)
- const appsOfTheWeek = parseAppsOfTheWeek(appsData)
+ // Apps of the week - only parse if we have data
+ const appsOfTheWeek = hasAppsData ? parseAppsOfTheWeek(appsData) : []
const bentoItems = await getBentoBoxItems(locale)
- const ethPriceHasError = "error" in ethPrice
+ const ethPriceHasError = "error" in safeEthPrice
const price = ethPriceHasError
- ? t("loading-error-refresh")
- : formatPriceUSD(ethPrice.value, locale)
+ ? "—"
+ : formatPriceUSD(safeEthPrice.value, locale)
const eventCategory = `Homepage - ${locale}`
@@ -210,7 +262,7 @@ const Page = async ({ params }: { params: PageParams }) => {
href: "/wallets/find-wallet/",
Svg: PickWalletIcon,
className: "text-primary hover:text-primary-hover",
- eventName: "find wallet",
+ eventName: "find_wallet",
},
{
label: t("page-index-cta-get-eth-label"),
@@ -218,7 +270,7 @@ const Page = async ({ params }: { params: PageParams }) => {
href: "/get-eth/",
Svg: EthTokenIcon,
className: "text-accent-a hover:text-accent-a-hover",
- eventName: "get eth",
+ eventName: "get_eth",
},
{
label: t("page-index-cta-dapps-label"),
@@ -229,7 +281,7 @@ const Page = async ({ params }: { params: PageParams }) => {
"text-accent-c hover:text-accent-c-hover",
isRtl && "[&_svg]:-scale-x-100"
),
- eventName: "dapps",
+ eventName: "try_apps",
},
{
label: t("page-index-cta-build-apps-label"),
@@ -237,7 +289,7 @@ const Page = async ({ params }: { params: PageParams }) => {
href: "/developers/",
Svg: BuildAppsIcon,
className: "text-accent-b hover:text-accent-b-hover",
- eventName: "build apps",
+ eventName: "start_building",
},
]
@@ -425,536 +477,616 @@ const Page = async ({ params }: { params: PageParams }) => {
]
const metricResults: AllHomepageActivityData = {
- ethPrice,
+ ethPrice: safeEthPrice,
totalEthStaked,
- totalValueLocked,
- txCount: growThePieData.txCount,
- txCostsMedianUsd: growThePieData.txCostsMedianUsd,
+ totalValueLocked: safeTotalValueLocked,
+ txCount: safeTxCount,
+ txCostsMedianUsd: safeTxCostsMedianUsd,
}
const metrics = await getActivity(metricResults, locale)
- // RSS feed items
- // polishRSSList expects RSSItem[][], so wrap attestantFeed in an array
- const polishedRssItems = polishRSSList([attestantFeed, ...rssFeeds], locale)
+ // RSS feed items - only process if we have enough items
+ const polishedRssItems = hasEnoughRssItems
+ ? polishRSSList([attestantFeed, ...rssFeeds], locale)
+ : []
const rssItems = polishedRssItems.slice(0, RSS_DISPLAY_COUNT)
- const blogLinks = polishedRssItems.map(({ source, sourceUrl }) => ({
- name: source,
- href: sourceUrl,
- })) as CommunityBlog[]
- blogLinks.push(...BLOGS_WITHOUT_FEED)
+ const blogLinks = hasEnoughRssItems
+ ? ([
+ ...polishedRssItems.map(({ source, sourceUrl }) => ({
+ name: source,
+ href: sourceUrl,
+ })),
+ ...BLOGS_WITHOUT_FEED,
+ ] as CommunityBlog[])
+ : []
return (
<>
-
-
-
-
- {subHeroCTAs.map(
- ({ label, description, href, className, Svg }, idx) => {
- const Link = (
- props: Omit<
- SvgButtonLinkProps,
- "Svg" | "href" | "label" | "children"
- >
- ) => (
-
- {description}
-
- )
- return (
-
-
-
-
- )
- }
- )}
-
-
- {/* What is Ethereum */}
-
+
-
-
-
-
-
- {t("page-index-network-tag")}
-
- {t("page-index-what-is-ethereum-title")}
-
-
-
{t("page-index-what-is-ethereum-description-1")}
-
{t("page-index-what-is-ethereum-description-2")}
-
-
-
- {t("page-index-what-is-ethereum-action")}
-
-
-
- {/* Popular topics */}
-
-
- {t("page-index-popular-topics-header")}
-
-
- {popularTopics
- .filter((topic) => topic.href !== "/what-is-ethereum/")
- .map(({ label, Svg, href, eventName, className }) => (
+
+
+
+ {subHeroCTAs.map(
+ ({ label, description, href, className, Svg }, idx) => {
+ const Link = (
+ props: Omit<
+ SvgButtonLinkProps,
+ "Svg" | "href" | "label" | "children"
+ >
+ ) => (
:first-child]:flex-row",
- className
- )}
+ label={label}
customEventOptions={{
eventCategory,
- eventAction: "popular topics",
- eventName,
+ eventAction: "cta_click",
+ eventName: subHeroCTAs[idx].eventName,
}}
+ {...props}
>
-
- {label}
-
+ {description}
- ))}
-
-
-
-
-
- {/* Use Cases - A new way to use the internet */}
-
-
-
- {t("page-index-use-cases-tag")}
+ )
+ return (
+
+
+
+
+ )
+ }
+ )}
-
- {t("page-index-bento-header")}
-
-
- {/* Mobile - dynamic / lazy loaded */}
-
-
- {/* Desktop */}
- {bentoItems.map(({ className, ...item }) => (
-
- ))}
-
-
- {/* What is ETH */}
-
-
-
-
-
-
- {t("page-index-token-tag")}
-
- {t("page-index-what-is-ether-title")}
-
-
-
{t("page-index-what-is-ether-description-1")}
-
{t("page-index-what-is-ether-description-2")}
-
-
-
+
+
+
+
+
+
+ {t("page-index-network-tag")}
+
+ {t("page-index-what-is-ethereum-title")}
+
+
+
{t("page-index-what-is-ethereum-description-1")}
+
{t("page-index-what-is-ethereum-description-2")}
+
+
+
+ {t("page-index-what-is-ethereum-action")}{" "}
+
+
+
+
+ {/* Popular topics */}
+
+
+ {t("page-index-popular-topics-header")}
+
+
+ {popularTopics
+ .filter(
+ (topic) => topic.href !== "/what-is-ethereum/"
+ )
+ .map(({ label, Svg, href, eventName, className }) => (
+
:first-child]:flex-row",
+ className
+ )}
+ customEventOptions={{
+ eventCategory,
+ eventAction: "popular topics",
+ eventName,
+ }}
+ >
+
+ {label}
+
+
+ ))}
+
+
+
+
+
+
+ {/* Use Cases - A new way to use the internet */}
+
+
-
- {tCommon("eth-current-price")}
-
- {tCommon("data-provided-by")}{" "}
-
- coingecko.com
-
-
- }
+
-
-
-
-
-
-
- {t("page-index-what-is-ether-action")}
-
-
-
-
-
- {/* Apps of the week - Discover the best apps on Ethereum */}
- {/* // TODO: Remove locale restriction after translation */}
- {locale === DEFAULT_LOCALE && (
-
-
-
-
Apps of the week
-
Discover apps on Ethereum
-
Start exploring Ethereum today
-
-
-
-
- Browse apps
-
-
-
-
- )}
-
- {/* Activity - The strongest ecosystem */}
-
-
-
-
-
-
- {t("page-index-activity-tag")}
- {t("page-index-activity-header")}
-
-
- {t("page-index-activity-description")}
-
-
- {t("page-index-activity-subtitle")}
-
-
-
-
-
- {t("page-index-activity-action-primary")}
-
-
- {t("page-index-activity-action")}
-
-
-
-
-
-
- {/* Values - The Internet Is Changing */}
-
-
- {t("page-index-values-tag")}
- {t("page-index-values-header")}
-
- {t("page-index-values-description")}
-
-
-
- {/* dynamic / lazy loaded */}
-
-
-
-
-
- {/* Builders - Blockchain's biggest builder community */}
-
-
-
-
-
-
- {t("page-index-builders-tag")}
- {t("page-index-builders-header")}
- {t("page-index-builders-description")}
-
-
+ {t("page-index-use-cases-tag")}
+
+
+ {t("page-index-bento-header")}
+
+
+
+ {/* Mobile - dynamic / lazy loaded */}
+
+
+ {/* Desktop */}
+ {bentoItems.map(({ className, ...item }) => (
+
+ ))}
+
+
+
+ {/* What is ETH */}
+
+
+
+
+
+
+
+ {t("page-index-token-tag")}
+
+ {t("page-index-what-is-ether-title")}
+
+
+
{t("page-index-what-is-ether-description-1")}
+
{t("page-index-what-is-ether-description-2")}
+
+
+
{price}
+
+ {tCommon("eth-current-price")}
+
+ {tCommon("data-provided-by")}{" "}
+
+ coingecko.com
+
+
+ }
+ >
+
+
+
+
+
+
+ {t("page-index-what-is-ether-action")}
+
+
+
+
+
+
+ {/* Apps of the week - Discover the best apps on Ethereum */}
+ {/* // TODO: Remove locale restriction after translation */}
+ {locale === DEFAULT_LOCALE && hasAppsData && (
+
- {t("page-index-builders-action-primary")}
-
-
+
+
+
Apps of the week
+
Discover apps on Ethereum
+
+ Start exploring Ethereum today
+
+
+
+
+
+ Browse apps
+
+
+
+
+
+ )}
+
+ {/* Activity - The strongest ecosystem */}
+
+
- {t("page-index-builders-action-secondary")}
-
-
-
- {/* CLIENT SIDE */}
-
-
-
-
-
- {/* Recent posts */}
-
-
- {t("page-index-posts-header")}
-
- {t("page-index-posts-subtitle")}
-
- {/* dynamic / lazy loaded */}
-
-
-
-
{t("page-index-posts-action")}
-
- {blogLinks.map(({ name, href }) => (
-
- {name}
-
- ))}
-
-
-
-
- {/* Events */}
-
-
- {t("page-index-events-header")}
-
- {t("page-index-events-subtitle")}
-
-
- {upcomingEvents.map(
- (
- {
- id,
- title,
- link,
- location,
- startTime,
- endTime,
- bannerImage,
- },
- idx
- ) => (
-
+
+
+
+
+ {t("page-index-activity-tag")}
+
+ {t("page-index-activity-header")}
+
+
+
+ {t("page-index-activity-description")}
+
+
+ {t("page-index-activity-subtitle")}
+
+
+
+
+
+ {t("page-index-activity-action-primary")}{" "}
+
+
+
+ {t("page-index-activity-action")}
+
+
+
+
+
+
+
+ {/* Values - The Internet Is Changing */}
+
+
+
+ {t("page-index-values-tag")}
+
+ {t("page-index-values-header")}
+
+
+ {t("page-index-values-description")}
+
+
+
+ {/* dynamic / lazy loaded */}
+
+
+
+
+
+
+ {/* Builders - Blockchain's biggest builder community */}
+
+
+
+
+
+
+
+ {t("page-index-builders-tag")}
+
+ {t("page-index-builders-header")}
+
+
+ {t("page-index-builders-description")}
+
+
+
+ {t("page-index-builders-action-primary")}{" "}
+
+
+
+ {t("page-index-builders-action-secondary")}
+
+
+
+ {/* CLIENT SIDE */}
+
+
+
+
+
+
+ {/* Recent posts - hide if insufficient RSS items */}
+ {hasEnoughRssItems && (
+
+
+
+ {t("page-index-posts-header")}
+
+ {t("page-index-posts-subtitle")}
+
+ {/* dynamic / lazy loaded */}
+
+
+
+
{t("page-index-posts-action")}
+
+ {blogLinks.map(({ name, href }) => (
+
+ {name}
+
+ ))}
+
+
+
+
+ )}
+
+ {/* Events */}
+
+
+
+ {t("page-index-events-header")}
+
+ {t("page-index-events-subtitle")}
+
+
+ {upcomingEvents.map(
+ (
+ {
+ id,
+ title,
+ link,
+ location,
+ startTime,
+ endTime,
+ bannerImage,
+ },
+ idx
+ ) => (
+
+
+ {bannerImage ? (
+
+ ) : (
+
+ )}
+
+
+ {title}
+
+ {formatDateRange(startTime, endTime, locale, {
+ month: "long",
+ year: "numeric",
+ })}
+
+
+ {location}
+
+
+
+ )
)}
+
+
+
+
-
- {bannerImage ? (
-
- ) : (
-
- )}
-
-
- {title}
-
- {formatDateRange(startTime, endTime, locale, {
- month: "long",
- year: "numeric",
- })}
-
-
- {location}
-
-
-
- )
- )}
-
-
-
-
- {t("page-index-events-action")}
-
-
-
-
- {/* Join ethereum.org */}
-
-
-
-
{t("page-index-join-header")}
-
{t("page-index-join-description")}
-
-
- {joinActions.map(
- ({ Svg, label, href, className, description, eventName }) => (
-
- {description}
-
- )
- )}
-
-
-
+
+
+
+
+
+ {/* Join ethereum.org */}
+
+
- {t("page-index-join-action-hub")}
-
-
+
+
+
{t("page-index-join-header")}
+
{t("page-index-join-description")}
+
+
+ {joinActions.map(
+ ({
+ Svg,
+ label,
+ href,
+ className,
+ description,
+ eventName,
+ }) => (
+
+ {description}
+
+ )
+ )}
+
+
+
+ {t("page-index-join-action-hub")}
+
+
+
+
+
-
-
-
+ ,
+ ,
+ ,
+ ]}
+ />
>
)
}
diff --git a/app/api/ab-config/route.ts b/app/api/ab-config/route.ts
index 318b8061dc2..04fbf6b327b 100644
--- a/app/api/ab-config/route.ts
+++ b/app/api/ab-config/route.ts
@@ -1,6 +1,6 @@
import { NextResponse } from "next/server"
-import { IS_PREVIEW_DEPLOY, IS_PROD } from "@/lib/utils/env"
+import { IS_PROD } from "@/lib/utils/env"
import type { ABTestConfig, MatomoExperiment } from "@/lib/ab-testing/types"
@@ -33,8 +33,9 @@ const getPreviewConfig = () => ({
export async function GET() {
// Preview mode: Show menu with original default
- if (!IS_PROD || IS_PREVIEW_DEPLOY)
+ if (!IS_PROD) {
return NextResponse.json(getPreviewConfig())
+ }
try {
const matomoUrl = process.env.NEXT_PUBLIC_MATOMO_URL
diff --git a/src/components/BigNumber/index.tsx b/src/components/BigNumber/index.tsx
index 1fd241f1dc6..24d03bcd183 100644
--- a/src/components/BigNumber/index.tsx
+++ b/src/components/BigNumber/index.tsx
@@ -118,9 +118,18 @@ const BigNumber = async ({
>
) : (
-
- {t("loading-error-refresh")}
-
+ <>
+
+ —
+
+
+ {children}
+
+ >
)}
)
diff --git a/src/components/Hero/HomeHero2026/index.tsx b/src/components/Hero/HomeHero2026/index.tsx
index a926a22a714..06ded61ee13 100644
--- a/src/components/Hero/HomeHero2026/index.tsx
+++ b/src/components/Hero/HomeHero2026/index.tsx
@@ -1,9 +1,17 @@
+import { Fragment } from "react"
import { getImageProps, type StaticImageData } from "next/image"
import type { ClassNameProp } from "@/lib/types"
import LanguageMorpher from "@/components/Homepage/LanguageMorpher"
import PersonaModalCTA from "@/components/Homepage/PersonaModalCTA"
+import EthGlyphIcon from "@/components/icons/eth-glyph.svg"
+import EthTokenIcon from "@/components/icons/eth-token.svg"
+import EthWalletIcon from "@/components/icons/eth-wallet.svg"
+import TryAppsIcon from "@/components/icons/phone-homescreen.svg"
+import SvgButtonLink, {
+ type SvgButtonLinkProps,
+} from "@/components/ui/buttons/SvgButtonLink"
import { cn } from "@/lib/utils/cn"
import { breakpointAsNumber } from "@/lib/utils/screen"
@@ -11,17 +19,58 @@ import { breakpointAsNumber } from "@/lib/utils/screen"
import heroBase from "@/public/images/home/hero.png"
import hero2xl from "@/public/images/home/hero-2xl.png"
+export type CTAVariant = "modal" | "direct-buttons"
+
type HomeHero2026Props = ClassNameProp & {
image?: StaticImageData
image2xl?: StaticImageData
alt?: string
+ ctaVariant?: CTAVariant
+ eventCategory?: string
}
+const directButtonCTAs = [
+ {
+ label: "Learn Ethereum",
+ description: "What is Ethereum?",
+ href: "/what-is-ethereum/",
+ Svg: EthGlyphIcon,
+ className: "text-accent-a hover:text-accent-a-hover",
+ eventName: "learn_ethereum",
+ },
+ {
+ label: "Pick a wallet",
+ description: "Create accounts, manage assets",
+ href: "/wallets/find-wallet/",
+ Svg: EthWalletIcon,
+ className: "text-primary hover:text-primary-hover",
+ eventName: "pick_wallet",
+ },
+ {
+ label: "Get ETH",
+ description: "The currency of Ethereum",
+ href: "/get-eth/",
+ Svg: EthTokenIcon,
+ className: "text-accent-b hover:text-accent-b-hover",
+ eventName: "get_eth",
+ },
+ {
+ label: "Try apps",
+ description: "See what Ethereum can do",
+ href: "/dapps/",
+ Svg: TryAppsIcon,
+ className: "text-accent-c hover:text-accent-c-hover",
+ eventName: "try_apps",
+ },
+]
+
const HomeHero2026 = ({
className,
image,
image2xl,
alt: altProp,
+ ctaVariant = "modal",
+ eventCategory = "Homepage",
}: HomeHero2026Props) => {
const baseImage = image ?? heroBase
const xlImage = image2xl ?? image ?? hero2xl
@@ -46,7 +95,7 @@ const HomeHero2026 = ({
} = getImageProps({ ...common, ...baseImage, quality: 5 })
return (
-
+
-
+ {ctaVariant === "modal" ? (
+
+ ) : (
+
+ {directButtonCTAs.map(
+ ({
+ label,
+ description,
+ href,
+ className: ctaClass,
+ Svg,
+ eventName,
+ }) => {
+ const Link = (
+ props: Omit<
+ SvgButtonLinkProps,
+ "Svg" | "href" | "label" | "children"
+ >
+ ) => (
+
+ {description}
+
+ )
+ return (
+
+
+
+
+ )
+ }
+ )}
+
+ )}
-
+
)
}
diff --git a/src/components/Homepage/BentoCardSwiper.tsx b/src/components/Homepage/BentoCardSwiper.tsx
index 521cfc3ad5b..7cb57c77558 100644
--- a/src/components/Homepage/BentoCardSwiper.tsx
+++ b/src/components/Homepage/BentoCardSwiper.tsx
@@ -32,8 +32,8 @@ const BentoCardSwiper = ({
onSlideChange={({ activeIndex }) => {
trackCustomEvent({
eventCategory,
- eventAction: "mobile use cases",
- eventName: `swipe to card ${activeIndex + 1}`,
+ eventAction: "cta_swipe",
+ eventName: String(activeIndex + 1),
})
}}
>
diff --git a/src/components/Homepage/FeatureCards.tsx b/src/components/Homepage/FeatureCards.tsx
index ace87660aa8..14b1d59f575 100644
--- a/src/components/Homepage/FeatureCards.tsx
+++ b/src/components/Homepage/FeatureCards.tsx
@@ -12,9 +12,13 @@ import publicRulesImage from "@/public/images/homepage/features/public-rules.png
type FeatureCardsProps = {
className?: string
+ eventCategory?: string
}
-const FeatureCards = ({ className }: FeatureCardsProps) => {
+const FeatureCards = ({
+ className,
+ eventCategory = "Homepage",
+}: FeatureCardsProps) => {
return (
{
-
- Learn more
+
+ What is Ethereum?
diff --git a/src/components/Homepage/GetStartedGrid.tsx b/src/components/Homepage/GetStartedGrid.tsx
index 127fb2c5eea..af39c7abebe 100644
--- a/src/components/Homepage/GetStartedGrid.tsx
+++ b/src/components/Homepage/GetStartedGrid.tsx
@@ -13,6 +13,7 @@ import learnImage from "@/public/images/homepage/get-started/learn.png"
const cards = [
{
+ id: "learn",
icon: Book,
iconBg: "bg-[#f7ecff]",
iconColor: "text-primary",
@@ -30,6 +31,7 @@ const cards = [
image: learnImage,
},
{
+ id: "developers",
icon: Code,
iconBg: "bg-[#e9f4ff]",
iconColor: "text-accent-a",
@@ -47,6 +49,7 @@ const cards = [
image: developersImage,
},
{
+ id: "enterprise",
icon: Building2,
iconBg: "bg-[#e6f7f6]",
iconColor: "text-accent-c",
@@ -67,9 +70,13 @@ const cards = [
type GetStartedGridProps = {
className?: string
+ eventCategory?: string
}
-const GetStartedGrid = ({ className }: GetStartedGridProps) => {
+const GetStartedGrid = ({
+ className,
+ eventCategory = "Homepage",
+}: GetStartedGridProps) => {
return (
@@ -137,6 +144,11 @@ const GetStartedGrid = ({ className }: GetStartedGridProps) => {
{card.cta}
diff --git a/src/components/Homepage/Homepage2026.tsx b/src/components/Homepage/Homepage2026.tsx
index 2606f61e357..59a06d9c6e5 100644
--- a/src/components/Homepage/Homepage2026.tsx
+++ b/src/components/Homepage/Homepage2026.tsx
@@ -3,12 +3,13 @@ import dynamic from "next/dynamic"
import type { Lang } from "@/lib/types"
-import HomeHero2026 from "@/components/Hero/HomeHero2026"
+import HomeHero2026, { type CTAVariant } from "@/components/Hero/HomeHero2026"
import FeatureCards from "@/components/Homepage/FeatureCards"
import GetStartedGrid from "@/components/Homepage/GetStartedGrid"
import { SimulatorI18nWrapper } from "@/components/Homepage/SimulatorSection/SimulatorI18nWrapper"
import TrustLogos from "@/components/Homepage/TrustLogos"
import MainArticle from "@/components/MainArticle"
+import { TrackedSection } from "@/components/TrackedSection"
import { Section } from "@/components/ui/section"
import { getDirection } from "@/lib/utils/direction"
@@ -30,45 +31,61 @@ const SectionSkeleton = ({ className }: { className?: string }) => (
type Homepage2026Props = {
locale: Lang
- accountHolders: number
- transactionsToday: number
+ accountHolders: number | null
+ transactionsToday: number | null
+ ctaVariant?: CTAVariant
}
const Homepage2026 = ({
locale,
accountHolders,
transactionsToday,
+ ctaVariant = "modal",
}: Homepage2026Props) => {
const { direction: dir } = getDirection(locale)
+ const eventCategory = `Homepage - ${locale}`
+
return (
-
+
- }>
-
-
+
+ }>
+
+
+
- }>
-
-
+
+ }>
+
+
+
-
+
+
+
-
+
+
+
- }>
-
-
-
-
+
+ }>
+
+
+
+
+
-
+
+
+
)
diff --git a/src/components/Homepage/KPISection.tsx b/src/components/Homepage/KPISection.tsx
index 67585509022..65c56be92c3 100644
--- a/src/components/Homepage/KPISection.tsx
+++ b/src/components/Homepage/KPISection.tsx
@@ -9,8 +9,8 @@ import { Section, SectionHeader, SectionTag } from "@/components/ui/section"
import { cn } from "@/lib/utils/cn"
type KPISectionProps = {
- accountHolders: number
- transactionsToday: number
+ accountHolders: number | null
+ transactionsToday: number | null
className?: string
}
@@ -23,8 +23,12 @@ const ANIMATION_DURATION_MS = 2000
/**
* Hook that returns an incrementing transaction count
* Adds ~2,914 transactions every 12 seconds when visible
+ * Returns null if initialValue is null (error state)
*/
-function useIncrementalCounter(initialValue: number, isVisible: boolean) {
+function useIncrementalCounter(
+ initialValue: number | null,
+ isVisible: boolean
+): number | null {
const [target, setTarget] = useState(initialValue)
// Sync with new initial value when it changes
@@ -32,16 +36,18 @@ function useIncrementalCounter(initialValue: number, isVisible: boolean) {
setTarget(initialValue)
}, [initialValue])
- // Increment counter every 12 seconds when visible
+ // Increment counter every 12 seconds when visible (only if we have a valid value)
useEffect(() => {
- if (!isVisible) return
+ if (!isVisible || initialValue === null) return
const interval = setInterval(() => {
- setTarget((prev) => prev + TRANSACTIONS_PER_INTERVAL)
+ setTarget((prev) =>
+ prev !== null ? prev + TRANSACTIONS_PER_INTERVAL : null
+ )
}, INTERVAL_MS)
return () => clearInterval(interval)
- }, [isVisible])
+ }, [isVisible, initialValue])
return target
}
@@ -144,7 +150,7 @@ const KPISection = ({
transactionsToday,
className,
}: KPISectionProps) => {
- const { ref: sectionRef, isIntersecting: isVisible } =
+ const { ref: intersectionRef, isIntersecting: isVisible } =
useIntersectionObserver({
threshold: 0.3,
freezeOnceVisible: true,
@@ -154,7 +160,7 @@ const KPISection = ({
return (
- {formatNumber(accountHolders)}
+ {accountHolders !== null ? formatNumber(accountHolders) : "—"}
ETH holders
@@ -201,11 +207,15 @@ const KPISection = ({
strokeWidth={1.5}
/>
-
+ {liveTransactions !== null ? (
+
+ ) : (
+
—
+ )}
Transactions today
diff --git a/src/components/Homepage/PersonaModalCTA.tsx b/src/components/Homepage/PersonaModalCTA.tsx
index aedc5b6a994..3a7ad3ebe12 100644
--- a/src/components/Homepage/PersonaModalCTA.tsx
+++ b/src/components/Homepage/PersonaModalCTA.tsx
@@ -1,6 +1,6 @@
"use client"
-import { useState } from "react"
+import { useRef, useState } from "react"
import { BookOpen, Building2, Code, ExternalLink } from "lucide-react"
import { ChevronNext } from "@/components/Chevron"
@@ -8,6 +8,7 @@ import { Button } from "@/components/ui/buttons/Button"
import {
Dialog,
DialogContent,
+ DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
@@ -21,6 +22,7 @@ type PersonaLink = {
label: string
href: string
isExternal?: boolean
+ eventName: string
}
type PersonaCategory = {
@@ -40,8 +42,16 @@ const categories: PersonaCategory[] = [
iconBgClass: "bg-accent-a/20",
iconColorClass: "text-accent-a",
links: [
- { label: "What is Ethereum?", href: "/what-is-ethereum/" },
- { label: "Get a wallet", href: "/wallets/find-wallet/" },
+ {
+ label: "What is Ethereum?",
+ href: "/what-is-ethereum/",
+ eventName: "learn_ethereum",
+ },
+ {
+ label: "Get a wallet",
+ href: "/wallets/find-wallet/",
+ eventName: "get_wallet",
+ },
],
},
{
@@ -51,8 +61,12 @@ const categories: PersonaCategory[] = [
iconBgClass: "bg-primary-low-contrast",
iconColorClass: "text-primary",
links: [
- { label: "Developer Hub", href: "/developers/" },
- { label: "Docs", href: "/developers/docs/" },
+ {
+ label: "Developer Hub",
+ href: "/developers/",
+ eventName: "developer_hub",
+ },
+ { label: "Docs", href: "/developers/docs/", eventName: "docs" },
],
},
{
@@ -62,11 +76,12 @@ const categories: PersonaCategory[] = [
iconBgClass: "bg-accent-c/20",
iconColorClass: "text-accent-c",
links: [
- { label: "Founders", href: "/founders/" },
+ { label: "Founders", href: "/founders/", eventName: "founders" },
{
label: "Institutions",
href: "https://institutions.ethereum.org/",
isExternal: true,
+ eventName: "institutions",
},
],
},
@@ -78,23 +93,40 @@ type PersonaModalCTAProps = {
const PersonaModalCTA = ({ eventCategory }: PersonaModalCTAProps) => {
const [isOpen, setIsOpen] = useState(false)
+ // Track if modal was closed via link click (not ESC/outside click/X button)
+ const closedViaLinkRef = useRef(false)
const handleOpenChange = (open: boolean) => {
if (open) {
+ // Modal is opening - fire both cta_click and modal_open for funnel analysis
+ closedViaLinkRef.current = false
+ trackCustomEvent({
+ eventCategory,
+ eventAction: "cta_click",
+ eventName: "start_here",
+ })
+ trackCustomEvent({
+ eventCategory,
+ eventAction: "modal_open",
+ eventName: "persona_modal",
+ })
+ } else if (!closedViaLinkRef.current) {
+ // Modal is closing without a link selection (ESC, click outside, X button)
trackCustomEvent({
eventCategory,
- eventAction: "start here",
- eventName: "start here",
+ eventAction: "modal_close",
+ eventName: "persona_modal",
})
}
setIsOpen(open)
}
- const handleLinkClick = (label: string) => {
+ const handleLinkClick = (eventName: string) => {
+ closedViaLinkRef.current = true
trackCustomEvent({
eventCategory,
- eventAction: "modal",
- eventName: label,
+ eventAction: "modal_select",
+ eventName,
})
setIsOpen(false)
}
@@ -112,6 +144,10 @@ const PersonaModalCTA = ({ eventCategory }: PersonaModalCTAProps) => {
What brings you here?
+
+ Choose your path: resources for beginners, developers, or
+ enterprise.
+
{categories.map(
@@ -137,29 +173,34 @@ const PersonaModalCTA = ({ eventCategory }: PersonaModalCTAProps) => {
{/* Links */}
- {links.map(({ label: linkLabel, href, isExternal }, idx) => (
-
- {idx > 0 &&
}
-
handleLinkClick(linkLabel)}
- hideArrow
- className="group flex items-center justify-between text-xl font-bold text-primary no-underline transition-colors hover:text-primary-hover md:text-3xl"
- {...(isExternal && {
- target: "_blank",
- rel: "noopener noreferrer",
- })}
- >
-
- {linkLabel}
- {isExternal && (
-
- )}
-
-
-
-
- ))}
+ {links.map(
+ (
+ { label: linkLabel, href, isExternal, eventName },
+ idx
+ ) => (
+
+ {idx > 0 &&
}
+
handleLinkClick(eventName)}
+ hideArrow
+ className="group flex items-center justify-between text-xl font-bold text-primary no-underline transition-colors hover:text-primary-hover md:text-3xl"
+ {...(isExternal && {
+ target: "_blank",
+ rel: "noopener noreferrer",
+ })}
+ >
+
+ {linkLabel}
+ {isExternal && (
+
+ )}
+
+
+
+
+ )
+ )}
)
diff --git a/src/components/Homepage/SavingsCarousel.tsx b/src/components/Homepage/SavingsCarousel.tsx
index 67467e60232..cb03b7314cd 100644
--- a/src/components/Homepage/SavingsCarousel.tsx
+++ b/src/components/Homepage/SavingsCarousel.tsx
@@ -19,6 +19,7 @@ import {
} from "@/components/ui/swiper"
import { cn } from "@/lib/utils/cn"
+import { trackCustomEvent } from "@/lib/utils/matomo"
import FloatingCard from "./FloatingCard"
@@ -135,6 +136,7 @@ const getComparison = (slide: Slide): ComparisonData => {
type SavingsCarouselProps = {
className?: string
+ eventCategory?: string
}
type ComparisonCardProps = {
@@ -193,9 +195,14 @@ const ComparisonCard = ({
type SlideContentProps = {
slide: Slide
isActive: boolean
+ eventCategory: string
}
-const SlideContent = ({ slide, isActive }: SlideContentProps) => {
+const SlideContent = ({
+ slide,
+ isActive,
+ eventCategory,
+}: SlideContentProps) => {
const comparison = getComparison(slide)
const traditionalControls = useAnimationControls()
const ethereumControls = useAnimationControls()
@@ -228,7 +235,15 @@ const SlideContent = ({ slide, isActive }: SlideContentProps) => {
{slide.description}
-
+
{slide.cta}
@@ -297,15 +312,23 @@ const SlideContent = ({ slide, isActive }: SlideContentProps) => {
)
}
-const SavingsCarousel = ({ className }: SavingsCarouselProps) => {
+const SavingsCarousel = ({
+ className,
+ eventCategory = "Homepage",
+}: SavingsCarouselProps) => {
const [activeIndex, setActiveIndex] = useState(0)
const handleSlideChange = (swiper: SwiperType) => {
setActiveIndex(swiper.activeIndex)
+ trackCustomEvent({
+ eventCategory,
+ eventAction: "cta_swipe",
+ eventName: String(swiper.activeIndex + 1),
+ })
}
return (
-
+
{
>
{slides.map((slide, index) => (
-
+
))}
-
+
)
}
diff --git a/src/components/Homepage/TrustLogos.tsx b/src/components/Homepage/TrustLogos.tsx
index c70479b8b33..293332a7af0 100644
--- a/src/components/Homepage/TrustLogos.tsx
+++ b/src/components/Homepage/TrustLogos.tsx
@@ -38,9 +38,13 @@ const logos: Logo[] = [
type TrustLogosProps = {
className?: string
+ eventCategory?: string
}
-const TrustLogos = ({ className }: TrustLogosProps) => {
+const TrustLogos = ({
+ className,
+ eventCategory = "Homepage",
+}: TrustLogosProps) => {
return (
{
See Institutional adoption
diff --git a/src/components/Matomo.tsx b/src/components/Matomo.tsx
index 9136e31f294..2f6a346b0df 100644
--- a/src/components/Matomo.tsx
+++ b/src/components/Matomo.tsx
@@ -4,16 +4,16 @@ import { useEffect, useState } from "react"
import { usePathname } from "next/navigation"
import { init, push } from "@socialgouv/matomo-next"
-import { IS_PREVIEW_DEPLOY } from "@/lib/utils/env"
+// Module-level flag to prevent double initialization in React Strict Mode
+let matomoInitialized = false
export default function Matomo() {
const pathname = usePathname()
- const [inited, setInited] = useState(false)
const [previousPath, setPreviousPath] = useState("")
useEffect(() => {
- if (!IS_PREVIEW_DEPLOY && !inited) {
+ if (!matomoInitialized) {
init({
url: process.env.NEXT_PUBLIC_MATOMO_URL!,
siteId: process.env.NEXT_PUBLIC_MATOMO_SITE_ID!,
@@ -23,9 +23,9 @@ export default function Matomo() {
"[Matomo] initialized with URL:",
process.env.NEXT_PUBLIC_MATOMO_URL
)
- setInited(true)
+ matomoInitialized = true
}
- }, [inited])
+ }, [])
/**
* The @socialgouv/matomo-next does not work with next 13
diff --git a/src/components/ScrollDepthTracker.tsx b/src/components/ScrollDepthTracker.tsx
new file mode 100644
index 00000000000..33881314e98
--- /dev/null
+++ b/src/components/ScrollDepthTracker.tsx
@@ -0,0 +1,72 @@
+"use client"
+
+import { useEffect, useRef } from "react"
+
+import { trackCustomEvent } from "@/lib/utils/matomo"
+
+const THRESHOLDS = [25, 50, 75, 100] as const
+
+interface ScrollDepthTrackerProps {
+ eventCategory: string
+}
+
+export default function ScrollDepthTracker({
+ eventCategory,
+}: ScrollDepthTrackerProps) {
+ const firedThresholds = useRef>(new Set())
+
+ useEffect(() => {
+ let ticking = false
+ let rafId: number | null = null
+
+ const cleanup = () => {
+ window.removeEventListener("scroll", throttledHandler)
+ if (rafId !== null) {
+ window.cancelAnimationFrame(rafId)
+ }
+ }
+
+ const handleScroll = () => {
+ const scrollTop = window.scrollY
+ const docHeight = document.documentElement.scrollHeight
+ const viewportHeight = window.innerHeight
+ const scrollPercent = ((scrollTop + viewportHeight) / docHeight) * 100
+
+ for (const threshold of THRESHOLDS) {
+ if (
+ scrollPercent >= threshold &&
+ !firedThresholds.current.has(threshold)
+ ) {
+ firedThresholds.current.add(threshold)
+ trackCustomEvent({
+ eventCategory,
+ eventAction: "scroll_depth",
+ eventName: `${threshold}%`,
+ })
+ }
+ }
+
+ // Remove listener once all thresholds have been tracked
+ if (firedThresholds.current.size === THRESHOLDS.length) {
+ cleanup()
+ }
+ }
+
+ const throttledHandler = () => {
+ if (!ticking) {
+ rafId = window.requestAnimationFrame(() => {
+ handleScroll()
+ ticking = false
+ })
+ ticking = true
+ }
+ }
+
+ window.addEventListener("scroll", throttledHandler, { passive: true })
+ handleScroll() // Check initial scroll position
+
+ return cleanup
+ }, [eventCategory])
+
+ return null
+}
diff --git a/src/components/TrackedSection.tsx b/src/components/TrackedSection.tsx
new file mode 100644
index 00000000000..b021ab180e1
--- /dev/null
+++ b/src/components/TrackedSection.tsx
@@ -0,0 +1,54 @@
+"use client"
+
+import { useEffect, useRef } from "react"
+import { useIntersectionObserver } from "usehooks-ts"
+import { Slot } from "@radix-ui/react-slot"
+
+import { trackCustomEvent } from "@/lib/utils/matomo"
+
+type TrackedSectionProps = {
+ id: string
+ eventCategory: string
+ children: React.ReactNode
+ asChild?: boolean
+}
+
+export function TrackedSection({
+ id,
+ eventCategory,
+ children,
+ asChild = false,
+}: TrackedSectionProps) {
+ const { ref, isIntersecting } = useIntersectionObserver({ threshold: 0.3 })
+ const hasTrackedView = useRef(false)
+ const timerRef = useRef(null)
+
+ // Track section_view after 1 second of visibility
+ useEffect(() => {
+ if (isIntersecting && !hasTrackedView.current) {
+ timerRef.current = setTimeout(() => {
+ trackCustomEvent({
+ eventCategory,
+ eventAction: "section_view",
+ eventName: id,
+ })
+ hasTrackedView.current = true
+ }, 1000)
+ } else if (!isIntersecting && timerRef.current) {
+ clearTimeout(timerRef.current)
+ timerRef.current = null
+ }
+
+ return () => {
+ if (timerRef.current) clearTimeout(timerRef.current)
+ }
+ }, [isIntersecting, eventCategory, id])
+
+ const Comp = asChild ? Slot : "div"
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/lib/ab-testing/server.ts b/src/lib/ab-testing/server.ts
index 34d62e6d9d4..004c7be7fd2 100644
--- a/src/lib/ab-testing/server.ts
+++ b/src/lib/ab-testing/server.ts
@@ -1,8 +1,16 @@
+import { SITE_URL } from "@/lib/constants"
+
import type { ABTestAssignment, ABTestConfig } from "./types"
+// Search engine and social media crawlers - serve Original to ensure consistent
+// indexing and link previews. This is NOT cloaking per Google's A/B testing guidelines:
+// https://developers.google.com/search/docs/advanced/guidelines/cloaking
+const BOT_PATTERN =
+ /googlebot|bingbot|yandex|baiduspider|duckduckbot|slurp|facebookexternalhit|twitterbot|linkedinbot|discordbot|telegrambot|whatsapp|slackbot/i
+
const getABTestConfigs = async (): Promise> => {
try {
- const response = await fetch("https://ethereum.org/api/ab-config", {
+ const response = await fetch(`${SITE_URL}/api/ab-config`, {
next: { revalidate: 3600 },
})
@@ -30,6 +38,10 @@ export const getABTestAssignment = async (
headers.get("x-forwarded-for") || headers.get("x-real-ip") || "unknown"
const userAgent = headers.get("user-agent") || ""
+ // Always serve Original to bots to prevent indexing fluctuation during A/B tests
+ // and ensure consistent social media link previews
+ if (BOT_PATTERN.test(userAgent)) return null
+
// Add privacy-preserving entropy sources
const acceptLanguage = headers.get("accept-language") || ""
const acceptEncoding = headers.get("accept-encoding") || ""
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index f1a39a1d69e..908da2d2dda 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -24,9 +24,13 @@ export const LOCALES_CODES = BUILD_LOCALES
? BUILD_LOCALES.split(",")
: i18nConfig.map(({ code }) => code)
-// Site urls
+// Site urls - auto-detect from Netlify deploy context
export const SITE_URL =
- process.env.NEXT_PUBLIC_SITE_URL || "https://ethereum.org"
+ process.env.NEXT_PUBLIC_SITE_URL ||
+ process.env.DEPLOY_PRIME_URL || // Branch/PR deploys
+ process.env.DEPLOY_URL || // Unique deploy URL
+ process.env.URL || // Primary site URL
+ "https://ethereum.org"
export const DISCORD_PATH = "https://discord.gg/ethereum-org/"
export const GITHUB_REPO_URL =
"https://github.com/ethereum/ethereum-org-website/"