Skip to content

Commit bd4fc9a

Browse files
authored
Merge pull request #18025 from konopkja/fix/find-wallet-methodology-seo
feat(seo): add visible wallet evaluation methodology on find-wallet
2 parents 68f8a70 + df5068b commit bd4fc9a

6 files changed

Lines changed: 162 additions & 5 deletions

File tree

app/[locale]/wallets/find-wallet/page.tsx

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@ import {
55
setRequestLocale,
66
} from "next-intl/server"
77

8-
import type { Lang, PageParams } from "@/lib/types"
8+
import type { Lang, PageParams, WalletData } from "@/lib/types"
99

1010
import Breadcrumbs from "@/components/Breadcrumbs"
1111
import FindWalletProductTable from "@/components/FindWalletProductTable/lazy"
1212
import I18nProvider from "@/components/I18nProvider"
13+
import ListingMethodology from "@/components/ListingMethodology"
1314
import MainArticle from "@/components/MainArticle"
15+
import { UnorderedList } from "@/components/ui/list"
1416

1517
import { getAppPageContributorInfo } from "@/lib/utils/contributors"
18+
import { formatDate } from "@/lib/utils/date"
1619
import { getMetadata } from "@/lib/utils/metadata"
1720
import { getRequiredNamespacesForPage } from "@/lib/utils/translations"
1821
import {
@@ -43,6 +46,16 @@ const Page = async (props: { params: Promise<PageParams> }) => {
4346
),
4447
}))
4548

