Skip to content

fix(seo): ssr the wallet list on /wallets/find-wallet#18067

Merged
wackerow merged 16 commits into
devfrom
fix/find-wallet-ssr-17717
Apr 30, 2026
Merged

fix(seo): ssr the wallet list on /wallets/find-wallet#18067
wackerow merged 16 commits into
devfrom
fix/find-wallet-ssr-17717

Conversation

@pettinarip
Copy link
Copy Markdown
Member

@pettinarip pettinarip commented Apr 29, 2026

Summary

Fixes #17717 — Googlebot was indexing the loading skeleton on /wallets/find-wallet (zero wallet content in SSR HTML). This branch restores SSR coverage for SEO, then defends INP against the regression that SSR introduces.

SSR coverage

  • Render the wallet list and filter sidebar on the server: drop ssr: false, drop MediaQuery wrappers, drop the dynamic-import skeleton, replace useSearchParams() with window.location.search to avoid Next's silent CSR-bailout on this SSG'd page.

INP defense

  • Drop list virtualization; lazy-mount the heavy expanded sub-component per row.
  • Replace Radix Collapsible with native <details>/<summary> (with the same focus-visible ring as Button).
  • Unify WalletInfo desktop + mobile trees behind a single CSS Grid (was two parallel hidden trees, both hydrating).
  • Hide-not-unmount filtered rows via a matchedIds: Set<string> — filter clicks no longer churn 52 subtrees.
  • Row is memo'd and the lazy sub-component is passed via children, so closed rows skip re-render on filter changes.
  • Tracking effect on the expanded sub-component fires once per expand (was once per render).

Verification

dev.ethereum.org (deployed) this branch
Wallet names in SSR HTML 0 52

INP (Playwright iPhone 12, 4× CPU throttle):

Scenario dev.ethereum.org this branch
Filter open — steady p75 24 ms 32 ms
Wallet expand — steady p75 40–48 ms 48–56 ms
Preset filter — steady p75 0–16 ms 0–16 ms
Filter open — early p75¹ 24–32 ms 432 ms
Wallet expand — early p75¹ 96–112 ms 456–488 ms

¹ dev's "early" measurement happens post-hydration since the page renders client-side; not directly comparable. Steady-state INP is equivalent. The remaining early-click cost is concentrated in the hydration window of the 52 wallet trees the page now ships.

Wire size (brotli, deploy-preview-18067 vs prod): 78 kb → 107 kb (+29 kb). Uncompressed grows 535 kb → 1452 kb because we now ship 52 cards' worth of markup + inline lucide SVGs; brotli compresses the repetition well, so wire impact is small.

Test plan

  • SSR HTML contains all 52 wallets
  • Visual parity at desktop + mobile, open + closed
  • Filter toggle keeps 52 rows in DOM, hides non-matches
  • URL params pre-filter correctly
  • No-results UI + reset still work
  • Layer2NetworksTable (other ProductTable consumer) unaffected
  • Visit-website link inside <summary> doesn't toggle the row (manual)
  • TypeScript + ESLint pass
  • CI green on Netlify
  • CrUX / RUM after merge to confirm real-world INP impact

Restores server-rendered wallet content so Googlebot sees real wallet
names, chips, devices, languages, supported chains, and the Visit
website link instead of the Loading skeleton.

- Remove ssr:false from FindWalletProductTable dynamic import so the
  component is emitted in initial HTML.
- Progressive virtualization in ProductTable/List: SSR + first client
  render emit the first 30 rows unvirtualized (in natural flow, no
  absolute positioning) so server and hydration HTML match; the
  useWindowVirtualizer takes over on mount for scroll perf on the full
  list.
- Drop the MediaQuery wrappers around WalletInfo's mobile/desktop
  layouts. Both are already gated by Tailwind responsive classes
  (hidden lg:flex / lg:hidden), but MediaQuery returned null on the
  server (no fallbackMatches), which stripped the wallet name and
  Visit website button from SSR HTML.

