Skip to content

chore: sync staging with prod#1703

Merged
Hugo0 merged 70 commits intodevfrom
chore/sync-131
Feb 27, 2026
Merged

chore: sync staging with prod#1703
Hugo0 merged 70 commits intodevfrom
chore/sync-131

Conversation

@kushagrasarathe
Copy link
Contributor

No description provided.

Hugo0 and others added 30 commits February 16, 2026 16:37
Marketing pages looked like a different product from /lp and /quests.
Upgrades 9 shared components so all ~350 programmatic pages inherit
the brand treatment: CloudsCss hero backgrounds, marquee strips,
solid shadow-4 cards, bolder typography, and higher-contrast text.
Add localized marketing pages (350+ routes across en/es/pt):
- Country hub, corridor, from-to, compare, convert, deposit,
  pay-with, receive-money, blog, and team pages
- i18n framework with locale-aware content and hreflang tags
- SEO data layer (corridors, exchanges, competitors, countries)
- JSON-LD structured data (BreadcrumbList, HowTo, FAQPage, etc.)
- Blog engine with markdown/frontmatter support

Design system and LP improvements:
- DS showcase at /dev/ds with primitives, tokens, and patterns
- CloudsCss pure-CSS cloud animations (no JS, SSR-safe)
- LP refactor: LandingPageShell, LandingPageClient, AnimateOnView
- Button/Card/Input primitive refinements

Code quality fixes from review:
- Fix XSS in JsonLd component (escape </script> in JSON output)
- Extract findMappingBySlug() helper (DRY, was duplicated 10x)
- Fix DS primitive count (10→9)
- Add optional `image` prop to MarketingHero, pass through from compare/deposit pages
- Add `isPublished()` helper to content.ts, filter drafts in listEntitySlugs()
- Update all 4 entity loaders to skip draft entities
- Corridors filter: both from/to countries must be published
- Update content symlink to track peanut-content changes
- Run prettier on all 60 files flagged by CI
- Add `dynamicParams = false` to all marketing pages + layout
  (prevents runtime SSR for unknown params like /fr/blog)
- Add file-level Map cache to content.ts readers
  (eliminates redundant fs reads during static generation)
- Extract findMappingBySlug() and fix DS primitive count (from prev)
- Use API contributedPoints instead of local TRANSITIVITY_MULTIPLIER calc
- Add formatPoints (thousands separators) and shortenPoints (K/M with pink suffix)
- Binary CashCard: "Lifetime cashback claimed" + status text
- Count-up animations on /points hero (localStorage memory across visits)
- Scroll-triggered count-up on invitee points badges
- Extract InviteePointsBadge to shared component (DRY)
- Remove TRANSITIVITY_MULTIPLIER constant (server-side only now)
Remove display from second useEffect deps and add isAnimating guard
to prevent snap-to-target firing mid-animation.
Cherry-picked from peanut-wallet-dev:
- 71fe5d3: fix kernel client ready state + transaction query key bug
- f24d698: add retry logic before kernel client logout
- 79e5c6f: move state updates after primary client check to prevent UI flicker
fix: kernel client stuck loading screen + retry logic
Replace i18n template-string page rendering with MDX-driven content.
LLM-generated markdown now embeds React components directly:

- Add next-mdx-remote + remark-gfm for server-side MDX compilation
- New MDX component library (src/components/Marketing/mdx/):
  Hero, Steps, FAQ, CTA, Callout, RelatedPages, CountryGrid,
  ExchangeWidget, ProseStars (animated stars on scroll)
- ContentPage wrapper with BreadcrumbList JSON-LD
- Rewrite send-money-to/[country] route to compile MDX content
- Refactor i18n: split es.json/pt.json into locale-specific files
  (es-419, es-ar, es-es, pt-br) matching content locales
- Refactor data loaders to read from content submodule
- Improve CSS spring animation for bouncier star entrance
- Remove MarketingNav and duplicate marquee from layout
Phase 1: All 7 marketing page routes now render MDX content first
with React fallback. Pages without MDX content return 404 or use
remaining fallback components.

Phase 4 cleanup:
- Delete 4 fallback React components (CorridorPageContent,
  HubPageContent, FromToCorridorContent, PayWithContent)
- Remove fallback blocks from send-money-to, country hub,
  send-money-from, and pay-with page routes
- Remove 26 unused i18n keys from all 5 locale JSONs + types
- Keep ReceiveMoneyContent (no MDX content yet) and compare/deposit
  fallbacks as safety nets

Fix: Add spacer after Hero marquee for consistent prose spacing
on pages without ExchangeWidget (e.g. compare pages).

Update content submodule to include Phase 3 template updates.
… script

- Add visible breadcrumb nav to ContentPage (all MDX pages) and blog posts
- Add FAQPage JSON-LD schema on blog posts via frontmatter `faqs` field
- Use per-entry lastModified in sitemap (blog posts use post date)
- Add scripts/ping-sitemap.sh for post-deploy sitemap submission
Resolve CashCard conflict: keep cashbackAllowance (dollar amounts) over hasCashbackLeft (binary)
…d origin

- Flip published: true on all 161 content pages in submodule
- Guard corridors.ts against entity files using destination: instead of origin:
  (prevents undefined from poisoning generateStaticParams)
…tent

- Move breadcrumb nav to bottom of article inside bg-background
- Add MarketingErrorBoundary wrapper around content pages
- Add SLUG_TO_ISO2 map for all 27 SEO country flags in DestinationGrid
- Restyle Hero: use Roboto Flex bold instead of knerd Title component
- Decouple ExchangeWidget from Hero, add sourceCurrency prop, blue bg
- Add Tabs/TabPanel MDX components
- Add h1 prose override in MDX component map
- Replace "Fiat / Crypto" and "CRYPTO" with "USDT/USDC" in marquees
- Update content submodule: remove duplicate headings before Steps/FAQ
- Fix currency selector dropdown clipped by overflow-hidden: move clouds
  and stars into their own overflow-hidden wrapper with pointer-events-none
- Update content submodule: strip currency= from 140 Hero tags, add
  inline <ExchangeWidget> on 70 send-to/corridor pages, update 4 templates
- Add migration script (scripts/migrate-exchange-widget.py)
…add error boundary

- Fix OpenGraph URL to use canonical path instead of always pointing to homepage
- Generate robots.txt locale allowlist from SUPPORTED_LOCALES (was missing es-419, es-ar, es-es, pt-br)
- Extract shared extractFaqs/extractSteps/extractTroubleshooting into src/data/seo/utils.ts
- extractFaqs now also supports <FAQItem> MDX syntax as fallback
- Add isPublished check in comparisons.ts (was skipped during transition)
- Add error.tsx boundary for marketing routes
- Add content validation script (pnpm validate-content)
…ction

- Delete convert pages (284 thin currency pair pages with no real content)
- Delete send-money-to index page (country grid, no unique content)
- Remove convert.ts data loader and its exports from seo/index.ts
- Remove convert + send-money-to index + blog + team from sitemap
- Gate blog and team generateStaticParams to dev-only (code kept, not built in prod)
- Simplify breadcrumbs in send-money-to/[country] and send-money-from/[from]/to/[to]
- Fix LandingPageClient scroll freeze on mobile (add touchmove handler, use refs)
- Fix Card Pioneer modal showing to purchased users when card API is down
- Add contributedPoints null guard to prevent "NaN pts" rendering
- Fix typo "Card Purchare" → "Card Purchase"
- Guard PEANUT_API_KEY env var with descriptive error
- Fix React.ReactNode import in LandingPageShell
- Prettier fix on RelatedPages.tsx
Button was disabled with no spinner while user data was being fetched
during setup, making the UI feel stuck. Now shows spinner + "Loading..."
until user is available.
Hugo0 and others added 24 commits February 26, 2026 03:00
* Add help center at /help with article pages, landing, and Crisp chat

- Create /[locale]/help landing page with category-grouped articles and search
- Create /[locale]/help/[slug] article pages with FAQPage + HowTo JSON-LD
- Add Crisp floating chat widget on help pages via layout
- Add help articles to sitemap
- Redirect /docs → /help (replaces external docs.peanut.me redirect)
- Add "Browse help articles" link to in-app /support page
- Add 'help' to i18n ROUTE_SLUGS for hreflang alternates
- Update content submodule with category + published frontmatter

* Fix help center: match marketing styling, /support redirect, /help routing

- Restyle HelpLanding to match SEO page design (prose width, content-page
  class, primary-3 hover, blockquote CTA, arrow-up-right icons)
- Replace /support page with server redirect to /en/help?chat=open
- Auto-open Crisp chat widget when ?chat=open query param is present
- Add /help → /en/help redirect in redirects.json for bare URL access
- Update /docs redirects to point to /en/help (not /help)
- Add $crisp type to Window interface
- Wrap HelpLanding in Suspense (useSearchParams requires it)

* Deprecate peanut.to: migrate all references to peanut.me

Replace all peanut.to URLs with peanut.me across production code:
- proxy.ts: promo link redirect
- CardInfoScreen: FAQ link
- did.json: WalletConnect DID identity
- wagmi mock, dev examples, QR generator docs, SVG easter egg

Archive tags created for old branches:
- archive/peanut-to → main (final peanut.to app, Nov 2025)
- archive/legacy-peanut-to → legacy (batch/raffle app, Mar 2025)

* Expand help center: i18n, Crisp chat, 11 new articles

- Move Crisp SDK from help layout to marketing layout (all marketing pages)
- Delete redundant help/layout.tsx
- Add #chat click interceptor (capture phase) for Crisp chat links
- Fix CTA component: use plain <a> for hash hrefs to prevent scroll
- Make Hero subtitle optional for help articles (title-only)
- Localize help breadcrumbs, landing page metadata, category names
- Add es-es → es-419 locale fallback (was falling back to en)
- Add 12 new i18n keys for help center across all 5 locales
- Update content submodule: 11 new articles, updated RelatedPages
- Add help/TODO.md documenting future improvements

* Add aria-label to help search input, fix CTA JSDoc

Addresses CodeRabbit review feedback:
- Search input now has aria-label for screen readers
- CTA JSDoc accurately describes #chat behavior
* Deprecate peanut.to: migrate env example, sunset legacy redirects

- .env.example: update docs.peanut.to → docs.peanut.me,
  hello@peanut.to → hello@peanut.me
- redirects.json: replace legacy.peanut.to batch/raffle/packet
  redirects with GitHub archive branch link
- sitemap.ts: remove stale DONE comment

* Update preview workflow branches: peanut-wallet → main, dev

Branch rename: peanut-wallet is now main, peanut-wallet-dev is now dev.

* Fix broken README badges: remove dead prettier.yml, fix CodeQL path

Code Formatting badge pointed to nonexistent prettier.yml workflow
(formatting is checked inside tests.yml). CodeQL badge pointed to
wrong path. Also pin badges to main branch.
The /support page crashed due to React hydration errors in the
(mobile-ui) layout. Replace it with an edge-level redirect in
redirects.json so users land on the help center directly.

Also removes completed TODO for peanut.to 301 redirects and
Google Search Console setup (both done).
Redirect /support to /en/help, remove crashing page
…rd height

- Add scripts/validate-links.ts to validate all internal links in content
- Add validate-links npm script
- Fix RelatedPages card height: cards now match height when titles wrap
- Update content submodule with 1435 fixed broken links
Runs pnpm validate-links on every push to catch broken internal
links before they reach production.
scripts/generate-valid-links.ts builds input/context/valid-links.md
from the content directory structure. LLMs load this during content
generation to know exactly which internal URLs exist, preventing
broken link hallucination.
- href regex now matches both href="..." and href='...'
- Trailing slashes stripped before URL lookup to avoid false positives
- Add update-content.yml workflow triggered by repository_dispatch
- Set submodule to track main branch
- peanut-content will send a dispatch event on every push to main
- Remove lowercase comments guideline from .cursorrules
- Guard output directory in generate-valid-links.ts (mkdirSync)
- Add permissions block to update-content workflow
Fix broken internal links + validation script
@vercel
Copy link