49+
const mostRecentWalletUpdate = walletsData
50+
.map((wallet: WalletData) => wallet.last_updated)
51+
.filter((d) => d.length > 0)
52+
.sort()
53+
.at(-1)
54+
55+
const lastUpdatedDisplay = mostRecentWalletUpdate
56+
? formatDate(mostRecentWalletUpdate, locale)
57+
: ""
58+
4659
// Get i18n messages
4760
const allMessages = await getMessages({ locale })
4861
const requiredNamespaces = getRequiredNamespacesForPage(
@@ -76,6 +89,40 @@ const Page = async (props: { params: Promise<PageParams> }) => {
7689
</div>
7790

7891
<FindWalletProductTable wallets={wallets} />
92+
93+
<ListingMethodology
94+
heading={t("page-find-wallet-methodology-title")}
95+
description={t("page-find-wallet-methodology-intro")}
96+
lastUpdated={lastUpdatedDisplay}
97+
href="/contributing/adding-wallets/"
98+
footers={[
99+
t("page-find-wallet-footnote-1"),
100+
t("page-find-wallet-footnote-2"),
101+
]}
102+
>
103+
<p>{t("page-find-wallet-methodology-must-haves-label")}</p>
104+
105+
<UnorderedList className="space-y-2">
106+
{[
107+
"security",
108+
"track-record",
109+
"maintenance",
110+
"honest-info",
111+
"contact",
112+
"eip1559",
113+
"ux",
114+
"ethereum-focused",
115+
].map((key) => (
116+
<li key={key}>
117+
{t(`page-find-wallet-methodology-criterion-${key}`)}
118+
</li>
119+
))}
120+
</UnorderedList>
121+
122+
<p>{t("page-find-wallet-methodology-verification")}</p>
123+
124+
<p>{t("page-find-wallet-methodology-filters")}</p>
125+
</ListingMethodology>
79126
</MainArticle>
80127
</I18nProvider>
81128
</>

src/components/ExpandableCard.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client"
22

33
import React, { type ReactNode, useState } from "react"
4+
import type { AccordionContentProps } from "@radix-ui/react-accordion"
45

56
import { Flex, HStack, VStack } from "@/components/ui/flex"
67

@@ -26,7 +27,7 @@ export type ExpandableCardProps = {
2627
eventName?: string
2728
visible?: boolean
2829
className?: string
29-
}
30+
} & Pick<AccordionContentProps, "forceMount">
3031

3132
const ExpandableCard = ({
3233
children,
@@ -38,6 +39,7 @@ const ExpandableCard = ({
3839
eventName = "",
3940
visible = false,
4041
className,
42+
forceMount,
4143
}: ExpandableCardProps) => {
4244
const [isVisible, setIsVisible] = useState(visible)
4345
const { t } = useTranslation("common")
@@ -91,7 +93,14 @@ const ExpandableCard = ({
9193
</span>
9294
</Flex>
9395
</AccordionTrigger>
94-
<AccordionContent className="p-6 pt-0 md:p-6 md:pt-0">
96+
<AccordionContent
97+
forceMount={forceMount}
98+
className={cn(
99+
"p-6 pt-0 md:p-6 md:pt-0",
100+
forceMount &&
101+
"in-data-[state=closed]:hidden in-data-[state=closed]:h-0"
102+
)}
103+
>
95104
<div className="border-t pt-6 text-md text-body">{children}</div>
96105
</AccordionContent>
97106
</AccordionItem>
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { ReactNode } from "react"
2+
import { getTranslations } from "next-intl/server"
3+
4+
import { BaseLink } from "@/components/ui/Link"
5+
import { Section } from "@/components/ui/section"
6+
7+
import ExpandableCard from "../ExpandableCard"
8+
9+
type ListingMethodologyProps = {
10+
heading: string
11+
description: string
12+
href?: string // Full criteria link
13+
lastUpdated: string
14+
children: ReactNode
15+
footers?: string[]
16+
}
17+
18+
const ListingMethodology = async ({
19+
heading,
20+
description,
21+
href,
22+
lastUpdated,
23+
children,
24+
footers,
25+
}: ListingMethodologyProps) => {
26+
const t = await getTranslations("component-listing-methodology")
27+
28+
return (
29+
<Section
30+
id="listing-methodology"
31+
aria-labelledby="methodology-heading"
32+
className="mt-12 border-t border-body-light pt-12 md:mt-16 md:pt-16"
33+
>
34+
<div className="flex w-full flex-col gap-6 px-4 pb-16 md:w-2/3 lg:w-3/5">
35+
<h2 id="methodology-heading" className="text-3xl font-bold md:text-4xl">
36+
{heading}
37+
</h2>
38+
39+
<p className="text-lg leading-relaxed text-body-medium">
40+
{description}
41+
</p>
42+
43+
{href && (
44+
<BaseLink href={href}>{t("full-criteria-link-label")}</BaseLink>
45+
)}
46+
47+
<div className="flex flex-col gap-1 text-base text-body-medium">
48+
<p>
49+
<strong>{t("attribution")}</strong>
50+
</p>
51+
<p>
52+
{t("last-update")} {lastUpdated}
53+
</p>
54+
</div>
55+
56+
<ExpandableCard
57+
title={t("details-title")}
58+
contentPreview={t("details-preview")}
59+
forceMount
60+
>
61+
<div className="space-y-4 text-lg leading-relaxed">
62+
{children}
63+
64+
{footers && (
65+
<div className="mt-6 space-y-2 border-t border-body-light pt-6 text-sm text-body-medium">
66+
{footers.map((footer) => (
67+
<p key={footer}>{footer}</p>
68+
))}
69+
</div>
70+
)}
71+
</div>
72+
</ExpandableCard>
73+
</div>
74+
</Section>
75+
)
76+
}
77+
78+
export default ListingMethodology
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"attribution": "Curated by the ethereum.org editorial team.",
3+
"details-preview": "Must-have requirements, re-verification policy, and disclaimers",
4+
"details-title": "See listing criteria and policies",
5+
"full-criteria-link-label": "Read the full listing criteria and removal policy",
6+
"last-update": "Most recent listing update:"
7+
}

src/intl/en/page-wallets-find-wallet.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,5 +87,18 @@
8787
"page-find-wallet-privacy": "Privacy",
8888
"page-find-wallet-privacy-desc": "Wallets that support built-in private transactions",
8989
"page-find-wallet-see-wallets": "See wallets",
90-
"page-find-wallet-search-languages": "Search languages..."
90+
"page-find-wallet-search-languages": "Search languages...",
91+
"page-find-wallet-methodology-title": "How we evaluate wallets",
92+
"page-find-wallet-methodology-intro": "Every wallet on this page is reviewed by the ethereum.org team before being listed. We apply a published set of criteria focused on security, self-custody, and Ethereum-native support so users can navigate the ecosystem with greater confidence.",
93+
"page-find-wallet-methodology-must-haves-label": "To be listed, a wallet must meet the following requirements:",
94+
"page-find-wallet-methodology-criterion-security": "Security-tested through audit, an internal security team, or open-source code review.",
95+
"page-find-wallet-methodology-criterion-track-record": "Been live for at least six months, or built by a team with an established track record.",
96+
"page-find-wallet-methodology-criterion-maintenance": "Actively maintained, with support available for users.",
97+
"page-find-wallet-methodology-criterion-honest-info": "Provides honest, accurate listing information. Products that falsify details are removed.",
98+
"page-find-wallet-methodology-criterion-contact": "Has a named point of contact so we can verify information when it changes.",
99+
"page-find-wallet-methodology-criterion-eip1559": "Supports EIP-1559 (type 2) transactions on Ethereum Mainnet.",
100+
"page-find-wallet-methodology-criterion-ux": "Offers a reviewable user experience. If our team finds a product difficult to use, we may request improvements before listing it.",
101+
"page-find-wallet-methodology-criterion-ethereum-focused": "Is Ethereum-focused, with Ethereum or a Layer 2 set as the default network.",
102+
"page-find-wallet-methodology-verification": "Listings are not static. Wallet providers are required to resubmit information every six months. If a team does not respond, we remove the wallet. This keeps the directory accurate as products evolve.",
103+
"page-find-wallet-methodology-filters": "Filter toggles on this page (open source, self-custody, hardware wallet support, and others) reflect attributes tracked per wallet. Each listing also shows the date its information was last verified."
91104
}

src/scripts/intl-pipeline/main.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -901,7 +901,10 @@ async function main() {
901901
// so a rerun of just the failed combinations naturally retries them without
902902
// touching the work that landed this run.
903903
if (failures.length > 0) {
904-
log(`${failures.length} task(s) failed (continuing with successes):`, "warn")
904+
log(
905+
`${failures.length} task(s) failed (continuing with successes):`,
906+
"warn"
907+
)
905908
for (const f of failures) {
906909
log(` [${f.locale}] ${f.file}: ${f.message}`, "warn")
907910
}

0 commit comments

Comments
 (0)