Conversation
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.
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
# Conflicts: # src/app/page.tsx
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.
* 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
…0226-222922 Update content submodule
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughMajor 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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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 | 🟠 MajorResolve canonical URLs with
URLinstead of string concatenation.Line 28 concatenates
BASE_URL+canonical, which can generate invalid URLs whencanonicalis absolute (or not slash-prefixed). Resolve once vianew 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 | 🟠 MajorLong-press gating blocks keyboard activation.
At Line 121, clicks are dropped unless
isLongPressedis 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 | 🟡 MinorPreserve valid
0values in percentage fallback logicLine 1281 and Line 1283 use truthy checks (
amountSponsored && ...,... || 100), so a valid0can 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 | 🟡 MinorUse 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 | 🟡 MinorUse design system text colors.
Per coding guidelines, use
text-grey-1for secondary text instead of Tailwind'stext-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-1for primary text,text-grey-1for 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 | 🟡 MinorHighlighter only loads 'tsx' language but hook accepts arbitrary
langparameter.The highlighter is initialized with only
langs: ['tsx'], but the hook accepts anylangstring. 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
langparameter 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 | 🟡 MinorKeep table column structure consistent across rows.
Rows without
descriptioncurrently 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 | 🟡 MinorAvoid partial-prefix matches for active tier detection.
startsWithalone 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 | 🟡 MinorLocalize 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 | 🟡 MinorOrphaned absolute positioning - parent lacks
relative.The
absolutepositioned element won't behave as intended because its parent (<div className="rounded-sm border border-n-1 p-4">at line 30) doesn't haverelativepositioning. Either addrelativeto 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 | 🟡 MinorAdd missing
credit-cardicon to theALL_ICONSarray.The
ALL_ICONSarray is manually maintained and has drifted from the source:credit-cardexists in theIconNametype 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 | 🟡 MinorModule-level counter mutated during render can cause non-deterministic behavior.
Mutating
counterduring 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
ProseStarsinstances 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 | 🟡 MinorUpdate type annotation to match marked 17.0.2
Tokens.Codesignature.The destructured parameter works with marked 17.0.2, but the type annotation is incomplete.
Tokens.Codeincludestext,lang, andescaped(plus standard token fields). Update to:renderer.code = ({ text, lang, escaped }: { text: string; lang?: string; escaped?: boolean }) => {Or use the full
Tokens.Codetype 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 | 🟡 MinorLong-press handlers can be silently overridden by forwarded props.
At Line 177,
{...props}is spread afteronMouseDown/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 | 🟡 MinorFix user-facing FAQ copy typos/grammar before release.
There are visible copy issues (e.g.,
Faqscasing 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 | 🟡 MinorAlign the DS layout root with the required app-page scaffold.
The root shell is missing
min-h-[inherit]andgap-8, andNavHeaderis 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 | 🟡 MinorUse 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 | 🟡 MinorGuard against empty or malformed
labelsto avoid invalid default tab state.If
labelsis empty (or only commas/spaces),tabs[0]becomesundefinedand the tab root gets an invaliddefaultValue.🛡️ 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 | 🟡 MinorPotential issue with MDX child type comparison.
The check
child.type === FAQItemmay not work reliably with MDX compilation, as the component identity can change during the MDX transformation. The fallbackchild.props?.questioncheck 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 | 🟡 MinorHover copy icon won’t appear (missing
groupclass on the button).Line 74 uses
group-hover, but Line 59 does not definegroup, 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 | 🟡 MinorPath construction may have redundant 'content' segment.
Line 117 joins
ROOT(src/content) with'content'andconfig.contentDir, resulting in paths likesrc/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 | 🟡 MinorDescription contains full body content instead of a summary.
Line 70 sets
description: content.bodywhich includes the entire MDX content. This could result in very large description values. Consider usingcontent.frontmatter.descriptionor 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 | 🟡 MinorString coercion may produce unhelpful text for complex children.
If
child.props.childrenis 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 | 🟡 MinorCategory 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 | 🟡 MinorCurrent 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
dynamicParamsor 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 | 🟡 MinorLast 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 | 🟡 MinorUse project color tokens instead of Tailwind defaults.
Per coding guidelines, use
text-grey-1for secondary text instead oftext-gray-500,text-gray-600, andtext-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-1for primary text,text-grey-1for 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 | 🟡 MinorCategory fallback logic may produce unexpected results.
When a category is not found in
CATEGORY_I18N_KEYS, the fallbacki18n['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 | 🟡 MinorReplace barrel import with direct file import.
The import from
@/data/seois 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 | 🟡 MinorRemove this unused function or clarify its intent with a comment.
The function
listPublishedSlugsis 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
⛔ Files ignored due to path filters (3)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlsrc/assets/cards/DEPRECATED_Cart Gradient 5.svgis excluded by!**/*.svgsrc/assets/chains/arbitrum.svgis 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.gitmodulesREADME.mdnext.config.jspackage.jsonpublic/.well-known/did.jsonpublic/llms-full.txtpublic/llms.txtredirects.jsonscripts/README-qr-generator.mdscripts/generate-valid-links.tsscripts/ping-sitemap.shscripts/validate-content.tsscripts/validate-links.tssrc/app/(mobile-ui)/dev/components/page.tsxsrc/app/(mobile-ui)/dev/ds/_components/CatalogCard.tsxsrc/app/(mobile-ui)/dev/ds/_components/CodeBlock.tsxsrc/app/(mobile-ui)/dev/ds/_components/DesignNote.tsxsrc/app/(mobile-ui)/dev/ds/_components/DoDont.tsxsrc/app/(mobile-ui)/dev/ds/_components/DocHeader.tsxsrc/app/(mobile-ui)/dev/ds/_components/DocPage.tsxsrc/app/(mobile-ui)/dev/ds/_components/DocSection.tsxsrc/app/(mobile-ui)/dev/ds/_components/DocSidebar.tsxsrc/app/(mobile-ui)/dev/ds/_components/Playground.tsxsrc/app/(mobile-ui)/dev/ds/_components/PropsTable.tsxsrc/app/(mobile-ui)/dev/ds/_components/SectionDivider.tsxsrc/app/(mobile-ui)/dev/ds/_components/StatusTag.tsxsrc/app/(mobile-ui)/dev/ds/_components/TierNav.tsxsrc/app/(mobile-ui)/dev/ds/_components/WhenToUse.tsxsrc/app/(mobile-ui)/dev/ds/_components/nav-config.tssrc/app/(mobile-ui)/dev/ds/_hooks/useHighlightedCode.tssrc/app/(mobile-ui)/dev/ds/foundations/borders/page.tsxsrc/app/(mobile-ui)/dev/ds/foundations/colors/page.tsxsrc/app/(mobile-ui)/dev/ds/foundations/icons/page.tsxsrc/app/(mobile-ui)/dev/ds/foundations/page.tsxsrc/app/(mobile-ui)/dev/ds/foundations/shadows/page.tsxsrc/app/(mobile-ui)/dev/ds/foundations/spacing/page.tsxsrc/app/(mobile-ui)/dev/ds/foundations/typography/page.tsxsrc/app/(mobile-ui)/dev/ds/layout.tsxsrc/app/(mobile-ui)/dev/ds/page.tsxsrc/app/(mobile-ui)/dev/ds/patterns/amount-input/page.tsxsrc/app/(mobile-ui)/dev/ds/patterns/cards-global/page.tsxsrc/app/(mobile-ui)/dev/ds/patterns/copy-share/page.tsxsrc/app/(mobile-ui)/dev/ds/patterns/drawer/page.tsxsrc/app/(mobile-ui)/dev/ds/patterns/feedback/page.tsxsrc/app/(mobile-ui)/dev/ds/patterns/layouts/page.tsxsrc/app/(mobile-ui)/dev/ds/patterns/loading/page.tsxsrc/app/(mobile-ui)/dev/ds/patterns/modal/page.tsxsrc/app/(mobile-ui)/dev/ds/patterns/navigation/page.tsxsrc/app/(mobile-ui)/dev/ds/patterns/page.tsxsrc/app/(mobile-ui)/dev/ds/primitives/base-input/page.tsxsrc/app/(mobile-ui)/dev/ds/primitives/base-select/page.tsxsrc/app/(mobile-ui)/dev/ds/primitives/button/page.tsxsrc/app/(mobile-ui)/dev/ds/primitives/card/page.tsxsrc/app/(mobile-ui)/dev/ds/primitives/checkbox/page.tsxsrc/app/(mobile-ui)/dev/ds/primitives/divider/page.tsxsrc/app/(mobile-ui)/dev/ds/primitives/page-container/page.tsxsrc/app/(mobile-ui)/dev/ds/primitives/page.tsxsrc/app/(mobile-ui)/dev/ds/primitives/title/page.tsxsrc/app/(mobile-ui)/dev/ds/primitives/toast/page.tsxsrc/app/(mobile-ui)/dev/page.tsxsrc/app/(mobile-ui)/home/page.tsxsrc/app/(mobile-ui)/points/invites/page.tsxsrc/app/(mobile-ui)/points/page.tsxsrc/app/(mobile-ui)/qr-pay/page.tsxsrc/app/(mobile-ui)/support/layout.tsxsrc/app/(mobile-ui)/support/page.tsxsrc/app/(mobile-ui)/withdraw/[country]/bank/page.tsxsrc/app/(mobile-ui)/withdraw/crypto/page.tsxsrc/app/[locale]/(marketing)/[country]/page.tsxsrc/app/[locale]/(marketing)/blog/[slug]/page.tsxsrc/app/[locale]/(marketing)/blog/category/[cat]/page.tsxsrc/app/[locale]/(marketing)/blog/page.tsxsrc/app/[locale]/(marketing)/compare/[slug]/page.tsxsrc/app/[locale]/(marketing)/deposit/[exchange]/page.tsxsrc/app/[locale]/(marketing)/error.tsxsrc/app/[locale]/(marketing)/help/TODO.mdsrc/app/[locale]/(marketing)/help/[slug]/page.tsxsrc/app/[locale]/(marketing)/help/page.tsxsrc/app/[locale]/(marketing)/layout.tsxsrc/app/[locale]/(marketing)/pay-with/[method]/page.tsxsrc/app/[locale]/(marketing)/receive-money-from/[country]/page.tsxsrc/app/[locale]/(marketing)/send-money-from/[from]/to/[to]/page.tsxsrc/app/[locale]/(marketing)/send-money-to/[country]/page.tsxsrc/app/[locale]/(marketing)/team/page.tsxsrc/app/actions/card.tssrc/app/api/peanut/user/get-jwt-token/route.tssrc/app/api/peanut/user/get-user-from-cookie/__tests__/route.test.tssrc/app/api/peanut/user/get-user-from-cookie/route.tssrc/app/api/peanut/user/login-user/route.tssrc/app/api/peanut/user/register-user/route.tssrc/app/layout.tsxsrc/app/lp/card/CardLandingPage.tsxsrc/app/lp/card/page.tsxsrc/app/lp/layout.tsxsrc/app/lp/page.tsxsrc/app/metadata.tssrc/app/page.tsxsrc/app/robots.tssrc/app/sitemap.tssrc/app/sw.tssrc/components/0_Bruddle/BaseInput.tsxsrc/components/0_Bruddle/BaseSelect.tsxsrc/components/0_Bruddle/Button.tsxsrc/components/0_Bruddle/Card.tsxsrc/components/0_Bruddle/Checkbox.tsxsrc/components/0_Bruddle/CloudsBackground.tsxsrc/components/0_Bruddle/Divider.tsxsrc/components/0_Bruddle/PageContainer.tsxsrc/components/0_Bruddle/Title.tsxsrc/components/Card/CardGeoScreen.tsxsrc/components/Card/CardInfoScreen.tsxsrc/components/Common/PointsCard.tsxsrc/components/Global/AnimateOnView.tsxsrc/components/Global/FooterVisibilityObserver.tsxsrc/components/Global/InvitesGraph/index.tsxsrc/components/Global/QRScanner/useQRScanner.tssrc/components/LandingPage/CloudsCss.tsxsrc/components/LandingPage/CurrencySelect.tsxsrc/components/LandingPage/LandingPageClient.tsxsrc/components/LandingPage/LandingPageShell.tsxsrc/components/LandingPage/Manteca.tsxsrc/components/LandingPage/RegulatedRails.tsxsrc/components/LandingPage/SendInSecondsCTA.tsxsrc/components/LandingPage/hero.tsxsrc/components/LandingPage/imageAssets.tsxsrc/components/LandingPage/landingPageData.tssrc/components/LandingPage/marquee.tsxsrc/components/LandingPage/sendInSeconds.tsxsrc/components/Marketing/BlogCard.tsxsrc/components/Marketing/ComparisonTable.tsxsrc/components/Marketing/ContentPage.tsxsrc/components/Marketing/DestinationGrid.tsxsrc/components/Marketing/FAQSection.tsxsrc/components/Marketing/HelpLanding.tsxsrc/components/Marketing/JsonLd.tsxsrc/components/Marketing/MarketingErrorBoundary.tsxsrc/components/Marketing/MarketingHero.tsxsrc/components/Marketing/MarketingNav.tsxsrc/components/Marketing/MarketingShell.tsxsrc/components/Marketing/RelatedPages.tsxsrc/components/Marketing/Section.tsxsrc/components/Marketing/Steps.tsxsrc/components/Marketing/index.tssrc/components/Marketing/mdx/CTA.tsxsrc/components/Marketing/mdx/Callout.tsxsrc/components/Marketing/mdx/CountryGrid.tsxsrc/components/Marketing/mdx/ExchangeWidget.tsxsrc/components/Marketing/mdx/FAQ.tsxsrc/components/Marketing/mdx/Hero.tsxsrc/components/Marketing/mdx/ProseStars.tsxsrc/components/Marketing/mdx/RelatedPages.tsxsrc/components/Marketing/mdx/Stars.tsxsrc/components/Marketing/mdx/Steps.tsxsrc/components/Marketing/mdx/Tabs.tsxsrc/components/Marketing/mdx/components.tsxsrc/components/Marketing/mdx/constants.tssrc/components/Marketing/pages/ReceiveMoneyContent.tsxsrc/components/Points/CashCard.tsxsrc/components/Points/InviteePointsBadge.tsxsrc/components/Setup/Views/SignTestTransaction.tsxsrc/components/TransactionDetails/TransactionDetailsReceipt.tsxsrc/constants/countryCurrencyMapping.tssrc/constants/points.consts.tssrc/constants/routes.tssrc/contentsrc/data/seo/comparisons.tssrc/data/seo/corridors.tssrc/data/seo/exchanges.tssrc/data/seo/index.tssrc/data/seo/payment-methods.tssrc/data/seo/utils.tssrc/data/team.tssrc/hooks/useCountUp.tssrc/hooks/useHomeCarouselCTAs.tsxsrc/hooks/useLongPress.tssrc/i18n/config.tssrc/i18n/en.jsonsrc/i18n/es-419.jsonsrc/i18n/es-ar.jsonsrc/i18n/es-es.jsonsrc/i18n/index.tssrc/i18n/pt-br.jsonsrc/i18n/types.tssrc/lib/blog.tssrc/lib/content.tssrc/lib/mdx.tssrc/lib/seo/schemas.tsxsrc/proxy.tssrc/services/points.tssrc/styles/globals.csssrc/types/global.d.tssrc/utils/__mocks__/wagmi-config.tssrc/utils/cookie-migration.utils.tssrc/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
| BRANCH="auto/update-content-$(date -u +%Y%m%d-%H%M%S)" | ||
| TREE=$(git write-tree) | ||
| PARENT=$(git rev-parse HEAD) |
There was a problem hiding this comment.
🧩 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.
| echo "Pinging Google..." | ||
| curl -s -o /dev/null -w " HTTP %{http_code}\n" "https://www.google.com/ping?sitemap=${SITEMAP_URL}" |
There was a problem hiding this comment.
🧩 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.xmlthat 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] siteUrlmust match the Search Console property exactly (URL-prefix likehttps://example.com/or domain property likesc-domain:example.com). [1]- Auth scope:
https://www.googleapis.com/auth/webmasters. [1]
3) Don’t “spam” submissions—rate-limit and retry properly
- Treat
submitas 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.txtas 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.txtas 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 }) |
There was a problem hiding this comment.
🛠️ 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.tsxRepository: 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=20Repository: peanutprotocol/peanut-ui
Length of output: 7299
🏁 Script executed:
#!/bin/bash
# Look for where DocPage file is imported
rg "from.*'.*DocPage" --max-count=20Repository: 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.
| const animatedTotal = useCountUp(tierInfo?.data?.totalPoints ?? 0, { | ||
| storageKey: 'hero_total', | ||
| duration: 1.8, | ||
| enabled: !!tierInfo?.data, | ||
| }) |
There was a problem hiding this comment.
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.
| <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 }} | ||
| /> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for sanitization patterns and dangerouslySetInnerHTML usage
rg -n "dangerouslySetInnerHTML|getPostBySlug|marked\(|sanitize|DOMPurify|rehype-sanitize|sanitize-html" -C3Repository: peanutprotocol/peanut-ui
Length of output: 35676
🏁 Script executed:
# Also check the blog page file structure and imports
fd "blog" --type f | head -20Repository: 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 -40Repository: 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.tsRepository: peanutprotocol/peanut-ui
Length of output: 3931
🏁 Script executed:
# Check the markdown/HTML processing pipeline
rg "marked\(" -A 5 -B 5Repository: 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, '"')}">${text.replace(/</g, '<')}</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.
| 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> | ||
| ) | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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' |
There was a problem hiding this comment.
🛠️ 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.
| 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' |
There was a problem hiding this comment.
🛠️ 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.
| } catch { | ||
| // Fallback for unsupported languages | ||
| return `<pre><code class="language-${language}">${text}</code></pre>` | ||
| } |
There was a problem hiding this comment.
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, '&').replace(/</g, '<').replace(/>/g, '>')
+}
+
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.
| } catch { | |
| // Fallback for unsupported languages | |
| return `<pre><code class="language-${language}">${text}</code></pre>` | |
| } | |
| function escapeHtml(str: string): string { | |
| return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') | |
| } | |
| 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`.
| 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) |
There was a problem hiding this comment.
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.
| 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.
No description provided.