vercel bot commented Feb 27, 2026

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

Project Deployment Actions Updated (UTC)
peanut-wallet Building Building Preview, Comment Feb 27, 2026 2:10pm

Request Review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 27, 2026

Walkthrough

Major refactor migrating domain from peanut.to to peanut.me, introducing content submodule, comprehensive i18n localization, design system documentation, marketing/MDX page infrastructure, SEO data loaders, and landing page component reorganization.

Changes

Cohort / File(s) Summary
Domain Migration
.cursorrules, .env.example, src/proxy.ts, public/did.json, src/components/Card/CardInfoScreen.tsx, src/utils/__mocks__/wagmi-config.ts, scripts/README-qr-generator.md
Updated domain references from peanut.to to peanut.me across environment, component, and documentation files.
Content Submodule & .gitmodules
.gitmodules, src/content, redirects.json
Added src/content as a Git submodule pointing to peanut-content repository on main branch; updated redirects to map /docs and related paths to /en/help.
GitHub Workflows & Deployment
.github/workflows/preview.yaml, .github/workflows/tests.yml, .github/workflows/update-content.yml
Enhanced workflows with SUBMODULE_TOKEN support, added content validation step (validate-links), and new automated content submodule update workflow with Git API integration.
Internationalization Infrastructure
src/i18n/types.ts, src/i18n/config.ts, src/i18n/index.ts, src/i18n/en.json, src/i18n/es-419.json, src/i18n/es-ar.json, src/i18n/es-es.json, src/i18n/pt-br.json
Complete i18n system with locale types, routing helpers (localizedPath, getAlternates), and translation files for 5 locales (en, es-419, es-ar, es-es, pt-br).
SEO & Content Data Infrastructure
src/data/seo/index.ts, src/data/seo/corridors.ts, src/data/seo/comparisons.ts, src/data/seo/exchanges.ts, src/data/seo/payment-methods.ts, src/data/seo/utils.ts, src/lib/content.ts, src/lib/blog.ts, src/lib/mdx.ts, src/lib/seo/schemas.tsx
Comprehensive content and SEO loaders: locale-aware country/corridor data, competitor comparisons, exchange deposits, payment methods; markdown parsers with frontmatter extraction, MDX compilation, and JSON-LD schema builders.
Design System Documentation (ds)
src/app/(mobile-ui)/dev/ds/_components/*, src/app/(mobile-ui)/dev/ds/_hooks/*, src/app/(mobile-ui)/dev/ds/foundations/*, src/app/(mobile-ui)/dev/ds/patterns/*, src/app/(mobile-ui)/dev/ds/primitives/*, src/app/(mobile-ui)/dev/ds/layout.tsx, src/app/(mobile-ui)/dev/ds/page.tsx
Extensive design system documentation pages with 50+ new files covering foundations (colors, typography, spacing, icons, borders, shadows), primitives (button, card, input, select, checkbox, toast, divider, title, page-container), patterns (modal, drawer, navigation, loading, feedback, layouts, copy-share, cards-global, amount-input), and a playground system with interactive examples.
Marketing & MDX Components
src/components/Marketing/*, src/components/Marketing/mdx/*, src/components/LandingPage/*
New marketing component library (MarketingHero, MarketingShell, MarketingNav, BlogCard, ContentPage, HelpLanding, FAQSection, DestinationGrid, etc.) and MDX-embeddable components (Hero, Steps, FAQ, CTA, Callout, ExchangeWidget, Tabs, Stars, CountryGrid, RelatedPages).
Landing Page Redesign
src/components/LandingPage/LandingPageClient.tsx, src/components/LandingPage/LandingPageShell.tsx, src/components/LandingPage/landingPageData.ts, src/components/LandingPage/SendInSecondsCTA.tsx, src/components/LandingPage/Manteca.tsx, src/components/LandingPage/RegulatedRails.tsx, src/components/LandingPage/sendInSeconds.tsx, src/components/LandingPage/hero.tsx, src/components/LandingPage/CloudsCss.tsx, src/components/LandingPage/imageAssets.tsx, src/components/LandingPage/marquee.tsx, src/app/lp/page.tsx, src/app/lp/layout.tsx, src/app/page.tsx
Restructured landing page with modular client/shell composition, new CloudsCss and AnimateOnView animations replacing framer-motion, data-driven configuration, and simplified responsive design without runtime width tracking.
Global UI Components
src/components/Global/AnimateOnView.tsx, src/components/Global/FooterVisibilityObserver.tsx, src/components/Global/InvitesGraph/index.tsx, src/components/Global/QRScanner/useQRScanner.ts
Added animation on view component, footer visibility observer, and QR scanner improvements (reduced scan frequency, adjusted region, continuous autofocus, inversion mode changes).
Component Refactoring & Named Exports
src/components/0_Bruddle/Button.tsx, src/components/0_Bruddle/BaseInput.tsx, src/components/0_Bruddle/BaseSelect.tsx, src/components/0_Bruddle/Checkbox.tsx, src/components/0_Bruddle/CloudsBackground.tsx, src/components/0_Bruddle/Divider.tsx, src/components/0_Bruddle/PageContainer.tsx, src/components/0_Bruddle/Title.tsx, src/components/0_Bruddle/Card.tsx
Replaced long-press implementation with useLongPress hook, removed green/yellow button variants, added named exports and displayName assignments across multiple primitives.
Marketing Pages (Dynamic Routes)
src/app/[locale]/(marketing)/[country]/page.tsx, src/app/[locale]/(marketing)/blog/[slug]/page.tsx, src/app/[locale]/(marketing)/blog/category/[cat]/page.tsx, src/app/[locale]/(marketing)/blog/page.tsx, src/app/[locale]/(marketing)/compare/[slug]/page.tsx, src/app/[locale]/(marketing)/deposit/[exchange]/page.tsx, src/app/[locale]/(marketing)/help/[slug]/page.tsx, src/app/[locale]/(marketing)/help/page.tsx, src/app/[locale]/(marketing)/pay-with/[method]/page.tsx, src/app/[locale]/(marketing)/receive-money-from/[country]/page.tsx, src/app/[locale]/(marketing)/send-money-from/[from]/to/[to]/page.tsx, src/app/[locale]/(marketing)/send-money-to/[country]/page.tsx, src/app/[locale]/(marketing)/team/page.tsx, src/app/[locale]/(marketing)/layout.tsx, src/app/[locale]/(marketing)/error.tsx
Comprehensive set of locale-aware marketing pages with static param generation, dynamic metadata, MDX/legacy fallback rendering, breadcrumbs, and JSON-LD schema integration.
Points & Rewards System Updates
src/app/(mobile-ui)/points/page.tsx, src/app/(mobile-ui)/points/invites/page.tsx, src/components/Points/CashCard.tsx, src/components/Points/InviteePointsBadge.tsx, src/utils/format.utils.ts, src/constants/points.consts.ts, src/services/points.ts
Refactored points display with animated counters (useCountUp), changed cashbackAllowance to hasCashbackLeft boolean, added formatPoints/shortenPoints utilities, removed TRANSITIVITY_MULTIPLIER constant.
Hooks & Utilities
src/hooks/useCountUp.ts, src/hooks/useLongPress.ts, src/utils/cookie-migration.utils.ts, src/utils/format.utils.ts, src/constants/countryCurrencyMapping.ts, src/constants/routes.ts, src/data/team.ts
Added useCountUp for animated counters with localStorage persistence, useLongPress for long-press interactions, expanded route constants, new country mapping and team data helpers.
API & Service Updates
src/app/api/peanut/user/get-jwt-token/route.ts, src/app/api/peanut/user/login-user/route.ts, src/app/api/peanut/user/register-user/route.ts, src/app/api/peanut/user/get-user-from-cookie/route.ts, src/app/api/peanut/user/get-user-from-cookie/__tests__/route.test.ts, src/app/actions/card.ts
Added JWT cookie maxAge (30 days) across login/register/refresh endpoints, best-effort cookie refresh on user fetch, stricter API key validation with runtime error on missing env var, comprehensive route tests.
Service Worker & Site Configuration
src/app/sw.ts, src/app/robots.ts, src/app/sitemap.ts, src/app/layout.tsx, src/app/metadata.ts, public/llms.txt, public/llms-full.txt
Updated service worker to skip caching for /api/peanut/user routes, expanded robots.ts with AI crawler rules and rate limits, rebuilt sitemap with locale-aware SEO routes, added JSON-LD schemas (Organization, WebApplication, WebSite), added llms.txt product descriptions.
Mobile App Pages
src/app/(mobile-ui)/home/page.tsx, src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx, src/app/(mobile-ui)/withdraw/crypto/page.tsx, src/app/(mobile-ui)/qr-pay/page.tsx, src/app/(mobile-ui)/support/layout.tsx, src/app/(mobile-ui)/support/page.tsx, src/app/(mobile-ui)/dev/page.tsx, src/app/(mobile-ui)/dev/components/page.tsx
Removed support page, updated home card pioneer modal gating, improved withdraw flow navigation and success handling, refined QR pay percentage calculations, simplified dev tools page.
Content Validation & Scripts
scripts/validate-content.ts, scripts/validate-links.ts, scripts/generate-valid-links.ts, scripts/ping-sitemap.sh
Added comprehensive content validation (frontmatter, slugs, en.md presence, locale consistency, entity cross-references), internal link validation across markdown/JSX, valid links generation for content submodule, and sitemap ping script for search engines.
Configuration & Documentation
.cursorrules, .env.example, .gitignore, package.json, next.config.js, README.md, src/app/(mobile-ui)/help/TODO.md
Removed comment rule from .cursorrules, updated environment variables and .gitignore, added validation and link-checking npm scripts, made bundle analyzer conditional on ANALYZE env var, added archived branches documentation and future help center improvements notes.
UI Refinements
src/components/Card/CardGeoScreen.tsx, src/components/Common/PointsCard.tsx, src/components/TransactionDetails/TransactionDetailsReceipt.tsx, src/components/Setup/Views/SignTestTransaction.tsx, src/components/LandingPage/CurrencySelect.tsx
Fixed typo ("Purchare" → "Purchase"), added formatPoints to points display, improved loading state logic, increased z-index for popover panel.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive No pull request description was provided, making it impossible to assess whether the description relates to the changeset. Add a description explaining the scope of changes, key modifications, and any important notes for reviewers.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'chore: sync staging with prod' accurately describes the main purpose: syncing staging with production branch, which is the primary intent of this changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore/sync-131

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 20

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/app/metadata.ts (1)

28-41: ⚠️ Potential issue | 🟠 Major

Resolve canonical URLs with URL instead of string concatenation.

Line 28 concatenates BASE_URL + canonical, which can generate invalid URLs when canonical is absolute (or not slash-prefixed). Resolve once via new URL(canonical, BASE_URL) and reuse for both Open Graph and alternates.

Proposed fix
 export function generateMetadata({
@@
     canonical?: string
 }): Metadata {
+    const resolvedCanonical = canonical ? new URL(canonical, BASE_URL).toString() : BASE_URL
+
     return {
@@
         openGraph: {
@@
-            url: canonical ? `${BASE_URL}${canonical}` : BASE_URL,
+            url: resolvedCanonical,
@@
-        ...(canonical ? { alternates: { canonical } } : {}),
+        ...(canonical ? { alternates: { canonical: resolvedCanonical } } : {}),
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/metadata.ts` around lines 28 - 41, Replace string concatenation of
BASE_URL + canonical with resolving via the URL constructor: when canonical is
truthy call new URL(canonical, BASE_URL) once (e.g., resolved = new
URL(canonical, BASE_URL)) and use resolved.href for the Open Graph url and for
alternates.canonical; if canonical is falsy keep url as BASE_URL and omit
alternates. Update references in the metadata object (the url field and the
alternates: { canonical }) to use the resolved URL variable.
src/components/0_Bruddle/Button.tsx (1)

119-132: ⚠️ Potential issue | 🟠 Major

Long-press gating blocks keyboard activation.

At Line 121, clicks are dropped unless isLongPressed is true. Keyboard clicks (Enter/Space) never set that pointer/touch long-press state, so this can make the action unreachable for keyboard users.

Suggested fix
 const handleClick = useCallback(
     (e: React.MouseEvent<HTMLButtonElement>) => {
-        if (longPress && !isLongPressed) {
+        const isKeyboardClick = e.detail === 0
+        if (longPress && !isKeyboardClick && !isLongPressed) {
             return
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/0_Bruddle/Button.tsx` around lines 119 - 132, The handleClick
callback currently drops all clicks when longPress is enabled and isLongPressed
is false, which blocks keyboard activation; update handleClick so the long-press
gate only blocks pointer-initiated clicks, not keyboard-initiated activations
(detect keyboard activation by checking e.nativeEvent.detail === 0 or
equivalent), i.e. only return early when longPress && !isLongPressed && the
event is a pointer click (detail !== 0); keep haptics and onClick invocation
unchanged so keyboard users can activate the button.
🟡 Minor comments (28)
src/app/(mobile-ui)/qr-pay/page.tsx-1277-1283 (1)

1277-1283: ⚠️ Potential issue | 🟡 Minor

Preserve valid 0 values in percentage fallback logic

Line 1281 and Line 1283 use truthy checks (amountSponsored && ..., ... || 100), so a valid 0 can be misread as “missing” and incorrectly show 100% messaging.

Suggested fix
-                                        const amountSponsored = qrPayment?.perk?.amountSponsored
-                                        const transactionUsd =
-                                            parseFloat(qrPayment?.details?.paymentAgainstAmount || '0') || 0
-                                        const percentage =
-                                            amountSponsored && transactionUsd > 0
-                                                ? Math.round((amountSponsored / transactionUsd) * 100)
-                                                : qrPayment?.perk?.discountPercentage || 100
+                                        const amountSponsored = qrPayment?.perk?.amountSponsored
+                                        const transactionUsd = Number(qrPayment?.details?.paymentAgainstAmount ?? 0)
+                                        const hasValidSponsoredAmount =
+                                            typeof amountSponsored === 'number' && Number.isFinite(amountSponsored)
+                                        const percentage =
+                                            hasValidSponsoredAmount && transactionUsd > 0
+                                                ? Math.round((amountSponsored / transactionUsd) * 100)
+                                                : (qrPayment?.perk?.discountPercentage ?? 100)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(mobile-ui)/qr-pay/page.tsx around lines 1277 - 1283, The percentage
calculation incorrectly treats valid 0 values as "missing" by using truthy
checks; update the logic in the block that defines amountSponsored,
transactionUsd and percentage so that amountSponsored is checked for
null/undefined (e.g., amountSponsored != null) rather than truthiness, compute
percentage when transactionUsd > 0 and amountSponsored != null, and use the
nullish coalescing operator for the fallback
(qrPayment?.perk?.discountPercentage ?? 100) so a legitimate 0 discount is
preserved.
src/app/[locale]/(marketing)/pay-with/[method]/page.tsx-63-64 (1)

63-64: ⚠️ Potential issue | 🟡 Minor

Use a locale-aware home breadcrumb link.

Line 63 uses href: '/', which can drop users out of their current locale context from a locale-scoped route. Use /${locale} for the home breadcrumb.

🔧 Suggested fix
         <ContentPage
             breadcrumbs={[
-                { name: i18n.home, href: '/' },
+                { name: i18n.home, href: `/${locale}` },
                 { name: pm.name, href: `/${locale}/pay-with/${method}` },
             ]}
         >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/(marketing)/pay-with/[method]/page.tsx around lines 63 -
64, The breadcrumb for the home link uses a non-locale-aware href ('/'); update
the breadcrumb object where it's built (the entry with name: i18n.home) to use
the current locale by setting href to `/${locale}` instead of `'/'` so users
remain in their locale context (refer to the breadcrumb array in page.tsx where
i18n.home and pm.name are defined).
src/app/[locale]/(marketing)/team/page.tsx-85-87 (1)

85-87: ⚠️ Potential issue | 🟡 Minor

Use design system text colors.

Per coding guidelines, use text-grey-1 for secondary text instead of Tailwind's text-gray-500 / text-gray-700.

🎨 Proposed fix
                             <div>
                                 <h2 className="text-lg font-bold">{member.name}</h2>
-                                <p className="text-sm font-medium text-gray-500">{member.role}</p>
+                                <p className="text-sm font-medium text-grey-1">{member.role}</p>
                             </div>
-                            <p className="text-sm text-gray-700">{member.bio}</p>
+                            <p className="text-sm text-grey-1">{member.bio}</p>

As per coding guidelines: "Text hierarchy: use text-n-1 for primary text, text-grey-1 for secondary text".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/(marketing)/team/page.tsx around lines 85 - 87, Replace
Tailwind color classes for secondary text with the design system token: locate
the JSX in page.tsx that renders member.role and member.bio and change the
className values using text-gray-500 and text-gray-700 to use text-grey-1
instead (i.e., update the element rendering member.role and the paragraph
rendering member.bio to use text-grey-1 for secondary text).
src/app/(mobile-ui)/dev/ds/_hooks/useHighlightedCode.ts-10-14 (1)

10-14: ⚠️ Potential issue | 🟡 Minor

Highlighter only loads 'tsx' language but hook accepts arbitrary lang parameter.

The highlighter is initialized with only langs: ['tsx'], but the hook accepts any lang string. If a caller passes a different language (e.g., 'css', 'json'), highlighting will fail or fall back to unhighlighted output.

Either expand the languages array to include commonly used languages, or constrain the lang parameter to only accept 'tsx'.

Option 1: Add common languages
         highlighterPromise = import('shiki/bundle/web').then((shiki) =>
             shiki.createHighlighter({
                 themes: ['github-light'],
-                langs: ['tsx'],
+                langs: ['tsx', 'typescript', 'javascript', 'css', 'json', 'shell'],
             })
         )
Option 2: Constrain the type
-export function useHighlightedCode(code: string, lang = 'tsx') {
+export function useHighlightedCode(code: string, lang: 'tsx' = 'tsx') {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(mobile-ui)/dev/ds/_hooks/useHighlightedCode.ts around lines 10 -
14, The highlighterPromise is created with only langs: ['tsx'] but
useHighlightedCode's lang parameter accepts arbitrary strings (so non-tsx
languages won't be highlighted); update the highlighter initialization
(highlighterPromise / shiki.createHighlighter) to include a broader set of
languages you need (e.g., 'tsx','ts','js','jsx','css','json','html', etc.) or
alternatively tighten the hook's type signature so lang only allows 'tsx' (or a
union of supported languages) and validate callers; edit the highlighterPromise
languages list or the useHighlightedCode hook's lang parameter type accordingly
and ensure any call sites match the new constraint.
src/app/(mobile-ui)/dev/ds/_components/PropsTable.tsx-38-40 (1)

38-40: ⚠️ Potential issue | 🟡 Minor

Keep table column structure consistent across rows.

Rows without description currently render fewer cells than the header declares. Render a placeholder cell to preserve table semantics.

🔧 Proposed fix
-                            {row.description && (
-                                <td className="hidden px-4 py-2.5 text-grey-1 sm:table-cell">{row.description}</td>
-                            )}
+                            <td className="hidden px-4 py-2.5 text-grey-1 sm:table-cell">
+                                {row.description ?? '—'}
+                            </td>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(mobile-ui)/dev/ds/_components/PropsTable.tsx around lines 38 - 40,
The table rows in PropsTable render fewer cells when row.description is falsy,
breaking column alignment; update the row rendering logic in the PropsTable
component so that instead of conditionally omitting the description cell you
always render a <td> for description (use the same className "hidden px-4 py-2.5
text-grey-1 sm:table-cell") and output a placeholder (empty string or
non-breaking space) when row.description is missing, ensuring each <tr> has the
same number of <td> elements as the header.
src/app/(mobile-ui)/dev/ds/_components/TierNav.tsx-14-14 (1)

14-14: ⚠️ Potential issue | 🟡 Minor

Avoid partial-prefix matches for active tier detection.

startsWith alone can incorrectly activate a tab for similarly prefixed paths. Match exact route or a slash-delimited child route.

🔧 Proposed fix
-                const isActive = pathname?.startsWith(tier.href)
+                const isActive = pathname === tier.href || pathname?.startsWith(`${tier.href}/`)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(mobile-ui)/dev/ds/_components/TierNav.tsx at line 14, The
active-tab check in TierNav.tsx uses isActive = pathname?.startsWith(tier.href),
which incorrectly matches partial prefixes; change the logic to consider either
an exact match (pathname === tier.href) or a proper child route by checking
startsWith(tier.href + '/') so that only exact routes or slash-delimited
children mark the tier active (use the existing pathname and tier.href
identifiers and update isActive accordingly).
src/components/Marketing/HelpLanding.tsx-121-121 (1)

121-121: ⚠️ Potential issue | 🟡 Minor

Localize the empty-state message through strings.

This hardcoded English copy bypasses the component’s locale-aware string props.

🔧 Proposed fix
 interface HelpLandingStrings {
     searchPlaceholder: string
     cantFind: string
     cantFindDesc: string
+    noResults?: string
 }
 ...
-                        <p className="text-base">No articles match your search.</p>
+                        <p className="text-base">{strings?.noResults ?? 'No articles match your search.'}</p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Marketing/HelpLanding.tsx` at line 121, Replace the hardcoded
English empty-state text in HelpLanding (the <p> element) with the component’s
locale-aware strings prop (e.g., use strings.noArticles or
strings.emptySearchResult) so the message is localized; if that key does not
exist add it to the strings interface/props and to the default/localization
files and update any types/tests that reference HelpLanding to use the new key.
src/app/(mobile-ui)/dev/ds/patterns/amount-input/page.tsx-39-39 (1)

39-39: ⚠️ Potential issue | 🟡 Minor

Orphaned absolute positioning - parent lacks relative.

The absolute positioned element won't behave as intended because its parent (<div className="rounded-sm border border-n-1 p-4"> at line 30) doesn't have relative positioning. Either add relative to the parent or remove this empty div if it's not needed.

Proposed fix
-                        <div className="absolute right-4 top-1/2 -translate-y-1/2 text-grey-1"></div>

Or if the swap icon placeholder is intended:

-                    <div className="rounded-sm border border-n-1 p-4">
+                    <div className="relative rounded-sm border border-n-1 p-4">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(mobile-ui)/dev/ds/patterns/amount-input/page.tsx at line 39, The
empty absolutely-positioned div (<div className="absolute right-4 top-1/2
-translate-y-1/2 text-grey-1"></div>) is orphaned because its containing element
(the <div className="rounded-sm border border-n-1 p-4">) lacks relative
positioning; either add "relative" to that parent div to anchor the absolute
element, or remove the empty absolute div entirely if it was a leftover
placeholder for the swap icon (ensure to update any intended icon/placeholder
insertion point such as the absolute div reference).
src/app/(mobile-ui)/dev/ds/foundations/icons/page.tsx-10-86 (1)

10-86: ⚠️ Potential issue | 🟡 Minor

Add missing credit-card icon to the ALL_ICONS array.

The ALL_ICONS array is manually maintained and has drifted from the source: credit-card exists in the IconName type definition but is missing from this list. Update line 10-86 to include 'credit-card' to keep the two synchronized.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(mobile-ui)/dev/ds/foundations/icons/page.tsx around lines 10 - 86,
The ALL_ICONS array is missing the 'credit-card' entry present in the IconName
type; update the ALL_ICONS constant to include the string 'credit-card' (e.g.,
insert 'credit-card' into the ALL_ICONS array alongside the other icon names) so
the ALL_ICONS list and IconName type stay synchronized; ensure you modify the
ALL_ICONS declaration in page.tsx.
src/components/Marketing/mdx/ProseStars.tsx-68-73 (1)

68-73: ⚠️ Potential issue | 🟡 Minor

Module-level counter mutated during render can cause non-deterministic behavior.

Mutating counter during the render phase is a side effect that violates React's render purity expectations. In React 18+ with concurrent rendering or StrictMode (which double-invokes renders), this counter will increment unpredictably, causing inconsistent star placements.

Consider using a ref or moving the counter logic to a context/hook if deterministic cycling is needed, or accept that placement is arbitrary per render.

🔧 Alternative: Use useRef to track counter per mount

If you need stable cycling across multiple ProseStars instances within a single page render, consider a context or a more controlled approach. For simple decoration where exact placement doesn't matter, this may be acceptable as-is.

+import { useRef } from 'react'
+
+// Move counter into a ref or context if deterministic behavior is needed
-let counter = 0
+const counterRef = { current: 0 }

 export function ProseStars() {
-    const set = placements[counter % placements.length]
-    counter++
+    const set = placements[counterRef.current % placements.length]
+    counterRef.current++
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Marketing/mdx/ProseStars.tsx` around lines 68 - 73, The
module-level variable counter is being mutated during render inside ProseStars
which causes nondeterministic placements; change this to a per-instance stable
value by using a ref set on mount (e.g., create a module-level sequence number
and in ProseStars use a useRef<number>() that, inside useLayoutEffect or
useEffect, assigns itself the next sequence value from that module-level
counter), then read placements[instanceRef.current % placements.length] during
render instead of mutating counter directly; reference the ProseStars function,
the module-level counter variable, and placements when making this change.
src/lib/blog.ts-69-71 (1)

69-71: ⚠️ Potential issue | 🟡 Minor

Update type annotation to match marked 17.0.2 Tokens.Code signature.

The destructured parameter works with marked 17.0.2, but the type annotation is incomplete. Tokens.Code includes text, lang, and escaped (plus standard token fields). Update to:

renderer.code = ({ text, lang, escaped }: { text: string; lang?: string; escaped?: boolean }) => {

Or use the full Tokens.Code type from marked for proper type safety.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/blog.ts` around lines 69 - 71, The type annotation for the marked
renderer code handler is incomplete; update the signature for renderer.code to
include the escaped field (or use marked's Tokens.Code type) so it matches
marked 17.0.2. Specifically, change the parameter typing for renderer.code (the
function assigned to renderer.code) from ({ text, lang }: { text: string; lang?:
string }) to include escaped (either ({ text, lang, escaped }: { text: string;
lang?: string; escaped?: boolean }) or by typing the whole param as Tokens.Code)
and adjust any usage accordingly.
src/components/0_Bruddle/Button.tsx-171-177 (1)

171-177: ⚠️ Potential issue | 🟡 Minor

Long-press handlers can be silently overridden by forwarded props.

At Line 177, {...props} is spread after onMouseDown/onTouchStart/..., so any consumer handler with the same prop name overrides the long-press wiring and disables the feature.

Suggested fix
+        const {
+            onMouseDown: onMouseDownProp,
+            onMouseUp: onMouseUpProp,
+            onMouseLeave: onMouseLeaveProp,
+            onTouchStart: onTouchStartProp,
+            onTouchEnd: onTouchEndProp,
+            onTouchCancel: onTouchCancelProp,
+            ...restProps
+        } = props
+
         return (
             <button
                 className={twMerge(buttonClasses, 'notranslate', longPress && 'relative overflow-hidden')}
                 ref={buttonRef}
                 translate="no"
                 onClick={handleClick}
-                onMouseDown={longPress ? longPressHandlers.onMouseDown : undefined}
-                onMouseUp={longPress ? longPressHandlers.onMouseUp : undefined}
-                onMouseLeave={longPress ? longPressHandlers.onMouseLeave : undefined}
-                onTouchStart={longPress ? longPressHandlers.onTouchStart : undefined}
-                onTouchEnd={longPress ? longPressHandlers.onTouchEnd : undefined}
-                onTouchCancel={longPress ? longPressHandlers.onTouchCancel : undefined}
-                {...props}
+                onMouseDown={(e) => {
+                    onMouseDownProp?.(e)
+                    if (longPress) longPressHandlers.onMouseDown()
+                }}
+                onMouseUp={(e) => {
+                    onMouseUpProp?.(e)
+                    if (longPress) longPressHandlers.onMouseUp()
+                }}
+                onMouseLeave={(e) => {
+                    onMouseLeaveProp?.(e)
+                    if (longPress) longPressHandlers.onMouseLeave()
+                }}
+                onTouchStart={(e) => {
+                    onTouchStartProp?.(e)
+                    if (longPress) longPressHandlers.onTouchStart()
+                }}
+                onTouchEnd={(e) => {
+                    onTouchEndProp?.(e)
+                    if (longPress) longPressHandlers.onTouchEnd()
+                }}
+                onTouchCancel={(e) => {
+                    onTouchCancelProp?.(e)
+                    if (longPress) longPressHandlers.onTouchCancel()
+                }}
+                {...restProps}
             >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/0_Bruddle/Button.tsx` around lines 171 - 177, The long-press
event handlers (longPressHandlers and the longPress prop usage) are being
overridden because {...props} is spread after the explicit
onMouseDown/onMouseUp/onTouchStart/onTouchEnd/onMouseLeave/onTouchCancel
attributes; move {...props} before those handler attributes or merge incoming
handlers with longPressHandlers (e.g. call props.onMouseDown from inside
longPressHandlers.onMouseDown) so consumer props don't silently override the
long-press wiring; update Button.tsx to either spread props first or
wrap/compose
props.{onMouseDown,onMouseUp,onMouseLeave,onTouchStart,onTouchEnd,onTouchCancel}
with longPressHandlers to preserve both behaviors.
src/components/LandingPage/landingPageData.ts-12-12 (1)

12-12: ⚠️ Potential issue | 🟡 Minor

Fix user-facing FAQ copy typos/grammar before release.

There are visible copy issues (e.g., Faqs casing and the malformed sentence in the security answer at Line 32) that reduce clarity and trust.

Also applies to: 32-32

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/LandingPage/landingPageData.ts` at line 12, The landing page
FAQ copy has typos: change the heading value 'Faqs' to correct casing 'FAQs' and
fix the malformed sentence in the security FAQ entry (look for the FAQs
array/object entry that contains the security answer—e.g., the item with key or
variable referencing the security question or the property name 'security' or
the answer string at line 32) so the sentence is grammatically correct and
clear; update only the text values in landingPageData (heading and the security
answer string) ensuring proper punctuation and capitalization.
src/app/(mobile-ui)/dev/ds/layout.tsx-9-13 (1)

9-13: ⚠️ Potential issue | 🟡 Minor

Align the DS layout root with the required app-page scaffold.

The root shell is missing min-h-[inherit] and gap-8, and NavHeader is wrapped instead of being the first child in the page shell.

As per coding guidelines Page layout outer shell: use flex min-h-[inherit] flex-col gap-8 with NavHeader as first child.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(mobile-ui)/dev/ds/layout.tsx around lines 9 - 13, Update the page
shell in layout.tsx so the root container uses the required scaffold classes and
NavHeader is the first child: change the outer <div className="flex w-full
flex-col"> to include "min-h-[inherit] gap-8" (e.g., "flex min-h-[inherit]
flex-col gap-8 ...") and move the NavHeader component (NavHeader title="Design
System" href="/dev") to be the very first child inside that root shell before
other content.
src/components/Marketing/ContentPage.tsx-43-46 (1)

43-46: ⚠️ Potential issue | 🟡 Minor

Use the standard link text style for breadcrumb links.

Please align breadcrumb links to the repo standard (text-black underline) for consistency.

As per coding guidelines Links: use text-black underline - never use text-purple-1 (pink).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Marketing/ContentPage.tsx` around lines 43 - 46, Update the
breadcrumb Link elements in the Marketing ContentPage component to use the
repo-standard link style instead of the custom decoration classes: replace the
current className on the breadcrumb <Link> instances (the elements rendering
crumb.href inside ContentPage) with the canonical "text-black underline" so
breadcrumb links use text-black underline consistently across the app.
src/components/Marketing/mdx/Tabs.tsx-41-45 (1)

41-45: ⚠️ Potential issue | 🟡 Minor

Guard against empty or malformed labels to avoid invalid default tab state.

If labels is empty (or only commas/spaces), tabs[0] becomes undefined and the tab root gets an invalid defaultValue.

🛡️ Suggested fix
 export function Tabs({ labels, children }: TabsProps) {
-    const tabs = labels.split(',').map((l) => l.trim())
+    const tabs = labels
+        .split(',')
+        .map((l) => l.trim())
+        .filter(Boolean)
+
+    if (tabs.length === 0) return null
+
     return (
         <div className={`mx-auto my-8 ${PROSE_WIDTH} px-6 md:px-4`}>
             <RadixTabs.Root defaultValue={tabs[0]} className="w-full">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Marketing/mdx/Tabs.tsx` around lines 41 - 45, The code splits
labels into tabs and uses tabs[0] as RadixTabs.Root's defaultValue which can be
undefined for empty or malformed labels; fix by sanitizing the labels before
use: trim and filter out empty strings after split (operate on labels in Tabs
component where labels is parsed), compute a fallback like 'tab-0' or the first
non-empty label, and pass that safe value to RadixTabs.Root's defaultValue (and
ensure any mapping for RadixTabs.Trigger/Content uses the same sanitized tabs
array).
src/components/Marketing/mdx/FAQ.tsx-41-51 (1)

41-51: ⚠️ Potential issue | 🟡 Minor

Potential issue with MDX child type comparison.

The check child.type === FAQItem may not work reliably with MDX compilation, as the component identity can change during the MDX transformation. The fallback child.props?.question check provides resilience, but consider relying primarily on the props-based check.

🛡️ More robust child detection
     Children.forEach(children, (child) => {
         if (!isValidElement(child)) return
-        if (child.type === FAQItem || child.props?.question) {
+        // Props-based check is more reliable with MDX compilation
+        if (child.props?.question) {
             const id = `faq-${questions.length}`
             questions.push({
                 id,
                 question: child.props.question,
                 answer: extractText(child.props.children),
             })
         }
     })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Marketing/mdx/FAQ.tsx` around lines 41 - 51, The child-type
equality check using FAQItem is brittle with MDX; update the Children.forEach
block to detect FAQ entries primarily via props (e.g., child.props?.question)
instead of strict component identity (child.type === FAQItem), so build the
questions array using the props-based detection and still guard with
isValidElement; ensure you keep extracting id, question and answer (using
extractText(child.props.children)) and pushing into questions exactly as before
but triggered by the props presence rather than the FAQItem type.
src/app/(mobile-ui)/dev/ds/foundations/colors/page.tsx-59-75 (1)

59-75: ⚠️ Potential issue | 🟡 Minor

Hover copy icon won’t appear (missing group class on the button).

Line 74 uses group-hover, but Line 59 does not define group, so the copy icon stays hidden.

💡 Proposed fix
- className="flex items-center gap-2 rounded-sm border border-n-1/20 p-2 text-left transition-colors hover:border-n-1/40"
+ className="group flex items-center gap-2 rounded-sm border border-n-1/20 p-2 text-left transition-colors hover:border-n-1/40"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(mobile-ui)/dev/ds/foundations/colors/page.tsx around lines 59 - 75,
The hover-copy icon stays hidden because the outer clickable element (the
element with className "flex items-center gap-2 rounded-sm border ...") lacks
the Tailwind "group" class used by the Icon's "group-hover:opacity-100"; update
that element's className to include "group" so the Icon (name="copy") can
respond to group-hover while preserving existing classes and behavior tied to
copiedColor and color.bg.
scripts/validate-content.ts-116-119 (1)

116-119: ⚠️ Potential issue | 🟡 Minor

Path construction may have redundant 'content' segment.

Line 117 joins ROOT (src/content) with 'content' and config.contentDir, resulting in paths like src/content/content/countries. If ROOT already points to the content directory, the extra 'content' segment may be incorrect.

🔧 Potential fix if ROOT is already the content root
 function validateContentType(config: ContentTypeConfig): TypeCounts {
-    const contentPath = path.join(ROOT, 'content', config.contentDir)
+    const contentPath = path.join(ROOT, config.contentDir)
     const slugs = listDirs(contentPath)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/validate-content.ts` around lines 116 - 119, The path construction in
validateContentType is adding an extra 'content' segment (path.join(ROOT,
'content', config.contentDir)) causing paths like src/content/content/...;
update validateContentType to build contentPath from ROOT and config.contentDir
only (or conditionally include 'content' if ROOT is not already the content
root) so that listDirs receives the correct directory; adjust the contentPath
calculation in the validateContentType function accordingly and ensure no other
callers rely on the double-'content' path.
src/data/seo/payment-methods.ts-66-76 (1)

66-76: ⚠️ Potential issue | 🟡 Minor

Description contains full body content instead of a summary.

Line 70 sets description: content.body which includes the entire MDX content. This could result in very large description values. Consider using content.frontmatter.description or extracting a summary instead.

🔧 Suggested fix
         result[slug] = {
             slug,
             name: fm.name,
             countries: fm.countries ?? [],
-            description: content.body,
+            description: content.frontmatter.description ?? '',
             steps: extractSteps(content.body, /Merchant QR Payments|How to Pay|Steps|How It Works/, (line) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/data/seo/payment-methods.ts` around lines 66 - 76, The description field
in the result[slug] object is currently set to the entire MDX body
(content.body); change it to use a concise summary by preferring
content.frontmatter.description if present, otherwise extract the first
paragraph or a short snippet from content.body (e.g., up to the first blank line
or a max character limit) and assign that to description; update the assignment
in the block that builds result[slug] so description uses
content.frontmatter.description || extractShortSummary(content.body) (or
equivalent logic) to avoid storing the full MDX body.
src/components/Marketing/mdx/RelatedPages.tsx-39-43 (1)

39-43: ⚠️ Potential issue | 🟡 Minor

String coercion may produce unhelpful text for complex children.

If child.props.children is a React element or array, String(...) will produce "[object Object]" or similar unhelpful text. Consider adding validation or logging a warning.

🛡️ Suggested defensive handling
             text:
                 typeof child.props.children === 'string'
                     ? child.props.children
-                    : String(child.props.children ?? ''),
+                    : typeof child.props.children === 'number'
+                      ? String(child.props.children)
+                      : '',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Marketing/mdx/RelatedPages.tsx` around lines 39 - 43, The
current text coercion in RelatedPages (where text is derived from
child.props.children) can produce unhelpful values like "[object Object]" for
React elements or arrays; replace the direct String() fallback with a small
defensive extractor: detect React elements with React.isValidElement and arrays,
recursively extract text nodes (or join array items), and return '' for non-text
content; additionally emit a dev-only warning (console.warn or the existing
logger) when non-text children are encountered so the issue is visible. Update
the logic around child.props.children in RelatedPages.tsx (the mapping that sets
text) and add a helper like getTextFromChild to centralize extraction and
warning.
src/app/[locale]/(marketing)/blog/page.tsx-49-53 (1)

49-53: ⚠️ Potential issue | 🟡 Minor

Category filters can disappear while English fallback posts are shown.

When locale posts are empty, you fall back to English posts (Line 50), but categories are still read only from the locale (Line 52). That can hide filters even though posts are rendered.

🐛 Suggested fix
-    let posts = getAllPosts(typedLocale)
-    if (posts.length === 0) posts = getAllPosts('en')
-
-    const categories = getAllCategories(typedLocale)
+    let posts = getAllPosts(typedLocale)
+    if (posts.length === 0) posts = getAllPosts('en')
+
+    const categories = Array.from(
+        new Set(posts.map((p) => p.frontmatter.category).filter(Boolean) as string[])
+    ).sort()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/(marketing)/blog/page.tsx around lines 49 - 53, When you
fall back to English posts by calling getAllPosts('en') for posts, also fetch
categories for the same fallback locale so filters match the rendered posts;
update the code around getAllPosts(typedLocale) / getAllPosts('en') and
getAllCategories(typedLocale) so that if posts are replaced with the English
fallback you call getAllCategories('en') (e.g. set categories =
getAllCategories(fallbackLocale) when posts use the fallback), keeping
typedLocale for the normal case.
src/app/[locale]/(marketing)/blog/[slug]/page.tsx-16-31 (1)

16-31: ⚠️ Potential issue | 🟡 Minor

Current prod guard can silently become a full blog-post outage once content lands.

Line 17 returns no static params in production while Line 30 keeps dynamicParams = false; once posts are added, this configuration will hard-404 all localized post routes.

If you want, I can draft a small follow-up patch that keeps today’s behavior but removes this rollout trap (e.g., conditionally toggling dynamicParams or removing the prod guard when content is present).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/(marketing)/blog/[slug]/page.tsx around lines 16 - 31, The
prod guard in generateStaticParams combined with static export const
dynamicParams = false will 404 new posts when content is added; fix by detecting
whether any blog content exists (use SUPPORTED_LOCALES and getAllPosts, falling
back to 'en') at module load and then: 1) if no content, keep the current
behavior (generateStaticParams returns [] and dynamicParams = false), 2) if
content exists, do not return [] in generateStaticParams (generate real params)
OR set export const dynamicParams = true so Next can serve new pages without a
redeploy; reference generateStaticParams, dynamicParams, SUPPORTED_LOCALES and
getAllPosts when implementing the conditional toggle.
src/app/[locale]/(marketing)/deposit/[exchange]/page.tsx-187-190 (1)

187-190: ⚠️ Potential issue | 🟡 Minor

Last updated date reflects build time, not content update time.

Using new Date().toISOString() will show the build timestamp rather than when the content was actually updated. Consider storing the last modified date in the exchange data or frontmatter for accuracy.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/(marketing)/deposit/[exchange]/page.tsx around lines 187 -
190, The current Last updated line uses new Date().toISOString() which shows
build time; instead read the canonical last-modified timestamp from the exchange
data or page frontmatter (e.g., use a field like exchange.lastUpdated or
frontmatter.updatedAt passed into the page component) and pass that value into
the t(i18n.lastUpdated, { date: ... }) call; ensure the component (page.tsx)
uses that prop (or fetches the exchange record) and formats/parses it into
YYYY-MM-DD before rendering so the displayed date reflects the actual content
update time rather than the build time.
src/app/[locale]/(marketing)/deposit/[exchange]/page.tsx-149-149 (1)

149-149: ⚠️ Potential issue | 🟡 Minor

Use project color tokens instead of Tailwind defaults.

Per coding guidelines, use text-grey-1 for secondary text instead of text-gray-500, text-gray-600, and text-gray-400. The project has its own color system.

🎨 Suggested fix
-                               <span className="text-xs text-gray-500">{item.label}</span>
+                               <span className="text-xs text-grey-1">{item.label}</span>
-                                   <p className="mt-1 text-sm text-gray-600">{item.fix}</p>
+                                   <p className="mt-1 text-sm text-grey-1">{item.fix}</p>
-               <p className="py-4 text-xs text-gray-400">
+               <p className="py-4 text-xs text-grey-1">

As per coding guidelines: "Text hierarchy: use text-n-1 for primary text, text-grey-1 for secondary text".

Also applies to: 166-166, 188-188

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/(marketing)/deposit/[exchange]/page.tsx at line 149,
Replace Tailwind default gray tokens with the project's color token
`text-grey-1` for secondary text: update the span that renders item.label (the
element containing {item.label}) and the other occurrences noted (the
spans/classes that currently use text-gray-500, text-gray-600, and
text-gray-400) to use `text-grey-1` so they follow the project's text hierarchy.
src/app/[locale]/(marketing)/help/page.tsx-78-81 (1)

78-81: ⚠️ Potential issue | 🟡 Minor

Category fallback logic may produce unexpected results.

When a category is not found in CATEGORY_I18N_KEYS, the fallback i18n['help'] is used, which would display "Help" as the category name. This could be confusing for users. Consider using a more explicit fallback like 'General' or keeping the original untranslated category name.

💡 Suggested fix
     // Translate category names
     const translatedArticles = articles.map((a) => ({
         ...a,
-        category: i18n[CATEGORY_I18N_KEYS[a.category] ?? 'help'] ?? a.category,
+        category: CATEGORY_I18N_KEYS[a.category] ? i18n[CATEGORY_I18N_KEYS[a.category]] : a.category,
     }))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/(marketing)/help/page.tsx around lines 78 - 81, The
category fallback currently uses i18n['help'] when
CATEGORY_I18N_KEYS[a.category] is missing, which yields "Help"; change the
fallback to be explicit by using the original untranslated category or a clearer
default like 'General' instead of i18n['help']. Update the translatedArticles
mapping (the articles.map callback referencing CATEGORY_I18N_KEYS and i18n) so
that when CATEGORY_I18N_KEYS[a.category] is undefined you set category to
a.category (or the literal 'General') rather than i18n['help'].
src/app/[locale]/(marketing)/deposit/[exchange]/page.tsx-4-4 (1)

4-4: ⚠️ Potential issue | 🟡 Minor

Replace barrel import with direct file import.

The import from @/data/seo is a barrel file. Change to:

import { EXCHANGES } from '@/data/seo/exchanges'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/(marketing)/deposit/[exchange]/page.tsx at line 4, The file
imports EXCHANGES via the barrel module '@/data/seo' which should be replaced
with a direct import from the specific module; update the import in page.tsx to
import EXCHANGES from '@/data/seo/exchanges' (replace the current import of
EXCHANGES from '@/data/seo' with the direct path) so the EXCHANGES symbol is
resolved from the concrete module.
src/lib/content.ts-204-209 (1)

204-209: ⚠️ Potential issue | 🟡 Minor

Remove this unused function or clarify its intent with a comment.

The function listPublishedSlugs is defined but never imported or used anywhere in the codebase—it appears to be dead code. While the concern about it only checking the English locale is technically valid (and consistent with the codebase's design where English serves as the ultimate fallback), this issue has no practical impact since the function isn't called. Either remove it or, if it's intentionally exported for external use, add a comment explaining that it uses English as the publication source of truth.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/content.ts` around lines 204 - 209, The function listPublishedSlugs
appears to be dead code; either delete the exported function or keep it with a
clarifying comment explaining its intent and why it always reads the English
locale as the source of truth (it calls listContentSlugs and
readPageContent<PublishableContent>(..., 'en') and filters via isPublished). If
you keep it, add a short comment above listPublishedSlugs stating it’s
intentionally exported for external use and that publication status is
determined from the English ('en') copy; otherwise remove the function and any
unused imports referencing listContentSlugs, readPageContent, or isPublished.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d324a1e and 907c878.

⛔ Files ignored due to path filters (3)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • src/assets/cards/DEPRECATED_Cart Gradient 5.svg is excluded by !**/*.svg
  • src/assets/chains/arbitrum.svg is excluded by !**/*.svg
📒 Files selected for processing (200)
  • .cursorrules
  • .env.example
  • .github/workflows/preview.yaml
  • .github/workflows/tests.yml
  • .github/workflows/update-content.yml
  • .gitignore
  • .gitmodules
  • README.md
  • next.config.js
  • package.json
  • public/.well-known/did.json
  • public/llms-full.txt
  • public/llms.txt
  • redirects.json
  • scripts/README-qr-generator.md
  • scripts/generate-valid-links.ts
  • scripts/ping-sitemap.sh
  • scripts/validate-content.ts
  • scripts/validate-links.ts
  • src/app/(mobile-ui)/dev/components/page.tsx
  • src/app/(mobile-ui)/dev/ds/_components/CatalogCard.tsx
  • src/app/(mobile-ui)/dev/ds/_components/CodeBlock.tsx
  • src/app/(mobile-ui)/dev/ds/_components/DesignNote.tsx
  • src/app/(mobile-ui)/dev/ds/_components/DoDont.tsx
  • src/app/(mobile-ui)/dev/ds/_components/DocHeader.tsx
  • src/app/(mobile-ui)/dev/ds/_components/DocPage.tsx
  • src/app/(mobile-ui)/dev/ds/_components/DocSection.tsx
  • src/app/(mobile-ui)/dev/ds/_components/DocSidebar.tsx
  • src/app/(mobile-ui)/dev/ds/_components/Playground.tsx
  • src/app/(mobile-ui)/dev/ds/_components/PropsTable.tsx
  • src/app/(mobile-ui)/dev/ds/_components/SectionDivider.tsx
  • src/app/(mobile-ui)/dev/ds/_components/StatusTag.tsx
  • src/app/(mobile-ui)/dev/ds/_components/TierNav.tsx
  • src/app/(mobile-ui)/dev/ds/_components/WhenToUse.tsx
  • src/app/(mobile-ui)/dev/ds/_components/nav-config.ts
  • src/app/(mobile-ui)/dev/ds/_hooks/useHighlightedCode.ts
  • src/app/(mobile-ui)/dev/ds/foundations/borders/page.tsx
  • src/app/(mobile-ui)/dev/ds/foundations/colors/page.tsx
  • src/app/(mobile-ui)/dev/ds/foundations/icons/page.tsx
  • src/app/(mobile-ui)/dev/ds/foundations/page.tsx
  • src/app/(mobile-ui)/dev/ds/foundations/shadows/page.tsx
  • src/app/(mobile-ui)/dev/ds/foundations/spacing/page.tsx
  • src/app/(mobile-ui)/dev/ds/foundations/typography/page.tsx
  • src/app/(mobile-ui)/dev/ds/layout.tsx
  • src/app/(mobile-ui)/dev/ds/page.tsx
  • src/app/(mobile-ui)/dev/ds/patterns/amount-input/page.tsx
  • src/app/(mobile-ui)/dev/ds/patterns/cards-global/page.tsx
  • src/app/(mobile-ui)/dev/ds/patterns/copy-share/page.tsx
  • src/app/(mobile-ui)/dev/ds/patterns/drawer/page.tsx
  • src/app/(mobile-ui)/dev/ds/patterns/feedback/page.tsx
  • src/app/(mobile-ui)/dev/ds/patterns/layouts/page.tsx
  • src/app/(mobile-ui)/dev/ds/patterns/loading/page.tsx
  • src/app/(mobile-ui)/dev/ds/patterns/modal/page.tsx
  • src/app/(mobile-ui)/dev/ds/patterns/navigation/page.tsx
  • src/app/(mobile-ui)/dev/ds/patterns/page.tsx
  • src/app/(mobile-ui)/dev/ds/primitives/base-input/page.tsx
  • src/app/(mobile-ui)/dev/ds/primitives/base-select/page.tsx
  • src/app/(mobile-ui)/dev/ds/primitives/button/page.tsx
  • src/app/(mobile-ui)/dev/ds/primitives/card/page.tsx
  • src/app/(mobile-ui)/dev/ds/primitives/checkbox/page.tsx
  • src/app/(mobile-ui)/dev/ds/primitives/divider/page.tsx
  • src/app/(mobile-ui)/dev/ds/primitives/page-container/page.tsx
  • src/app/(mobile-ui)/dev/ds/primitives/page.tsx
  • src/app/(mobile-ui)/dev/ds/primitives/title/page.tsx
  • src/app/(mobile-ui)/dev/ds/primitives/toast/page.tsx
  • src/app/(mobile-ui)/dev/page.tsx
  • src/app/(mobile-ui)/home/page.tsx
  • src/app/(mobile-ui)/points/invites/page.tsx
  • src/app/(mobile-ui)/points/page.tsx
  • src/app/(mobile-ui)/qr-pay/page.tsx
  • src/app/(mobile-ui)/support/layout.tsx
  • src/app/(mobile-ui)/support/page.tsx
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx
  • src/app/[locale]/(marketing)/[country]/page.tsx
  • src/app/[locale]/(marketing)/blog/[slug]/page.tsx
  • src/app/[locale]/(marketing)/blog/category/[cat]/page.tsx
  • src/app/[locale]/(marketing)/blog/page.tsx
  • src/app/[locale]/(marketing)/compare/[slug]/page.tsx
  • src/app/[locale]/(marketing)/deposit/[exchange]/page.tsx
  • src/app/[locale]/(marketing)/error.tsx
  • src/app/[locale]/(marketing)/help/TODO.md
  • src/app/[locale]/(marketing)/help/[slug]/page.tsx
  • src/app/[locale]/(marketing)/help/page.tsx
  • src/app/[locale]/(marketing)/layout.tsx
  • src/app/[locale]/(marketing)/pay-with/[method]/page.tsx
  • src/app/[locale]/(marketing)/receive-money-from/[country]/page.tsx
  • src/app/[locale]/(marketing)/send-money-from/[from]/to/[to]/page.tsx
  • src/app/[locale]/(marketing)/send-money-to/[country]/page.tsx
  • src/app/[locale]/(marketing)/team/page.tsx
  • src/app/actions/card.ts
  • src/app/api/peanut/user/get-jwt-token/route.ts
  • src/app/api/peanut/user/get-user-from-cookie/__tests__/route.test.ts
  • src/app/api/peanut/user/get-user-from-cookie/route.ts
  • src/app/api/peanut/user/login-user/route.ts
  • src/app/api/peanut/user/register-user/route.ts
  • src/app/layout.tsx
  • src/app/lp/card/CardLandingPage.tsx
  • src/app/lp/card/page.tsx
  • src/app/lp/layout.tsx
  • src/app/lp/page.tsx
  • src/app/metadata.ts
  • src/app/page.tsx
  • src/app/robots.ts
  • src/app/sitemap.ts
  • src/app/sw.ts
  • src/components/0_Bruddle/BaseInput.tsx
  • src/components/0_Bruddle/BaseSelect.tsx
  • src/components/0_Bruddle/Button.tsx
  • src/components/0_Bruddle/Card.tsx
  • src/components/0_Bruddle/Checkbox.tsx
  • src/components/0_Bruddle/CloudsBackground.tsx
  • src/components/0_Bruddle/Divider.tsx
  • src/components/0_Bruddle/PageContainer.tsx
  • src/components/0_Bruddle/Title.tsx
  • src/components/Card/CardGeoScreen.tsx
  • src/components/Card/CardInfoScreen.tsx
  • src/components/Common/PointsCard.tsx
  • src/components/Global/AnimateOnView.tsx
  • src/components/Global/FooterVisibilityObserver.tsx
  • src/components/Global/InvitesGraph/index.tsx
  • src/components/Global/QRScanner/useQRScanner.ts
  • src/components/LandingPage/CloudsCss.tsx
  • src/components/LandingPage/CurrencySelect.tsx
  • src/components/LandingPage/LandingPageClient.tsx
  • src/components/LandingPage/LandingPageShell.tsx
  • src/components/LandingPage/Manteca.tsx
  • src/components/LandingPage/RegulatedRails.tsx
  • src/components/LandingPage/SendInSecondsCTA.tsx
  • src/components/LandingPage/hero.tsx
  • src/components/LandingPage/imageAssets.tsx
  • src/components/LandingPage/landingPageData.ts
  • src/components/LandingPage/marquee.tsx
  • src/components/LandingPage/sendInSeconds.tsx
  • src/components/Marketing/BlogCard.tsx
  • src/components/Marketing/ComparisonTable.tsx
  • src/components/Marketing/ContentPage.tsx
  • src/components/Marketing/DestinationGrid.tsx
  • src/components/Marketing/FAQSection.tsx
  • src/components/Marketing/HelpLanding.tsx
  • src/components/Marketing/JsonLd.tsx
  • src/components/Marketing/MarketingErrorBoundary.tsx
  • src/components/Marketing/MarketingHero.tsx
  • src/components/Marketing/MarketingNav.tsx
  • src/components/Marketing/MarketingShell.tsx
  • src/components/Marketing/RelatedPages.tsx
  • src/components/Marketing/Section.tsx
  • src/components/Marketing/Steps.tsx
  • src/components/Marketing/index.ts
  • src/components/Marketing/mdx/CTA.tsx
  • src/components/Marketing/mdx/Callout.tsx
  • src/components/Marketing/mdx/CountryGrid.tsx
  • src/components/Marketing/mdx/ExchangeWidget.tsx
  • src/components/Marketing/mdx/FAQ.tsx
  • src/components/Marketing/mdx/Hero.tsx
  • src/components/Marketing/mdx/ProseStars.tsx
  • src/components/Marketing/mdx/RelatedPages.tsx
  • src/components/Marketing/mdx/Stars.tsx
  • src/components/Marketing/mdx/Steps.tsx
  • src/components/Marketing/mdx/Tabs.tsx
  • src/components/Marketing/mdx/components.tsx
  • src/components/Marketing/mdx/constants.ts
  • src/components/Marketing/pages/ReceiveMoneyContent.tsx
  • src/components/Points/CashCard.tsx
  • src/components/Points/InviteePointsBadge.tsx
  • src/components/Setup/Views/SignTestTransaction.tsx
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx
  • src/constants/countryCurrencyMapping.ts
  • src/constants/points.consts.ts
  • src/constants/routes.ts
  • src/content
  • src/data/seo/comparisons.ts
  • src/data/seo/corridors.ts
  • src/data/seo/exchanges.ts
  • src/data/seo/index.ts
  • src/data/seo/payment-methods.ts
  • src/data/seo/utils.ts
  • src/data/team.ts
  • src/hooks/useCountUp.ts
  • src/hooks/useHomeCarouselCTAs.tsx
  • src/hooks/useLongPress.ts
  • src/i18n/config.ts
  • src/i18n/en.json
  • src/i18n/es-419.json
  • src/i18n/es-ar.json
  • src/i18n/es-es.json
  • src/i18n/index.ts
  • src/i18n/pt-br.json
  • src/i18n/types.ts
  • src/lib/blog.ts
  • src/lib/content.ts
  • src/lib/mdx.ts
  • src/lib/seo/schemas.tsx
  • src/proxy.ts
  • src/services/points.ts
  • src/styles/globals.css
  • src/types/global.d.ts
  • src/utils/__mocks__/wagmi-config.ts
  • src/utils/cookie-migration.utils.ts
  • src/utils/format.utils.ts
💤 Files with no reviewable changes (4)
  • src/app/(mobile-ui)/support/page.tsx
  • .cursorrules
  • src/app/(mobile-ui)/support/layout.tsx
  • src/components/LandingPage/imageAssets.tsx

Comment on lines +42 to +44
BRANCH="auto/update-content-$(date -u +%Y%m%d-%H%M%S)"
TREE=$(git write-tree)
PARENT=$(git rev-parse HEAD)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE=".github/workflows/update-content.yml"

# Check if file exists
if [[ ! -f "$FILE" ]]; then
  echo "ERROR: File $FILE not found"
  exit 1
fi

# Find relevant lines
echo "=== Checking git commands order ==="
rg -n "(git add|git write-tree|git rev-parse)" "$FILE" -A 1 -B 1 || echo "No matches found"

echo ""
echo "=== Full context around lines 40-50 ==="
sed -n '40,50p' "$FILE"

Repository: peanutprotocol/peanut-ui

Length of output: 986


🏁 Script executed:

#!/bin/bash
FILE=".github/workflows/update-content.yml"

echo "=== Checking for git add commands in entire file ==="
rg -n "git add" "$FILE" || echo "No 'git add' found"

echo ""
echo "=== Checking for submodule-related commands ==="
rg -n "submodule|git fetch" "$FILE" || echo "No submodule commands found"

echo ""
echo "=== Full run block context (lines 38-60) ==="
sed -n '38,60p' "$FILE"

Repository: peanutprotocol/peanut-ui

Length of output: 1995


🏁 Script executed:

#!/bin/bash
FILE=".github/workflows/update-content.yml"

echo "=== Full workflow structure (all step names) ==="
rg -n "name:" "$FILE" | head -20

echo ""
echo "=== Check for any index-resetting commands between lines 18-60 ==="
sed -n '18,60p' "$FILE" | rg -n "reset|checkout|clean|rm -rf"  || echo "No index-resetting commands found"

echo ""
echo "=== Complete 'Init and update submodule' step (lines 18-36) ==="
sed -n '18,36p' "$FILE"

Repository: peanutprotocol/peanut-ui

Length of output: 1226


🏁 Script executed:

#!/bin/bash

# Let's check the full picture and understand git submodule behavior
echo "=== Key question: Does 'git checkout' inside a submodule stage the change in parent? ==="
echo "Testing git submodule staging behavior..."

python3 << 'PYTHON_EOF'
# Simulate and document the git submodule behavior
behavior_summary = """
When you run in parent repo:
  1. git submodule update --init src/content
     -> Stages the submodule pointer in parent's index to match .gitmodules

When you then cd INTO the submodule and run:
  2. git fetch origin main
  3. git checkout origin/main
     -> Changes the submodule's working tree and index
     -> DOES NOT update the parent repo's index

The parent repo's working tree SHOWS the new submodule SHA
(because you checked out a new commit in the working tree)
But the parent repo's INDEX still has the OLD SHA from step 1.

When git write-tree reads the parent repo's index, it captures the OLD SHA.

RESULT: The commit created has the old submodule SHA, not the new one.
"""

print(behavior_summary)
PYTHON_EOF

echo ""
echo "=== Verification: Check workflow again for explicit staging ==="
grep -n "git add\|git stage" ".github/workflows/update-content.yml" || echo "No explicit staging found"

Repository: peanutprotocol/peanut-ui

Length of output: 978


Stage src/content before git write-tree to capture the updated submodule SHA.

The step "Init and update submodule" runs git checkout origin/main inside src/content, but this updates only the submodule's working tree—not the parent repo's index. The parent index still holds the old submodule SHA from git submodule update --init. When git write-tree reads the index, it snapshots the old SHA, producing a commit that doesn't reflect the actual submodule update.

Proposed fix
               run: |
                   BRANCH="auto/update-content-$(date -u +%Y%m%d-%H%M%S)"
+                  git add src/content
                   TREE=$(git write-tree)
                   PARENT=$(git rev-parse HEAD)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/update-content.yml around lines 42 - 44, The commit
currently snapshots the parent index with the old submodule SHA because the
updated submodule working tree in src/content isn't staged before calling git
write-tree; after the "Init and update submodule" step and before the lines that
set BRANCH, TREE, and PARENT, stage the submodule update by running a git add of
src/content (i.e., update the parent index to the new submodule SHA), then
proceed to run git write-tree so TREE reflects the updated submodule SHA.

Comment on lines +7 to +8
echo "Pinging Google..."
curl -s -o /dev/null -w " HTTP %{http_code}\n" "https://www.google.com/ping?sitemap=${SITEMAP_URL}"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Google sitemap ping endpoint deprecated 2025 2026

💡 Result:

Google’s sitemap “ping” endpoint (https://www.google.com/ping?sitemap=...) was deprecated on June 26, 2023 and Google said it would stop functioning 6 months later, after which pings would return HTTP 404 (i.e., it no longer does anything). [1]

For 2025–2026, the supported options are to let Google discover sitemaps via robots.txt and/or submit sitemaps via Google Search Console (including programmatically via the Search Console API “sitemaps.submit” method). [1][2]


🌐 Web query:

Google Search Console sitemap submission API best practices 2025

💡 Result:

Google Search Console Sitemap submission API best practices (2025)

1) Submit the sitemap index, not every child sitemap

  • If you have many sitemaps, submit the sitemap index (e.g., sitemap.xml that references the others). Submitting each child sitemap is optional, and sitemap submissions don’t guarantee immediate recrawling. [4]

2) Use the correct API method + property format

  • Endpoint: PUT https://www.googleapis.com/webmasters/v3/sites/{siteUrl}/sitemaps/{feedpath} (no request body). [1]
  • siteUrl must match the Search Console property exactly (URL-prefix like https://example.com/ or domain property like sc-domain:example.com). [1]
  • Auth scope: https://www.googleapis.com/auth/webmasters. [1]

3) Don’t “spam” submissions—rate-limit and retry properly

  • Treat submit as a configuration/update action (new sitemap, moved location, major restructure), not something to call on every deploy.
  • Implement exponential backoff on 429/5xx.
  • Respect quotas: for “all other resources” (includes Sitemaps), Google documents 20 QPS / 200 QPM per user and 100,000,000 QPD per project. [2]

4) Make the sitemap itself maximally crawlable

  • Keep within limits: 50,000 URLs or 50MB uncompressed per sitemap; split and use a sitemap index as needed. [3]
  • Use UTF‑8, absolute canonical URLs, and (for simplest coverage) host at the site root. [3]
  • Also reference the sitemap in robots.txt as an additional discovery path. [3]

5) Monitor outcomes in GSC—submission is only a hint

  • “Submitted successfully” just means Google accepted the sitemap URL; it doesn’t guarantee Google will crawl or index the listed URLs on a fixed timeline. Use GSC reporting (and URL Inspection for one-offs) to verify impact. [3][4]

🌐 Web query:

sitemap ping endpoint Google Bing current status

💡 Result:

Current status (Feb 27, 2026)

Engine “Sitemap ping” endpoint Status What to do instead
Google Search https://www.google.com/ping?sitemap={SITEMAP_URL} Deprecated; no longer functions (Google said it would return 404 after deprecation, and the deprecation is complete). [1] Submit via Google Search Console and/or reference the sitemap in robots.txt. [1]
Bing https://www.bing.com/ping?sitemap={SITEMAP_URL} (also historically http://...) Removed / deprecated for anonymous sitemap submission. [2] Use Bing Webmaster Tools for sitemap submission and/or adopt IndexNow for near-real-time URL notifications. [2][3]

Sources: Google Search Central (June 26, 2023) [1]; Bing Webmaster Blog (May 13, 2022) [2]; IndexNow official site [3].


Both Google and Bing sitemap ping endpoints are deprecated and no longer functional.

Google's ping endpoint (deprecated June 2023) and Bing's endpoint (deprecated May 2022) now return HTTP 404 and should not be used. Replace with current best practices:

  • Submit sitemaps via Google Search Console (or programmatically via the Search Console API)
  • Reference the sitemap in robots.txt as an additional discovery path
  • For Bing, use Bing Webmaster Tools or IndexNow for URL notifications
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/ping-sitemap.sh` around lines 7 - 8, The script currently calls the
deprecated Google/Bing ping endpoints using the curl line referencing
SITEMAP_URL; remove that curl ping and instead update the script
(scripts/ping-sitemap.sh) to stop attempting HTTP pings and output guidance or
trigger supported flows: log a message that sitemaps must be submitted via
Google Search Console (or programmatically via the Search Console API), ensure
the sitemap is referenced in robots.txt (mention SITEMAP_URL), and for Bing
recommend using Bing Webmaster Tools or IndexNow for URL notifications; if
automation is required, add an optional code path to invoke the Search Console
API or IndexNow rather than the old curl ping.

return <>{children}</>
}

export const DocPage = Object.assign(DocPageRoot, { Design, Code })
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if DocPage.Design or DocPage.Code are used elsewhere
rg -nP --type=tsx '\bDocPage\.(Design|Code)\b'

Repository: peanutprotocol/peanut-ui

Length of output: 95


🏁 Script executed:

#!/bin/bash
# Read the DocPage.tsx file to understand the component structure
cat -n src/app/\(mobile-ui\)/dev/ds/_components/DocPage.tsx

Repository: peanutprotocol/peanut-ui

Length of output: 1292


🏁 Script executed:

#!/bin/bash
# Search for DocPage.Design or DocPage.Code usage in all TypeScript/TSX files
rg 'DocPage\.(Design|Code)' --type=ts --type=tsx 2>/dev/null || rg 'DocPage\.(Design|Code)'

Repository: peanutprotocol/peanut-ui

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Alternative: Search for any imports or uses of Design or Code from DocPage
rg -i 'from.*DocPage' --include='*.ts' --include='*.tsx'

Repository: peanutprotocol/peanut-ui

Length of output: 509


🏁 Script executed:

#!/bin/bash
# Search for imports of DocPage or references to Design/Code from that module
rg 'DocPage' --max-count=20

Repository: peanutprotocol/peanut-ui

Length of output: 7299


🏁 Script executed:

#!/bin/bash
# Look for where DocPage file is imported
rg "from.*'.*DocPage" --max-count=20

Repository: peanutprotocol/peanut-ui

Length of output: 3015


Split multiple components into separate files.

This file exports DocPageRoot, Design, and Code via Object.assign, violating the guideline that components should not be exported together from the same file. Since the composite API (DocPage.Design, DocPage.Code) is not used anywhere in the codebase, abstract these components to separate files.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(mobile-ui)/dev/ds/_components/DocPage.tsx at line 33, The file
currently combines DocPageRoot, Design, and Code into a single export via
Object.assign (export const DocPage = Object.assign(DocPageRoot, { Design, Code
})); split these into separate component files by moving Design and Code into
their own modules (e.g., Design.tsx and Code.tsx) that export their components
individually, keep DocPageRoot as the default export of this file (or rename to
DocPageRoot and export it), and update any imports across the codebase to import
DocPageRoot, Design, and Code from their new paths instead of using the
composite DocPage.Design/DocPage.Code API; remove the Object.assign export to
enforce single-component-per-file guideline.

Comment on lines +82 to +86
const animatedTotal = useCountUp(tierInfo?.data?.totalPoints ?? 0, {
storageKey: 'hero_total',
duration: 1.8,
enabled: !!tierInfo?.data,
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Scope persisted hero points by user identity.

At Line 83, storageKey: 'hero_total' is global for the browser. After account switches on the same device, the page can briefly show the previous user’s stored points before animating, which is a data isolation/UX issue.

🔧 Proposed fix
-    const animatedTotal = useCountUp(tierInfo?.data?.totalPoints ?? 0, {
-        storageKey: 'hero_total',
+    const animatedTotal = useCountUp(tierInfo?.data?.totalPoints ?? 0, {
+        storageKey: user?.user.userId ? `hero_total_${user.user.userId}` : undefined,
         duration: 1.8,
         enabled: !!tierInfo?.data,
     })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(mobile-ui)/points/page.tsx around lines 82 - 86, The persisted
storage key for animatedTotal is global and causes cross-account leakage; change
the storageKey used in the useCountUp call to include the current user's unique
identity (e.g., user id or session id) so it becomes per-user (for example
derive id from tierInfo?.data or currentUser and use `hero_total_${userId}`),
and ensure a safe fallback for unauthenticated users (e.g., `guest`) so
useCountUp(animation) still works when id is absent.

Comment on lines +140 to +143
<article
className="prose prose-lg prose-headings:font-bold prose-a:text-black prose-a:underline prose-pre:border prose-pre:border-n-1 prose-pre:bg-white max-w-none"
dangerouslySetInnerHTML={{ __html: post.html }}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for sanitization patterns and dangerouslySetInnerHTML usage
rg -n "dangerouslySetInnerHTML|getPostBySlug|marked\(|sanitize|DOMPurify|rehype-sanitize|sanitize-html" -C3

Repository: peanutprotocol/peanut-ui

Length of output: 35676


🏁 Script executed:

# Also check the blog page file structure and imports
fd "blog" --type f | head -20

Repository: peanutprotocol/peanut-ui

Length of output: 121


🏁 Script executed:

# Look at the specific file mentioned
cat -n src/app/\[locale\]/\(marketing\)/blog/\[slug\]/page.tsx | head -160 | tail -40

Repository: peanutprotocol/peanut-ui

Length of output: 2593


🏁 Script executed:

# Check for package.json dependencies related to markdown/HTML processing
cat package.json | grep -A 20 '"dependencies"' | grep -E "marked|remark|rehype|unified|sanitize|dompurify"

Repository: peanutprotocol/peanut-ui

Length of output: 50


🏁 Script executed:

# Check package.json for sanitization dependencies
cat package.json | grep -E "sanitize|dompurify|rehype" || echo "No exact matches found, checking all dependencies..."

Repository: peanutprotocol/peanut-ui

Length of output: 120


🏁 Script executed:

# Look at the getPostBySlug function in blog.ts to see how HTML is generated
cat -n src/lib/blog.ts

Repository: peanutprotocol/peanut-ui

Length of output: 3931


🏁 Script executed:

# Check the markdown/HTML processing pipeline
rg "marked\(" -A 5 -B 5

Repository: peanutprotocol/peanut-ui

Length of output: 617


Add HTML sanitization before rendering blog content with dangerouslySetInnerHTML.

The marked() library converts markdown to HTML but does not sanitize the output. The blog post HTML reaches dangerouslySetInnerHTML (line 142) without any sanitization step, creating an XSS vulnerability. The fallback code renderer (line 79 in src/lib/blog.ts) also directly interpolates the language variable into HTML attributes without escaping.

Install and use a sanitization library like dompurify to clean the HTML in getPostBySlug() before returning it, or configure marked with a sanitization option. Example:

import DOMPurify from 'isomorphic-dompurify'
const html = DOMPurify.sanitize(await marked(content, { renderer }))

Also escape the language variable in the code block fallback:

return `<pre><code class="language-${language.replace(/"/g, '&quot;')}">${text.replace(/</g, '&lt;')}</code></pre>`
🧰 Tools
🪛 ast-grep (0.41.0)

[warning] 141-141: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🪛 Biome (2.4.4)

[error] 142-142: Avoid passing content using the dangerouslySetInnerHTML prop.

(lint/security/noDangerouslySetInnerHtml)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/(marketing)/blog/[slug]/page.tsx around lines 140 - 143,
The blog HTML is passed to dangerouslySetInnerHTML without sanitization and the
code-block fallback in src/lib/blog.ts interpolates the language variable
unsafely; update getPostBySlug (or wherever marked() is called) to sanitize the
generated HTML using a server-safe sanitizer (e.g., import and call
isomorphic-dompurify/DOMPurify.sanitize on the result of marked(renderer))
before assigning post.html, and in the fallback renderer function escape the
language identifier and code text (replace dangerous characters like quotes and
angle brackets) before embedding them into the returned <pre><code> HTML string
to prevent XSS.

Comment on lines +40 to +71
export function Tabs({ labels, children }: TabsProps) {
const tabs = labels.split(',').map((l) => l.trim())
return (
<div className={`mx-auto my-8 ${PROSE_WIDTH} px-6 md:px-4`}>
<RadixTabs.Root defaultValue={tabs[0]} className="w-full">
<RadixTabs.List
className="flex w-full items-center rounded-xl bg-white p-1 shadow-sm ring-1 ring-n-1/10"
aria-label="Content tabs"
>
{tabs.map((tab) => (
<RadixTabs.Trigger key={tab} value={tab} className={triggerClasses}>
{tab}
</RadixTabs.Trigger>
))}
</RadixTabs.List>
{children}
</RadixTabs.Root>
</div>
)
}

export function TabPanel({ label, children }: TabPanelProps) {
return (
<RadixTabs.Content
value={label}
forceMount
className="mt-4 text-base leading-[1.75] text-grey-1 data-[state=inactive]:hidden"
>
{children}
</RadixTabs.Content>
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Split Tabs and TabPanel into separate component files.

This file exports multiple components, which violates the repository component export rule.

As per coding guidelines: "Do not export multiple components from same file - abstract multiple components to separate files" and "Do not export multiple items from same component - never export types or utility methods from a component or hook; use separate files for types (interfaces.ts) and utility functions (utils file)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Marketing/mdx/Tabs.tsx` around lines 40 - 71, This file
exports two components (Tabs and TabPanel) which violates the rule to export
only one component per file; split them by moving the Tabs component (function
Tabs and TabsProps type) into its own Tabs.tsx and the TabPanel component
(function TabPanel and TabPanelProps type) into its own TabPanel.tsx, extract
shared types (TabsProps, TabPanelProps) into a separate interfaces.ts if needed,
and move any shared constants/imports (e.g., PROSE_WIDTH, triggerClasses,
RadixTabs) to be imported by both new files; update all imports throughout the
codebase to reference the new single-export files and ensure each file
defaults/explicitly exports only its single component.

Comment on lines +1 to +11
export { COUNTRIES_SEO, CORRIDORS, getLocalizedSEO, getCountryName } from './corridors'
export type { CountrySEO, Corridor } from './corridors'

export { COMPETITORS } from './comparisons'
export type { Competitor } from './comparisons'

export { EXCHANGES } from './exchanges'
export type { Exchange } from './exchanges'

export { PAYMENT_METHODS, PAYMENT_METHOD_SLUGS } from './payment-methods'
export type { PaymentMethod } from './payment-methods'
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Avoid introducing src/data/seo/index.ts as a barrel entrypoint.

This file creates exactly the barrel pattern the repo rules prohibit. Please keep imports direct from concrete modules (corridors, comparisons, exchanges, payment-methods) and remove this aggregate export surface.

As per coding guidelines "No barrel imports - never use import * as X from '@/constants' or create index.ts barrel files; always import from specific files (e.g. import { PEANUT_API_URL } from '@/constants/general.consts')".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/data/seo/index.ts` around lines 1 - 11, Remove this barrel file and
update all call sites to import directly from the concrete modules: import
COUNTRIES_SEO, CORRIDORS, getLocalizedSEO, getCountryName, and types
(CountrySEO, Corridor) from './corridors'; import COMPETITORS and type
Competitor from './comparisons'; import EXCHANGES and type Exchange from
'./exchanges'; import PAYMENT_METHODS, PAYMENT_METHOD_SLUGS and type
PaymentMethod from './payment-methods'. Delete src/data/seo/index.ts (or stop
exporting from it) so the aggregated symbols (COUNTRIES_SEO, CORRIDORS,
getLocalizedSEO, getCountryName, COMPETITORS, EXCHANGES, PAYMENT_METHODS,
PAYMENT_METHOD_SLUGS and their types) are only imported from their specific
modules.

Comment on lines +27 to +38
export { type Locale, type Translations } from './types'
export { SUPPORTED_LOCALES, DEFAULT_LOCALE } from './types'
export {
ROUTE_SLUGS,
localizedPath,
localizedBarePath,
getAlternates,
getBareAlternates,
isValidLocale,
NON_DEFAULT_LOCALES,
type RouteSlug,
} from './config'
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Please avoid src/i18n/index.ts as a barrel re-export layer.

This consolidates re-exports in an index.ts barrel, which conflicts with the repository import/export rule.

As per coding guidelines: "No barrel imports - never use import * as X from '@/constants' or create index.ts barrel files; always import from specific files."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/i18n/index.ts` around lines 27 - 38, This file is acting as a barrel
re-export; remove the barrel by deleting or disabling src/i18n/index.ts and
update all imports to reference the concrete modules directly (import
Locale/Translations from './types' and import SUPPORTED_LOCALES, DEFAULT_LOCALE
from './types'; import ROUTE_SLUGS, localizedPath, localizedBarePath,
getAlternates, getBareAlternates, isValidLocale, NON_DEFAULT_LOCALES, RouteSlug
from './config') so callers consume specific files instead of the index barrel.

Comment on lines +77 to +80
} catch {
// Fallback for unsupported languages
return `<pre><code class="language-${language}">${text}</code></pre>`
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fallback code block doesn't escape HTML, potential XSS risk.

If the highlighter fails and falls back to the raw <pre><code> block, the text content is inserted without escaping. If blog content is untrusted or user-contributed, this could allow XSS.

🛡️ Proposed fix to escape text in fallback
+function escapeHtml(str: string): string {
+    return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
+}
+
 renderer.code = ({ text, lang }: { text: string; lang?: string }) => {
     const language = lang || 'text'
     try {
         return highlighter.codeToHtml(text, {
             lang: language,
             theme: 'github-light',
         })
     } catch {
         // Fallback for unsupported languages
-        return `<pre><code class="language-${language}">${text}</code></pre>`
+        return `<pre><code class="language-${language}">${escapeHtml(text)}</code></pre>`
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch {
// Fallback for unsupported languages
return `<pre><code class="language-${language}">${text}</code></pre>`
}
function escapeHtml(str: string): string {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
}
renderer.code = ({ text, lang }: { text: string; lang?: string }) => {
const language = lang || 'text'
try {
return highlighter.codeToHtml(text, {
lang: language,
theme: 'github-light',
})
} catch {
// Fallback for unsupported languages
return `<pre><code class="language-${language}">${escapeHtml(text)}</code></pre>`
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/blog.ts` around lines 77 - 80, The fallback catch in the syntax
highlighting path inserts raw text into the returned string causing XSS; update
the catch branch in the function that returns `<pre><code
class="language-${language}">${text}</code></pre>` to HTML-escape `text` before
interpolation (use an existing escapeHtml/escape function if available or add a
small utility to replace & < > " ' /), preserving the language class (`language`
variable) and returning the escaped content instead of raw `text`.

Comment on lines 49 to 52
if (isPromoLink(url)) {
const fragment = url.searchParams.toString()
const redirectUrl = `https://peanut.to/claim?&${promoList[fragment]}`
const redirectUrl = `https://peanut.me/claim?&${promoList[fragment]}`
return NextResponse.redirect(redirectUrl)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard missing promo mappings before redirecting.

If promoList[fragment] is absent, users get redirected to https://peanut.me/claim?&undefined. Add a null-check and only redirect when a mapping exists.

Suggested fix
 if (isPromoLink(url)) {
     const fragment = url.searchParams.toString()
-    const redirectUrl = `https://peanut.me/claim?&${promoList[fragment]}`
-    return NextResponse.redirect(redirectUrl)
+    const promoQuery = promoList[fragment]
+    if (!promoQuery) {
+        return NextResponse.next()
+    }
+    const redirectUrl = `https://peanut.me/claim?${promoQuery}`
+    return NextResponse.redirect(redirectUrl)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (isPromoLink(url)) {
const fragment = url.searchParams.toString()
const redirectUrl = `https://peanut.to/claim?&${promoList[fragment]}`
const redirectUrl = `https://peanut.me/claim?&${promoList[fragment]}`
return NextResponse.redirect(redirectUrl)
if (isPromoLink(url)) {
const fragment = url.searchParams.toString()
const promoQuery = promoList[fragment]
if (!promoQuery) {
return NextResponse.next()
}
const redirectUrl = `https://peanut.me/claim?${promoQuery}`
return NextResponse.redirect(redirectUrl)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/proxy.ts` around lines 49 - 52, The promo redirect currently builds
redirectUrl using promoList[fragment] without guarding for missing mappings,
which can produce a URL ending with "undefined"; update the isPromoLink handling
(the block that computes fragment and redirectUrl and calls
NextResponse.redirect) to first check that promoList[fragment] is a
defined/non-empty value and only call NextResponse.redirect when that mapping
exists, otherwise fall through/return the regular response (or a safe fallback)
to avoid redirecting to an invalid URL.

@Hugo0 Hugo0 merged commit b582765 into dev Feb 27, 2026
5 checks passed
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.

3 participants