Skip to content

chore(marketing): consolidate the stray pages, and give /careers a real one - #2767

Closed
0xkkonrad wants to merge 6 commits into
devfrom
chore/stray-pages-consolidation
Closed

chore(marketing): consolidate the stray pages, and give /careers a real one#2767
0xkkonrad wants to merge 6 commits into
devfrom
chore/stray-pages-consolidation

Conversation

@0xkkonrad

Copy link
Copy Markdown
Contributor

Cleans up the stray marketing pages on peanut.me, found in a routing + copy audit of origin/dev. Six commits, each independently reviewable.

Two items from that audit are deliberately not here, per Konrad: the public /dev/* pages (filed as a Notion task for Jota instead) and the /pricing fee wording.

What changes

/careers becomes a real page. It was a 30-word interstitial — a mascot, one sentence, a link out to Notion. No <h1>, no footer, no Crisp, and metadata still selling "Instant Global P2P Payments". The footer's Jobs link bypassed it entirely for a second URL pointing at the same Notion page. It now has its own content (what Peanut is, how the work works, open roles), sourced from content/press/en.md so nothing is invented, and Notion is reached only by Apply on Notion. The footer now links /careers.

/shhhhh drops the card reward claims. The Rain resubmission note records the "$10 after $100 card spend" activation reward and the "$10 / $10 referral" as removed everywhere they appeared. Both were still live in all four locales — and the same claim was live in the app home carousel. Also moves the "where can I get the card?" answer from issuance framing to acceptance framing, per the rule that public card copy describes acceptance only. Peanut's general referral share is untouched; the ruling keeps it, because it isn't tied to the card.

Four routes retired, all permanent redirects:

Route Why
/lp/card /shhhhh Dead behind disableCardPioneers: 200 with full metadata, zero body copy, sitemap priority 0.9
/exchange / Renders the same NoFees component already on the homepage; ships an empty client shell
/:locale/blog /:locale/content Byte-identical to /content with fixedType="blog". Index only — all 21 posts survive
/:locale/team /:locale/press /press already renders the same two members from the same source, with real photos

The content hub is crawlable again. /en/content and /en/blog emitted zero article links in server HTML while the sitemap listed 21 posts — ContentLanding is a client component read through nuqs, so Next prerenders the Suspense fallback, and the fallback was a pulsing skeleton. The list rendering is now an exported ContentLinkList that touches no URL state, and the fallback renders it.

Plus: /en/stories/index (a live duplicate of its own hub, linked from that hub) is gone; /press section headings are localized; three client routes stop declaring the homepage as their canonical; the root title and description drop the protocol-era framing; @PeanutProtocol@joinpeanut in all six places; and the five card legal pages join the sitemap.

Ragdoll. It had no prefers-reduced-motion handling at all — it now follows confetti.ts and degrades to a single settled frame rather than vanishing. Two new homes, both non-scrolling panes with a definite height: /maintenance (whose copy was the worst voice on the site) and the waitlist jail pane, gated on step === 'jail' so plain /invite doesn't get a draggable toy beside its CTA.

One real bug fixed on the way

The (mobile-ui) layout registers pull-to-refresh on document before the waitlist early-return, and its #scrollable-content guard is absent on that screen, so the callback fell through to true. Dragging the peanut down ~160px would have fired window.location.reload() and wiped the user's typed invite code. The ragdoll host now swallows touchstart — a native listener, because React hydrates on document and a synthetic stopPropagation wouldn't stop a same-target sibling.

Verification

  • npx jest261 suites, 3344 tests, 0 failures
  • npx tsc --noEmit0 errors
  • npx eslint on all 37 changed TS/TSX files — 0 errors; prettier --check clean
  • verify-content.ts — Pass 11 green, all 1165 expected URLs have routes
  • generateSitemap() executed: 742 URLs, 21 blog posts intact, 0 blog-index, 5 card-legal, /careers still present
  • Redirects checked statically: no duplicate sources, every source route removed, every destination resolves. The two pre-existing /:path* catch-alls are host-scoped to docs.peanut.* and can't shadow these
  • ContentLinkList proven to server-render real anchors (/en/blog/rewards, /en/stories/lynn) with the | Peanut suffix stripped

Not verified locally: next build could not complete — earlyoom SIGTERMs the build worker at ~2.7 GB on this box, with zero compile or type errors in the log before the kill. CI is the real gate for the build. Runtime redirect behaviour also can't be exercised locally: next.config.js:363 deliberately applies redirects.json only outside development.

Coordination

Follow-ups, deliberately not in this PR

  • SEOFooter still links Jobs → Notion from footer-manifest.json in the peanut-content submodule. That's a content change, and content and code don't travel in the same PR.
  • The /shhhhh "20 a week" strings predate the ruling recorded in product/card.md that the pace is now ~50/week — 7 catalog strings, 1 metadata line, and 3 hardcoded literals in ScarcityCounter.
  • /en/help looks to have the same client-component crawlability gap: its prod HTML carries only 3 /en/help/ links.
  • /maintenance and the 404 are still hardcoded English.

…ions only

/careers was a 30-word interstitial: a mascot, one sentence, and a link out
to Notion. No <h1>, no footer, no Crisp, and metadata still selling 'Instant
Global P2P Payments' from the protocol era. The footer's Jobs link bypassed
it entirely, pointing at a second URL for the same Notion page.

The page now carries its own content — what Peanut is, how the work works,
and the open roles — sourced from content/press/en.md so nothing is invented.
Notion is reached only by 'Apply on Notion'. The footer Jobs link now points
at /careers instead of routing around it.

Uses the public *.notion.site URL; the old www.notion.so workspace URL
bounced logged-out visitors to a login.
Peanut's Rain resubmission note records the '$10 after $100 card spend'
activation reward and the '$10 / $10 referral' as removed everywhere they
appeared. Both were still live on /shhhhh in all four locales, and the same
claim was live in the app home carousel.

- shhhhh.faq.q4 no longer carries the reward. The FAQ list is a hardcoded
  q1..q8 array, so the entry is replaced rather than deleted — renumbering
  four catalogs to remove one answer buys nothing. It now answers identity
  verification, which Rain's own guidance states.
- shhhhh.faq.q6 moves from issuance framing ('rolling out to select regions
  first') to acceptance framing, per the rule that public card copy describes
  acceptance and never availability.
- home.carousel.card.description drops the same reward clause.

The es-AR override for the carousel is deleted rather than rewritten: it only
existed for a voseo imperative inside the reward clause, and es-AR must stay a
strict subset of es-419 or it silently stops inheriting fixes.

Peanut's general referral share is untouched — the ruling keeps it, because it
is not tied to the card.
Four marketing routes that carried no unique content, all now permanent
redirects:

- /lp/card -> /shhhhh. Dead behind disableCardPioneers: 200 with full 'Card
  Pioneers' metadata and zero body copy, while sitting in the sitemap at
  priority 0.9. Matches PR #2690's destination and wording so the two do not
  diverge.
- /exchange -> /. Rendered the same NoFees component already on the homepage
  plus a footer, shipped an empty client shell, and its title promised 'Live
  Exchange Rates' while its H1 said 'ZERO TRANSFER FEES'. NoFees itself is
  untouched and still used by the homepage and /quests.
- /:locale/blog -> /:locale/content. Byte-identical to /content with
  fixedType='blog'. Only the index is gone; all 21 posts survive.
- /:locale/team -> /:locale/press. /press already renders the same two member
  cards from the same source, with real photos.

Fallout cleaned up in the same pass: robots.ts no longer allows two routes
that are now 308s; CardPioneers pushes to /shhhhh instead of client-navigating
into a redirect; and the blog post breadcrumb — which feeds a BreadcrumbList
JSON-LD — now points at /content?type=blog rather than naming a redirecting
URL. That form matches how /compare and /use-cases already link back, and
keeps the crumb reading 'Blog'.

Also drops the now-dead 'team' entry from ROUTE_SLUGS and refreshes comments
that cited the retired routes as live.
…root identity

- /crisp-proxy, /kyc/success and /maintenance defined no metadata, so all
  three inherited the root layout's canonical: '/' and declared the homepage
  as their canonical while serving index, follow. Each is a client component,
  so metadata cannot live in the page — added sibling layouts carrying a
  self-canonical plus robots noindex, the pattern /exchange already used.
- Root title was 'Peanut - Instant Global P2P Payments in Digital Dollars',
  protocol-era framing inherited by every page without an override. The root
  description said the same thing. Both now describe the money app.
- The X handle @peanutprotocol appeared in six places while the footer links
  x.com/joinpeanut. All six now say @joinpeanut, including the Organization
  JSON-LD sameAs and llms-full.txt.
- Adds the five card legal pages to the sitemap. They return 200 with index,
  follow and were listed nowhere. English only: the localized copies serve
  English and already canonicalize to /en, so listing all twenty would create
  fifteen duplicates competing with their own canonical.
…ate, localize /press

- /en/content and /en/blog emitted zero article links in server HTML while the
  sitemap listed 21 posts. ContentLanding is a client component read through
  nuqs, so Next prerenders the Suspense fallback — and the fallback was a
  pulsing skeleton. The list rendering is now an exported ContentLinkList that
  touches no URL state, and the fallback renders it, so the links exist in the
  prerendered HTML. Interactive filtering is unchanged.
- Content cards rendered the raw frontmatter title, leaking the SEO suffix into
  the UI ('Lynn Pham: The Brand Girl | Peanut'). Stripped at render time only;
  sitemap and metadata still read the raw value.
- /en/stories linked to /en/stories/index, a live second copy of the hub with a
  self-referencing canonical and different hero copy. The page now applies the
  same slug === 'index' guard the sitemap already had.
- /press rendered all six section headings as hardcoded English on a [locale]
  route. They now come from the marketing catalogs, translated in all four.
- /press absorbs the team data: its Organization JSON-LD gains the member[]
  array that only /team used to emit, so deleting /team does not drop the
  site's only Person schema.

Drops the now-unused fixedType prop and the teamTitle/teamSubtitle keys, both
orphaned by the routes retired in this branch.
…nce and the waitlist

The ragdoll had no prefers-reduced-motion handling at all. It now follows the
same shape as confetti.ts — matchMedia read live per mount — and degrades
rather than disappearing: the world is stepped off-screen until the peanut
settles, then painted once, with no RAF loop and no pointer listeners.

Two new homes, both non-scrolling panes with a definite height:

- /maintenance replaces a static crying-peanut image. The copy was the worst
  voice on the site ('We are currently going through maintenance... Sorry for
  the inconvenience') and a button labelled 'Contact Support?'. Rewritten to
  match the 404's register.
- The waitlist jail pane, gated on step === 'jail' so plain /invite — a signup
  conversion surface — does not get a draggable toy next to its CTA. The
  illustration pane moves from min-h to a definite h-[55dvh]; min-h gives
  h-full nothing to resolve against, so the canvas would have silently
  collapsed to zero height while the loop kept running.

Fixes a real bug found while placing it: the (mobile-ui) layout registers
pull-to-refresh on document before the waitlist early-return, and its
#scrollable-content guard is absent on that screen, so the handler fell
through to true. Dragging the peanut down ~160px would have reloaded the page
and wiped the user's typed invite code. The host now swallows touchstart — a
native listener, because React hydrates on document and a synthetic
stopPropagation would not stop a same-target sibling.

startRagdoll now runs in a try/catch: it throws synchronously on a failed 2D
context, which would white-screen /maintenance, the one page whose job is to
work when nothing else does.

Also retires the stale 'triple-click Rewards title' comments — that easter egg
was deliberately cut in 347757d and never shipped.
@vercel

vercel Bot commented Aug 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview Aug 20, 2026 2:01pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e7f70835-47b6-4546-bff1-3a381f46ea9c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 7289.31 → 7275.96 (-13.35)
Findings: -27 net (+47 new, -74 resolved)

🆕 New findings (47)

  • critical complexity — src/components/PeanutRagdoll/ragdoll.ts — CC 140, MI 51.34, SLOC 779
  • critical complexity — src/components/Invites/JoinWaitlistPage.tsx — CC 84, MI 56.03, SLOC 293
  • high complexity — src/app/[locale]/(marketing)/press/page.tsx — CC 42, MI 61.59, SLOC 97
  • high hotspot — src/components/Invites/JoinWaitlistPage.tsx — 30 commits, +463/-204 lines since 6 months ago
  • high complexity — src/app/sitemap.ts — CC 29, MI 47.39, SLOC 228
  • medium high-mdd — src/components/PeanutRagdoll/ragdoll.ts:113 — startRagdoll: MDD 198.0 (uses across many lines from declarations)
  • medium high-dlt — src/components/PeanutRagdoll/ragdoll.ts:113 — startRagdoll: DLT 85 (calls 85 distinct functions — high context load)
  • medium high-mdd — src/app/sitemap.ts:58 — generateSitemap: MDD 79.0 (uses across many lines from declarations)
  • medium high-mdd — src/app/[locale]/(marketing)/press/page.tsx:102 — PressPage: MDD 63.8 (uses across many lines from declarations)
  • medium high-mdd — src/components/LandingPage/Footer.tsx:57 — Footer: MDD 48.6 (uses across many lines from declarations)
  • medium high-mdd — src/components/Invites/InvitesPageLayout.tsx:32 — InvitesPageLayout: MDD 41.0 (uses across many lines from declarations)
  • medium high-mdd — src/components/PeanutRagdoll/ragdoll.ts:341 — buildWorld: MDD 38.6 (uses across many lines from declarations)
  • medium high-mdd — src/app/[locale]/(marketing)/blog/[slug]/page.tsx:52 — BlogPostPageLocalized: MDD 33.6 (uses across many lines from declarations)
  • medium structural-dup — app/[locale]/(marketing)/card-esign/page.tsx:24 — 30 duplicate lines / 172 tokens with app/[locale]/(marketing)/card-privacy/page.tsx:24
  • medium structural-dup — app/[locale]/(marketing)/card-esign/page.tsx:24 — 30 duplicate lines / 172 tokens with app/[locale]/(marketing)/card-prohibited-activities/page.tsx:24
  • medium structural-dup — app/[locale]/(marketing)/card-esign/page.tsx:24 — 30 duplicate lines / 172 tokens with app/[locale]/(marketing)/card-terms-international/page.tsx:24
  • medium structural-dup — app/[locale]/(marketing)/card-esign/page.tsx:24 — 30 duplicate lines / 172 tokens with app/[locale]/(marketing)/card-terms-us/page.tsx:24
  • medium structural-dup — app/[locale]/(marketing)/card-esign/page.tsx:24 — 30 duplicate lines / 172 tokens with app/[locale]/(marketing)/privacy/page.tsx:25
  • medium structural-dup — app/[locale]/(marketing)/card-esign/page.tsx:24 — 30 duplicate lines / 172 tokens with app/[locale]/(marketing)/terms/page.tsx:25
  • medium complexity — src/components/Marketing/ContentLanding.tsx — CC 28, MI 68.09, SLOC 89

…and 27 more.

✅ Resolved (74)

  • src/components/PeanutRagdoll/ragdoll.ts — CC 131, MI 51.47, SLOC 753
  • src/components/Invites/JoinWaitlistPage.tsx — CC 84, MI 56.04, SLOC 293
  • src/app/[locale]/(marketing)/press/page.tsx — CC 40, MI 61.93, SLOC 87
  • src/app/sitemap.ts — CC 26, MI 43.05, SLOC 219
  • src/app/lp/card/CardLandingPage.tsx — CC 15, MI 45.15, SLOC 308
  • src/app/lp/card/CardLandingPage.tsx:44 — CardLandingPage: MDD 318.2 (uses across many lines from declarations)
  • src/components/PeanutRagdoll/ragdoll.ts:95 — startRagdoll: MDD 190.1 (uses across many lines from declarations)
  • src/components/PeanutRagdoll/ragdoll.ts:95 — startRagdoll: DLT 83 (calls 83 distinct functions — high context load)
  • src/app/sitemap.ts:49 — generateSitemap: MDD 80.0 (uses across many lines from declarations)
  • src/app/[locale]/(marketing)/press/page.tsx:101 — PressPage: MDD 56.9 (uses across many lines from declarations)
  • src/components/LandingPage/Footer.tsx:57 — Footer: MDD 49.2 (uses across many lines from declarations)
  • src/components/Marketing/ContentLanding.tsx:32 — ContentLanding: MDD 41.0 (uses across many lines from declarations)
  • src/components/PeanutRagdoll/ragdoll.ts:321 — buildWorld: MDD 38.6 (uses across many lines from declarations)
  • src/app/[locale]/(marketing)/blog/[slug]/page.tsx:52 — BlogPostPageLocalized: MDD 33.0 (uses across many lines from declarations)
  • app/[locale]/(marketing)/blog/page.tsx:17 — 32 duplicate lines / 172 tokens with app/[locale]/(marketing)/card-privacy/page.tsx:24
  • app/[locale]/(marketing)/blog/page.tsx:17 — 32 duplicate lines / 172 tokens with app/[locale]/(marketing)/card-prohibited-activities/page.tsx:24
  • app/[locale]/(marketing)/blog/page.tsx:17 — 32 duplicate lines / 172 tokens with app/[locale]/(marketing)/card-terms-international/page.tsx:24
  • app/[locale]/(marketing)/blog/page.tsx:17 — 32 duplicate lines / 172 tokens with app/[locale]/(marketing)/card-terms-us/page.tsx:24
  • app/[locale]/(marketing)/blog/page.tsx:17 — 32 duplicate lines / 172 tokens with app/[locale]/(marketing)/privacy/page.tsx:25
  • app/[locale]/(marketing)/blog/page.tsx:17 — 32 duplicate lines / 172 tokens with app/[locale]/(marketing)/terms/page.tsx:25

…and 54 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/components/Jobs/openRoles.ts 0.0 3.3 +3.3
src/app/crisp-proxy/layout.tsx 0.0 3.0 +3.0
src/app/kyc/success/layout.tsx 0.0 3.0 +3.0
src/app/maintenance/layout.tsx 0.0 3.0 +3.0
src/components/Jobs/Careers.tsx 0.0 2.4 +2.4
src/components/Jobs/RoleCard.tsx 0.0 1.8 +1.8
src/components/PeanutRagdoll/index.tsx 4.2 5.4 +1.2
src/app/sitemap.ts 11.6 10.7 -0.9
src/components/Jobs/index.tsx 1.7 0.0 -1.7
src/app/exchange/page.tsx 2.1 0.0 -2.1
src/app/exchange/layout.tsx 2.5 0.0 -2.5
src/app/lp/card/page.tsx 2.7 0.0 -2.7
src/app/[locale]/(marketing)/blog/page.tsx 5.5 0.0 -5.5
src/app/[locale]/(marketing)/team/page.tsx 7.4 0.0 -7.4
src/app/lp/card/CardLandingPage.tsx 11.8 0.0 -11.8

@github-actions

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 3347 ran, 0 failed, 0 skipped, 1.0m

📊 Coverage (unit)

metric %
statements 68.2%
branches 53.4%
functions 59.2%
lines 69.1%
⏱ 10 slowest test cases
time test
4.1s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.0s src/hooks/query/__tests__/user.test.tsx › does NOT clear a token that rotated mid-request (stale 401 racing a fresh login)
0.9s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.8s src/utils/__tests__/crisp.test.ts › resets the native session on logout once support has been opened
0.6s src/utils/__tests__/crisp.test.ts › settles, and hands back a usable plugin, against a real-shaped plugin proxy
0.5s src/utils/__tests__/sentry.utils.test.ts › defaults to the client budget under a browser global
0.4s src/utils/__tests__/auth-token.test.ts › authReady does not park — hydrates the plain token without an unlock
0.4s src/utils/__tests__/crisp.test.ts › configures once across repeated support opens
0.3s src/utils/__tests__/crisp.test.ts › retries configuration on the next open after a failure
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › Bank withdrawal keeps the $1 minimum for sub-$1 amounts
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@0xkkonrad

Copy link
Copy Markdown
Contributor Author

Superseded by #2782, which is rebased onto current dev.

Two parts of this PR were overtaken while it sat open:

Everything else still applies and is in #2782: the real /careers page, the /exchange, blog-index and /team retirements, the content-hub crawlability fix, the stories/index duplicate, the canonical/identity work, and the ragdoll reduced-motion guard plus its two placements. Plus the footer label, which now reads Careers.

One correction from this PR's description: the claim that SEOFooter still links Jobs to Notion was wrong — RESOURCES_MOVED_ELSEWHERE already suppresses that manifest entry (#2755), so no content-submodule change is needed.

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.

1 participant