diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 5c0b6d622fd..e689338a8e9 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -7,19 +7,16 @@ 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" @@ -38,9 +35,7 @@ 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, @@ -71,10 +66,10 @@ import { getMetadata } from "@/lib/utils/metadata" import { formatPriceUSD } from "@/lib/utils/numbers" import { polishRSSList } from "@/lib/utils/rss" -import { ENTERPRISE_ETHEREUM_URL } from "@/lib/constants" import { BLOGS_WITHOUT_FEED, DEFAULT_LOCALE, + ENTERPRISE_ETHEREUM_URL, GITHUB_REPO_URL, LOCALES_CODES, RSS_DISPLAY_COUNT, @@ -85,7 +80,6 @@ import IndexPageJsonLD from "./page-jsonld" import { getActivity } from "./utils" import { - getAccountHolders, getAppsData, getAttestantPosts, getBeaconchainData, @@ -97,9 +91,6 @@ 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"), { @@ -146,8 +137,6 @@ 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, @@ -157,102 +146,61 @@ const Page = async ({ params }: { params: PageParams }) => { rssData, appsData, eventsData, - accountHolders, ] = await Promise.all([ - 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), + getEthPrice(), + getBeaconchainData(), + getTotalValueLockedData(), + getGrowThePieData(), + getAttestantPosts(), + getRSSData(), + getAppsData(), + getEventsData(), ]) - // 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 + // Handle null cases - throw error if required data is missing if (!ethPrice) { - console.error("[Homepage] Failed to fetch ETH price data") + throw new Error("Failed to fetch ETH price data") } - const safeEthPrice = - ethPrice ?? createErrorMetric("Failed to fetch ETH price") - - // Beaconchain data - show "—" on failure if (!beaconchainData) { - console.error("[Homepage] Failed to fetch Beaconchain data") + throw new Error("Failed to fetch Beaconchain data") } - const totalEthStaked = - beaconchainData?.totalEthStaked ?? - createErrorMetric("Failed to fetch staked ETH") - - // Total Value Locked - show "—" on failure if (!totalValueLocked) { - console.error("[Homepage] Failed to fetch TVL data") + throw new Error("Failed to fetch total value locked data") } - const safeTotalValueLocked = - totalValueLocked ?? createErrorMetric("Failed to fetch TVL") - - // GrowThePie data - show "—" on failure if (!growThePieData) { - 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") + throw new Error("Failed to fetch GrowThePie 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) { - console.error("[Homepage] Failed to fetch apps data") + throw new Error("Failed to fetch apps data") } - const hasAppsData = !!appsData - // RSS feeds - hide section if insufficient items + // RSS feeds - graceful degradation: use what's available if we have enough items const rssFeeds = rssData ?? [] const attestantFeed = attestantPosts ?? [] const totalRssItems = rssFeeds.reduce((sum, feed) => sum + feed.length, 0) + attestantFeed.length if (totalRssItems < RSS_DISPLAY_COUNT) { - console.error( - `[Homepage] Insufficient RSS data: have ${totalRssItems}, need ${RSS_DISPLAY_COUNT}` + throw new Error( + `Insufficient RSS data: need at least ${RSS_DISPLAY_COUNT} items` ) } - const hasEnoughRssItems = totalRssItems >= RSS_DISPLAY_COUNT + + // Extract totalEthStaked from beaconchainData + const { totalEthStaked } = beaconchainData // Events - use empty array as fallback const upcomingEvents = (eventsData ?? []).slice(0, 3) - // Apps of the week - only parse if we have data - const appsOfTheWeek = hasAppsData ? parseAppsOfTheWeek(appsData) : [] + const appsOfTheWeek = parseAppsOfTheWeek(appsData) const bentoItems = await getBentoBoxItems(locale) - const ethPriceHasError = "error" in safeEthPrice + const ethPriceHasError = "error" in ethPrice const price = ethPriceHasError - ? "—" - : formatPriceUSD(safeEthPrice.value, locale) + ? t("loading-error-refresh") + : formatPriceUSD(ethPrice.value, locale) const eventCategory = `Homepage - ${locale}` @@ -478,616 +426,536 @@ const Page = async ({ params }: { params: PageParams }) => { ] const metricResults: AllHomepageActivityData = { - ethPrice: safeEthPrice, + ethPrice, totalEthStaked, - totalValueLocked: safeTotalValueLocked, - txCount: safeTxCount, - txCostsMedianUsd: safeTxCostsMedianUsd, + totalValueLocked, + txCount: growThePieData.txCount, + txCostsMedianUsd: growThePieData.txCostsMedianUsd, } const metrics = await getActivity(metricResults, locale) - // RSS feed items - only process if we have enough items - const polishedRssItems = hasEnoughRssItems - ? polishRSSList([attestantFeed, ...rssFeeds], locale) - : [] + // RSS feed items + // polishRSSList expects RSSItem[][], so wrap attestantFeed in an array + const polishedRssItems = polishRSSList([attestantFeed, ...rssFeeds], locale) const rssItems = polishedRssItems.slice(0, RSS_DISPLAY_COUNT) - const blogLinks = hasEnoughRssItems - ? ([ - ...polishedRssItems.map(({ source, sourceUrl }) => ({ - name: source, - href: sourceUrl, - })), - ...BLOGS_WITHOUT_FEED, - ] as CommunityBlog[]) - : [] + const blogLinks = polishedRssItems.map(({ source, sourceUrl }) => ({ + name: source, + href: sourceUrl, + })) as CommunityBlog[] + blogLinks.push(...BLOGS_WITHOUT_FEED) return ( <> - - + +
+
+ {subHeroCTAs.map( + ({ label, description, href, className, Svg }, idx) => { + const Link = ( + props: Omit< + SvgButtonLinkProps, + "Svg" | "href" | "label" | "children" + > + ) => ( + +

{description}

+
+ ) + return ( + + + + + ) + } + )} +
+ + {/* What is Ethereum */} +
- -
-
- {subHeroCTAs.map( - ({ label, description, href, className, Svg }, idx) => { - const Link = ( - props: Omit< - SvgButtonLinkProps, - "Svg" | "href" | "label" | "children" - > - ) => ( + + + + + + {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: "cta_click", - eventName: subHeroCTAs[idx].eventName, + eventAction: "popular topics", + eventName, }} - {...props} > -

{description}

+

+ {label} +

- ) - return ( - - - - - ) - } - )} + ))} +
+
+
+ + {/* Use Cases - A new way to use the internet */} +
+
+
+ {t("page-index-use-cases-tag")} +
+

+ {t("page-index-bento-header")} +

+
- {/* What is Ethereum */} - + + {/* Desktop */} + {bentoItems.map(({ className, ...item }) => ( + -
- - - - - - {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 */} - -
+ ))} +
+ + {/* 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-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 - -
- } - > - - + {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 && ( - + + + + +
+ -
- -
- Apps of the week - Discover apps on Ethereum -

- Start exploring Ethereum today -

-
- -
- - Browse apps - -
-
-
- - )} - - {/* Activity - The strongest ecosystem */} - -
+ +
+ + + + {/* 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-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} - - - - ) + {t("page-index-builders-action-primary")} + + + {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-events-action")} - -
-
-
- - {/* Join ethereum.org */} - -
+ {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}

+
+ ) + )} +
+
+ -
-
-

{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")} - -
-
-
-
+ {t("page-index-join-action-hub")} + + - , - , - , - ]} - /> + + + ) }