Fixes #17717
The component rendered two parallel trees for image+name+PersonaTags+
ChainImages, gated only by `hidden`/`lg:hidden`. Both hydrated on every
device, doubling the per-row React node count for 52 rows.

Single CSS Grid (`grid-cols-[auto_1fr]`, image `lg:row-span-full`,
right-column children `col-span-2 lg:col-start-2`) reproduces both
layouts from one tree. The two duplicated stripe spacers collapse into a
single absolute element on the relative outer container.

Cuts ~30% of WalletInfo nodes per row, attacking the dominant early-click
hydration cost identified in the find-wallet INP follow-ups.
Filtering recomputed `filteredData` and React unmounted/remounted every
non-matching Row on each filter change — cheap per click, but each remount
re-instantiates 52 WalletInfo subtrees in aggregate as users tweak.

Render every wallet always; toggle visibility with `display: none` via a
matchedIds Set. Filter changes become re-renders that flip a class —
no mount/unmount of WalletInfo/Row subtrees.

ProductTable derives `matchedIds` from the same filterFn output as
filteredData and exposes it (plus `data`) through the children render
prop. List takes optional `matchedIds`; when omitted, every row is
visible (legacy path used by Layer2NetworksTable). FindWalletProductTable
switches to the new path; SSR still emits all 52 rows for SEO.
The `cn()` call already encodes the conflict-resolution behavior; the
prose comment was duplicating what the conditional already says.
@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 29, 2026

Deploy Preview for ethereumorg ready!

Name Link
🔨 Latest commit 99ecb53
🔍 Latest deploy log https://app.netlify.com/projects/ethereumorg/deploys/69f3bb77162bc60008ecb319
😎 Deploy Preview https://deploy-preview-18067.ethereum.it
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
7 paths audited
Performance: 64 (🟢 up 10 from production)
Accessibility: 96 (🟢 up 3 from production)
Best Practices: 100 (no change from production)
SEO: 98 (🔴 down 1 from production)
PWA: 59 (no change from production)
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

Same pattern as /wallets/find-wallet (#18067, #17717): the page mounted
Layer2NetworksTable through a `dynamic(..., { ssr: false })` wrapper,
so Googlebot indexed only the loading skeleton — zero network names in
the HTML.

Drop the lazy wrapper and import directly. Add `"use client"` to
`Layer2NetworksTable` since it needs hooks (`useTranslation`,
`useNetworkFilters`) and is no longer crossing the boundary via
`dynamic()`. Delete the now-unused `lazy.tsx` and `loading.tsx`.

The shared `ProductTable` SSR fixes (`useSearchParams` → window.location,
`MediaQuery` wrapper removal) already landed in #18067 and apply
unchanged to this consumer.
@pettinarip pettinarip marked this pull request as ready for review April 30, 2026 11:28
{/* Open-state stripe (desktop only), sits in the image-column gutter. */}
<div
aria-hidden
className={`pointer-events-none absolute top-14 -bottom-9 left-7 hidden w-1 -translate-x-1/2 lg:group-[[open]]/collapsible:block ${wallet.twBackgroundColor}`}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Patched in 9923abf

Co-Authored-By: wackerow <54227730+wackerow@users.noreply.github.com>
Copy link
Copy Markdown
Member

@wackerow wackerow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good @pettinarip! Spotting a could things we could clean up, but they don't need to block.

The gutter decoration line loads a little buggy when expanding the card—could be nice to see if there is a cleaner way to have this load as a single dom element, perhaps even with a smooth animation so it's not so abrupt.

Also some clean up here with string interpolating classes–could use cn() to pass this as it's own arg.

Pushed a commit to patch an RTL bug, adjusting the positioning of that gutter decoration. Pulling in!

fix(seo): ssr the network list on /layer-2/networks
@wackerow wackerow merged commit 5f77152 into dev Apr 30, 2026
5 checks passed
@wackerow wackerow deleted the fix/find-wallet-ssr-17717 branch April 30, 2026 20:29
@wackerow wackerow mentioned this pull request May 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(seo): /wallets/find-wallet/ serves no wallet content to search engines (ssr: false)

3 participants