Skip to content

fix: improve SEO for listing and hackathon pages#1349

Draft
devanshk404 wants to merge 8 commits into
SuperteamDAO:mainfrom
devanshk404:fix/seo-listing-pages-ssr
Draft

fix: improve SEO for listing and hackathon pages#1349
devanshk404 wants to merge 8 commits into
SuperteamDAO:mainfrom
devanshk404:fix/seo-listing-pages-ssr

Conversation

@devanshk404

@devanshk404 devanshk404 commented Mar 19, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Layer 1 — Hackathon meta tags: Added missing <Meta> to /hackathon/all (had none); updated all 8 individual hackathon page titles to [Name] — Submission Tracks | Superteam Earn with specific descriptions replacing the old generic copy
  • Layer 2 — SSR content prefetch: Listing and grant content now ships in the initial HTML so Google sees real data without waiting for JS; implemented via React Query dehydration pattern — getServerSideProps prefetches the default view directly via Prisma, dehydrates into page props, HydrationBoundary in _app.tsx rehydrates on client, staleTime: 60s prevents immediate re-fetch

Pages changed

Page Change
/hackathon/all Added Meta (was missing entirely)
/hackathon/radar, /breakout, /cypherpunk, /mobius, /redacted, /renaissance, /scribes, /talent-olympics Title + description updated
/earn/grants Added getServerSideProps with Prisma prefetch
/earn/all, /earn/bounties, /earn/projects Extended getServerSideProps with Prisma prefetch
useListings, useGrants Added staleTime: 60 * 1000
_app.tsx Added HydrationBoundary

Technical notes

  • Query keys normalize undefined → null to survive JSON.parse/JSON.stringify serialization through Next.js props
  • Prisma Date objects serialized with JSON.parse(JSON.stringify(...)) before passing through getServerSideProps
  • Only the default view (first tab, category: All, status: open) is SSR'd; tab switches and pagination remain client-side

Test plan

  • View page source (not DevTools) on /earn/grants, /earn/all, /earn/bounties — listing/grant cards should be visible in raw HTML
  • Open Network tab, hard-refresh /earn/grants — no /api/grants request should fire on initial load (data comes from SSR)
  • Check <title> tag on all /earn/hackathon/* pages matches [Name] — Submission Tracks | Superteam Earn
  • Confirm no double-fetch within 60s of page load

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Performance

    • Improved client-side caching freshness and more consistent listing/filter behavior for listings and grants.
    • Added server-side data prefetching and client hydration for many listings, projects, and grants pages for faster initial loads.
  • SEO

    • Updated hackathon page titles, descriptions, and structured data (JSON-LD) to emphasize submission tracks and improve search visibility.

Layer 1 — Hackathon meta tags:
- Add missing Meta to /hackathon/all (had none)
- Update all individual hackathon page titles to include hackathon
  name + "Submission Tracks" (Radar, Renaissance, Scribes, Breakout,
  Cypherpunk, Mobius, Redacted, Talent Olympics)
- Replace generic descriptions with hackathon-specific copy

Layer 2 — SSR content prefetch for listing pages:
- Add HydrationBoundary to _app.tsx to enable React Query dehydration
- Add staleTime (60s) to useGrants and useListings to prevent
  immediate client refetch of SSR-prefetched data
- Add getServerSideProps to /grants: prefetches all grants via Prisma
  directly, dehydrates into page props
- Extend getServerSideProps on /all, /bounties, /projects: prefetches
  default listing view (tab/category/status defaults) via Prisma,
  dehydrates into page props
- Normalize undefined → null in useListings query key to ensure
  dehydrated state keys survive JSON serialization
- Use JSON.parse/JSON.stringify on dehydrated state and Prisma results
  to handle Date objects in Next.js props serialization

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel

vercel Bot commented Mar 19, 2026

Copy link
Copy Markdown

@devanshk404 is attempting to deploy a commit to the Superteam Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Mar 19, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

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

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aa4cc32e-b616-4a2c-86b4-25d11c91e981

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

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This PR adds server-side React Query prefetching and dehydration to multiple earn pages, sets a 60s staleTime on listing/grants hooks, wraps the app page render in a HydrationBoundary, and updates SEO metadata/JSON‑LD across many hackathon pages. No exported API signatures were changed.

Changes

Query Hooks & App Hydration

Layer / File(s) Summary
Hook option changes
src/features/grants/hooks/useGrants.ts, src/features/listings/hooks/useListings.ts
Added staleTime: 60 * 1000 to React Query options; useListings also normalizes optional params (undefined -> null) in the queryKey.
App hydration wiring
src/pages/_app.tsx
Wrapped <Component {...pageProps} key={router.asPath} /> in a React Query HydrationBoundary using pageProps.dehydratedState.

Server-Side Query Prefetching (listings / grants / projects / bounties)

Layer / File(s) Summary
Server query client & imports
src/pages/earn/... (multiple pages)
Added QueryClient, dehydrate, prisma and listing/grants helper imports to support server prefetching.
Build query / fetch / transform
src/pages/earn/all/index.tsx, src/pages/earn/bounties/index.tsx, src/pages/earn/grants/index.tsx, src/pages/earn/projects/index.tsx
getServerSideProps creates QueryClient, uses buildListingQuery/buildGrantsQuery to compute where/orderBy/take, fetches via prisma.*.findMany with listingSelect/grantsSelect, post-processes results (reorderFeaturedOngoing, ISO date normalization, compute totalApplications).
Prefetch into QueryClient
same files
Prefetches queries via prefetchQuery into the server QueryClient.
Dehydrate & return props
same files
Returns props that include dehydratedState: dehydrate(queryClient) alongside existing potentialSession.

Hackathon pages: SEO, JSON‑LD, and server prefetching

Layer / File(s) Summary
Meta / JSON‑LD updates
src/pages/earn/hackathon/* (breakout, cypherpunk, mobius, radar, redacted, renaissance, scribes, talent-olympics, all)
Updated Meta titles/descriptions to submission-track wording and added or updated JsonLd breadcrumb/Event JSON‑LD blocks on several pages.
Server prefetch wiring
src/pages/earn/hackathon/* (breakout, cypherpunk, radar, redacted, renaissance, scribes, plus others)
Added getServerSideProps to multiple hackathon pages that create QueryClient, prefetch tracks and stats (Prisma-backed), and return dehydratedState: dehydrate(queryClient) for client hydration.
Minor content normalization
src/pages/earn/hackathon/*
Normalized punctuation/quotes in several FAQ strings (typographic apostrophe -> straight apostrophe) without semantic changes.

Sequence Diagram

sequenceDiagram
    participant Browser as Browser (Client)
    participant Server as Next.js Server
    participant QueryClient as QueryClient
    participant Database as Prisma/Database
    participant Hydrator as HydrationBoundary

    Browser->>Server: GET /earn/...
    activate Server
    Server->>QueryClient: new QueryClient()
    Server->>Database: buildListingQuery() + prisma.findMany()
    Database-->>Server: records
    Server->>Server: transform results (reorder, ISO dates, totals)
    Server->>QueryClient: prefetchQuery(queryKey, transformedData)
    Server->>Server: dehydrate(queryClient)
    Server-->>Browser: HTML + dehydratedState
    deactivate Server

    Browser->>Hydrator: render with dehydratedState
    Hydrator->>QueryClient: hydrate client cache
    Hydrator-->>Browser: hydrated UI
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • SuperteamDAO/earn#1145: Changes to src/features/listings/hooks/useListings.ts affecting query key/params handling — closely related.
  • SuperteamDAO/earn#983: Edits affecting src/pages/earn/hackathon/redacted.tsx Meta/FAQ — overlaps with this PR's redacted page changes.
  • SuperteamDAO/earn#995: Modifies the Mobius hackathon page — related to metadata/FAQ edits here.

Suggested reviewers

  • scutuatua-crypto
  • a20hek

Poem

🐰
I prefetch by moonlight, hydrate by day,
Server gives the state, so clients need not stray.
Queries settled, metadata bright,
Hop—your pages load just right!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'fix: improve SEO for listing and hackathon pages' accurately describes the main changes across the changeset, which focus on SEO improvements through Meta tags, JSON-LD structured data, and SSR prefetching for listing and hackathon pages.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
src/features/grants/hooks/useGrants.ts (1)

50-59: ⚠️ Potential issue | 🟠 Major

Query key mismatch will prevent SSR hydration from working.

The queryKey uses raw undefined values for optional parameters (region, sponsor, skill), but the server-side prefetch in grants/index.tsx passes through JSON.parse(JSON.stringify(...)) which converts undefined to null in arrays. This creates a cache key mismatch:

  • Server key after serialization: ['grants', 'all', 'All', null, null, null]
  • Client key: ['grants', 'all', 'All', undefined, undefined, undefined]

React Query won't find the prefetched data, causing an unnecessary client-side refetch.

Proposed fix: Normalize optional params to null
 export function useGrants({
   context,
   category,
   region,
   sponsor,
   skill,
 }: GrantsParams) {
   return useQuery({
-    queryKey: ['grants', context, category, region, sponsor, skill],
+    queryKey: [
+      'grants',
+      context,
+      category,
+      region ?? null,
+      sponsor ?? null,
+      skill ?? null,
+    ],
     queryFn: () =>
       fetchGrants({
         context,
         category,
         region,
         sponsor,
         skill,
       }),
     staleTime: 60 * 1000,
   });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/grants/hooks/useGrants.ts` around lines 50 - 59, The query key
in useGrants (the queryKey array built in the hook) must normalize optional
parameters to null to match server-side serialization; update the construction
of the queryKey (and any place that computes region, sponsor, skill for the key)
to replace undefined with null (e.g., use region ?? null, sponsor ?? null, skill
?? null) so the client key matches the server-prefetched key and SSR hydration
succeeds for the useGrants query.
🧹 Nitpick comments (5)
src/pages/earn/hackathon/cypherpunk.tsx (1)

194-195: Remove debug console logs.

These console.log statements appear to be debugging artifacts and should be removed from production code to keep the browser console clean.

🧹 Proposed cleanup
   useEffect(() => {
-    console.log('start date', START_DATE);
-    console.log('close date', CLOSE_DATE);
     function updateStatus() {
       if (dayjs().isAfter(dayjs(CLOSE_DATE))) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/earn/hackathon/cypherpunk.tsx` around lines 194 - 195, Remove the
two debug console.log calls that print START_DATE and CLOSE_DATE; locate the
logs in the component file where START_DATE and CLOSE_DATE are referenced (the
lines containing console.log('start date', START_DATE) and console.log('close
date', CLOSE_DATE)) and delete them so no debug output is left in production
code.
src/pages/earn/grants/index.tsx (1)

103-126: Use explicit null values in queryKey for consistency.

The queryKey uses undefined values that will be converted to null during JSON serialization. For clarity and to match the client-side hook pattern (after the fix I suggested for useGrants.ts), use explicit null values.

Proposed fix
   await queryClient.prefetchQuery({
-    queryKey: ['grants', 'all', 'All', undefined, undefined, undefined],
+    queryKey: ['grants', 'all', 'All', null, null, null],
     queryFn: async () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/earn/grants/index.tsx` around lines 103 - 126, The queryKey passed
to queryClient.prefetchQuery uses undefined entries which will be serialized to
null; update the array in the prefetchQuery call (the queryKey inside the async
block that calls buildGrantsQuery and prisma.grants.findMany) to use explicit
null values instead of undefined (e.g., replace the last three undefineds with
null) so it matches the client-side useGrants.ts pattern and keeps keys
consistent with buildGrantsQuery/query hooks.
src/pages/earn/bounties/index.tsx (2)

59-95: Consider extracting shared SSR prefetch logic.

The prefetch logic is nearly identical to projects/index.tsx (and likely all/index.tsx), differing only in the tab parameter. Consider extracting a shared helper:

♻️ Example shared helper
// src/features/listings/utils/ssr-prefetch.ts
export async function prefetchListingsQuery(
  queryClient: QueryClient,
  tab: 'all' | 'bounties' | 'projects',
) {
  await queryClient.prefetchQuery({
    queryKey: ['listings', 'all', tab, 'All', 'open', 'Date', 'asc', null, null, null, false],
    queryFn: async () => {
      const { where, orderBy, take } = await buildListingQuery(
        { context: 'all', tab, category: 'All', status: 'open', sortBy: 'Date', order: 'asc' },
        null,
      );
      const listings = await prisma.bounties.findMany({
        where, orderBy, take, select: listingSelect,
      });
      return JSON.parse(JSON.stringify(reorderFeaturedOngoing(listings)));
    },
  });
}

This would reduce duplication and ensure consistency across pages.

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

In `@src/pages/earn/bounties/index.tsx` around lines 59 - 95, Extract the repeated
SSR prefetch logic into a shared helper (e.g., prefetchListingsQuery) that
accepts a QueryClient and a tab value ('all' | 'bounties' | 'projects'); move
the queryKey assembly and queryFn there and keep it calling buildListingQuery,
prisma.bounties.findMany (with listingSelect), and reorderFeaturedOngoing as-is,
then replace the inline queryClient.prefetchQuery call in this file with a call
to prefetchListingsQuery(queryClient, 'bounties') to remove duplication and
centralize behavior.

16-18: Same type inconsistency: dehydratedState returned but not in interface.

Same issue as in projects/index.tsx. Consider adding dehydratedState to the interface for type completeness.

🔧 Suggested fix
+import type { DehydratedState } from '@tanstack/react-query';

 interface BountiesPageProps {
   readonly potentialSession: boolean;
+  readonly dehydratedState: DehydratedState;
 }

Also applies to: 97-102

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

In `@src/pages/earn/bounties/index.tsx` around lines 16 - 18, The
BountiesPageProps interface is missing the dehydratedState property returned
from getServerSideProps/getStaticProps; update interface BountiesPageProps to
include dehydratedState (e.g., dehydratedState?: DehydratedState | unknown) and
import the correct DehydratedState type from react-query (or use appropriate
type/unknown) so the component signature and any props usage (BountiesPageProps,
dehydratedState) are type-consistent.
src/pages/earn/projects/index.tsx (1)

17-19: Props interface doesn't include dehydratedState but it's returned from getServerSideProps.

The GetServerSideProps<ProjectsPageProps> generic constrains the return type, but dehydratedState is returned without being declared in the interface. While this works at runtime (Next.js passes it to _app.tsx via pageProps), the typing is incomplete.

🔧 Suggested fix
+import type { DehydratedState } from '@tanstack/react-query';

 interface ProjectsPageProps {
   readonly potentialSession: boolean;
+  readonly dehydratedState: DehydratedState;
 }

Also applies to: 100-105

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

In `@src/pages/earn/projects/index.tsx` around lines 17 - 19, The
ProjectsPageProps interface is missing the dehydratedState property that
getServerSideProps returns; update ProjectsPageProps to include dehydratedState
(e.g. dehydratedState?: DehydratedState) and import the correct DehydratedState
type from your query library (e.g. '@tanstack/react-query' or 'react-query') or
use a safe fallback type (Record<string, any>) so the
GetServerSideProps<ProjectsPageProps> generic is correctly satisfied; key
symbols: ProjectsPageProps, getServerSideProps, dehydratedState.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pages/earn/all/index.tsx`:
- Around line 48-61: The prefetch uses a hardcoded authenticated:false in
queryClient.prefetchQuery which will mismatch the client-side useListings call
that uses the real auth state from usePrivy() (causing cache misses); fix by
deriving the authenticated flag on the server from the same request/session
context you use for hydration (e.g., check cookies/session/user id) and replace
the hardcoded false in the query key with that computed boolean so the query key
used in prefetchQuery matches the authenticated value used by
ListingsSection/useListings on the client.
- Around line 84-89: The SSR prefetch uses a hardcoded authenticated: false in
the queryKey while ListingsSection calls useListings with the runtime usePrivy
authenticated value, causing key mismatch; update the server-side prefetch to
use the same authenticated value that the client will use (pass potentialSession
into the prefetch/queryKey instead of hardcoding false), and adjust
ListingsSection/useListings to accept and use that potentialSession prop (or
alternatively have ListingsSection call useListings with potentialSession rather
than usePrivy) so the queryKey used during dehydrate/hydration matches the
client-side key.

---

Outside diff comments:
In `@src/features/grants/hooks/useGrants.ts`:
- Around line 50-59: The query key in useGrants (the queryKey array built in the
hook) must normalize optional parameters to null to match server-side
serialization; update the construction of the queryKey (and any place that
computes region, sponsor, skill for the key) to replace undefined with null
(e.g., use region ?? null, sponsor ?? null, skill ?? null) so the client key
matches the server-prefetched key and SSR hydration succeeds for the useGrants
query.

---

Nitpick comments:
In `@src/pages/earn/bounties/index.tsx`:
- Around line 59-95: Extract the repeated SSR prefetch logic into a shared
helper (e.g., prefetchListingsQuery) that accepts a QueryClient and a tab value
('all' | 'bounties' | 'projects'); move the queryKey assembly and queryFn there
and keep it calling buildListingQuery, prisma.bounties.findMany (with
listingSelect), and reorderFeaturedOngoing as-is, then replace the inline
queryClient.prefetchQuery call in this file with a call to
prefetchListingsQuery(queryClient, 'bounties') to remove duplication and
centralize behavior.
- Around line 16-18: The BountiesPageProps interface is missing the
dehydratedState property returned from getServerSideProps/getStaticProps; update
interface BountiesPageProps to include dehydratedState (e.g., dehydratedState?:
DehydratedState | unknown) and import the correct DehydratedState type from
react-query (or use appropriate type/unknown) so the component signature and any
props usage (BountiesPageProps, dehydratedState) are type-consistent.

In `@src/pages/earn/grants/index.tsx`:
- Around line 103-126: The queryKey passed to queryClient.prefetchQuery uses
undefined entries which will be serialized to null; update the array in the
prefetchQuery call (the queryKey inside the async block that calls
buildGrantsQuery and prisma.grants.findMany) to use explicit null values instead
of undefined (e.g., replace the last three undefineds with null) so it matches
the client-side useGrants.ts pattern and keeps keys consistent with
buildGrantsQuery/query hooks.

In `@src/pages/earn/hackathon/cypherpunk.tsx`:
- Around line 194-195: Remove the two debug console.log calls that print
START_DATE and CLOSE_DATE; locate the logs in the component file where
START_DATE and CLOSE_DATE are referenced (the lines containing
console.log('start date', START_DATE) and console.log('close date', CLOSE_DATE))
and delete them so no debug output is left in production code.

In `@src/pages/earn/projects/index.tsx`:
- Around line 17-19: The ProjectsPageProps interface is missing the
dehydratedState property that getServerSideProps returns; update
ProjectsPageProps to include dehydratedState (e.g. dehydratedState?:
DehydratedState) and import the correct DehydratedState type from your query
library (e.g. '@tanstack/react-query' or 'react-query') or use a safe fallback
type (Record<string, any>) so the GetServerSideProps<ProjectsPageProps> generic
is correctly satisfied; key symbols: ProjectsPageProps, getServerSideProps,
dehydratedState.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 18e60055-7f83-4d01-b97e-111dfbd0cdb5

📥 Commits

Reviewing files that changed from the base of the PR and between 490b36d and 31f43bd.

📒 Files selected for processing (16)
  • src/features/grants/hooks/useGrants.ts
  • src/features/listings/hooks/useListings.ts
  • src/pages/_app.tsx
  • src/pages/earn/all/index.tsx
  • src/pages/earn/bounties/index.tsx
  • src/pages/earn/grants/index.tsx
  • src/pages/earn/hackathon/all.tsx
  • src/pages/earn/hackathon/breakout.tsx
  • src/pages/earn/hackathon/cypherpunk.tsx
  • src/pages/earn/hackathon/mobius.tsx
  • src/pages/earn/hackathon/radar.tsx
  • src/pages/earn/hackathon/redacted.tsx
  • src/pages/earn/hackathon/renaissance.tsx
  • src/pages/earn/hackathon/scribes.tsx
  • src/pages/earn/hackathon/talent-olympics.tsx
  • src/pages/earn/projects/index.tsx

Comment on lines +48 to +61
await queryClient.prefetchQuery({
queryKey: [
'listings',
'all',
'all',
'All',
'open',
'Date',
'asc',
null,
null,
null,
false,
],

Copy link
Copy Markdown
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 useListings usage in ListingsSection to check authenticated parameter
ast-grep --pattern $'useListings({
  $$$
})'

# Also check ListingsSection component for potentialSession/authenticated handling
rg -n -A 20 'function ListingsSection' --type ts --type tsx

Repository: SuperteamDAO/earn

Length of output: 1018


🏁 Script executed:

# Find ListingsSection component file and check authenticated initialization
rg -n "authenticated\s*=" src/features/listings/components/ListingsSection.tsx | head -20

# Also check imports to understand potentialSession usage
rg -n -B 5 -A 5 "useListings" src/features/listings/components/ListingsSection.tsx | head -40

Repository: SuperteamDAO/earn

Length of output: 1174


🏁 Script executed:

# Get the full ListingsSection component to find authenticated initialization
sed -n '1,200p' src/features/listings/components/ListingsSection.tsx | cat -n

# Also search specifically for where authenticated is assigned
rg -n "authenticated\s*[:=]" src/features/listings/components/ListingsSection.tsx

Repository: SuperteamDAO/earn

Length of output: 7266


🏁 Script executed:

# Check the all/index.tsx file for the SSR context
sed -n '40,85p' src/pages/earn/all/index.tsx | cat -n

Repository: SuperteamDAO/earn

Length of output: 1372


Query key mismatch: authenticated parameter must align between SSR and client.

The prefetch query key hardcodes authenticated: false (line 60), but the client-side ListingsSection component passes the actual authenticated state from usePrivy() to useListings (line 183 of ListingsSection.tsx). For any authenticated user, the query keys will not match, causing a cache miss and defeating the SSR prefetch optimization.

The server should derive the authenticated value from the same source used for client hydration. Consider checking the authentication state from the request context (e.g., session or user ID hint from cookies) and passing that value to the prefetch query key so it aligns with what the client will request during hydration.

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

In `@src/pages/earn/all/index.tsx` around lines 48 - 61, The prefetch uses a
hardcoded authenticated:false in queryClient.prefetchQuery which will mismatch
the client-side useListings call that uses the real auth state from usePrivy()
(causing cache misses); fix by deriving the authenticated flag on the server
from the same request/session context you use for hydration (e.g., check
cookies/session/user id) and replace the hardcoded false in the query key with
that computed boolean so the query key used in prefetchQuery matches the
authenticated value used by ListingsSection/useListings on the client.

Comment on lines +84 to +89
return {
props: {
potentialSession: cookieExists,
dehydratedState: JSON.parse(JSON.stringify(dehydrate(queryClient))),
},
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how ListingsSection uses potentialSession prop
rg -n -B 5 -A 30 'export.*function ListingsSection|export const ListingsSection' --type ts

# Check if potentialSession is passed to useListings as authenticated
rg -n 'potentialSession|authenticated' src/features/listings/components/ListingsSection.tsx

Repository: SuperteamDAO/earn

Length of output: 3506


🏁 Script executed:

# Check the page file and how ListingsSection is used
rg -n -B 10 -A 10 'ListingsSection' src/pages/earn/all/index.tsx | head -80

# Also check what useListings does with authenticated parameter
rg -n -A 20 'export.*function useListings|export const useListings' src/features/listings

Repository: SuperteamDAO/earn

Length of output: 4584


🏁 Script executed:

# Get the complete getServerSideProps function
sed -n '40,100p' src/pages/earn/all/index.tsx

# Check if there's prefetching of useListings with authenticated parameter
rg -n 'prefetchQuery.*useListings\|setQueryData.*listings' src/pages/earn/all/index.tsx

Repository: SuperteamDAO/earn

Length of output: 1178


🏁 Script executed:

# Get the full ListingsSection component to see how useListings is called
cat -n src/features/listings/components/ListingsSection.tsx | head -200

Repository: SuperteamDAO/earn

Length of output: 7266


Fix the queryKey mismatch between SSR prefetch and client-side useListings call.

The SSR prefetch at line 48 hardcodes authenticated: false in the queryKey, but ListingsSection calls useListings with the authenticated value from the usePrivy hook (line 183). If Privy's authenticated state differs from the hardcoded false value during hydration, the queryKey won't match and the prefetched data won't be used. Either ensure the prefetch uses the same authenticated value that will be used on the client, or have ListingsSection pass a consistent value (such as from potentialSession) to useListings.

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

In `@src/pages/earn/all/index.tsx` around lines 84 - 89, The SSR prefetch uses a
hardcoded authenticated: false in the queryKey while ListingsSection calls
useListings with the runtime usePrivy authenticated value, causing key mismatch;
update the server-side prefetch to use the same authenticated value that the
client will use (pass potentialSession into the prefetch/queryKey instead of
hardcoding false), and adjust ListingsSection/useListings to accept and use that
potentialSession prop (or alternatively have ListingsSection call useListings
with potentialSession rather than usePrivy) so the queryKey used during
dehydrate/hydration matches the client-side key.

@vercel

vercel Bot commented Mar 23, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
earn Ready Ready Preview Apr 15, 2026 4:22am

Request Review

- Add getServerSideProps to radar, renaissance, scribes (new)
- Update getServerSideProps in breakout, cypherpunk, redacted to also
  prefetch tracks + stats via React Query dehydration
- Add BreadcrumbList + Event JSON-LD schema to all 8 hackathon pages
- mobius and talent-olympics get JSON-LD only (static data / custom gssp)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread src/pages/earn/all/index.tsx Outdated
Comment thread src/pages/earn/bounties/index.tsx Outdated
Comment thread src/pages/earn/grants/index.tsx
Comment thread src/pages/earn/hackathon/breakout.tsx Outdated
Comment thread src/pages/earn/hackathon/breakout.tsx Outdated
Comment thread src/pages/earn/projects/index.tsx Outdated
Remove JSON.parse(JSON.stringify(...)) workaround from listing and
hackathon page getServerSideProps. Use .map() with .toISOString() for
Date fields and pass dehydrate(queryClient) directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/pages/earn/hackathon/cypherpunk.tsx`:
- Line 363: Update the anchor HTML string that currently contains
target="_blank" (the string starting with 'Check out <a
href="https://www.colosseum.com/cypherpunk/resources" target="_blank">...') to
include rel="noopener noreferrer" before any sanitization or rendering step so
the link becomes <a href="..." target="_blank" rel="noopener noreferrer">...,
ensuring you modify the string at its declaration in the Cypherpunk FAQ/FAQ
content block.
- Around line 415-472: The SSR prefetchQuery handlers for queryKey ['tracks',
slug] and ['stats', slug] are calling prisma.bounties directly and returning
shapes that may differ from the client-side API routes (/api/hackathon/{slug}
and /api/hackathon/public-stats/), risking hydration mismatches; fix by making
the server-side prefetch use the same data path and shape as the client: either
call the same API endpoints from the SSR (invoke the internal handlers used by
/api/hackathon/{slug} and /api/hackathon/public-stats/) or extract a shared
data-fetching function used by both the API routes and these prefetchQuery
blocks so queryClient.prefetchQuery (for ['tracks', slug] and ['stats', slug])
receives identical fields, filters, and value conversions (e.g., usdValue,
dates) as the client endpoints (ensure hackathon, hackathon.id, and returned
fields match exactly).

In `@src/pages/earn/hackathon/radar.tsx`:
- Around line 78-90: The Event JSON-LD is missing startDate and endDate so add
them to the object: reference the existing startDate constant for the
"startDate" property and the deadline constant for the "endDate" property inside
the JSON-LD block (the object that has '@type': 'Event' in the render of the
Radar component). Ensure the property names are startDate and endDate (ISO
string values are already available via the startDate and deadline constants).
- Around line 196-276: Wrap the entire getServerSideProps implementation (the
async body that calls prisma.hackathon.findUnique and performs
queryClient.prefetchQuery) in a try/catch around the code using QueryClient,
prisma.hackathon.findUnique and the prefetchQuery queryFns; on error catch and
log it, then return a safe fallback props object (e.g., dehydratedState:
dehydrate(new QueryClient()) or minimal stats/tracks defaults) so the page
doesn’t throw a 500 and client-side fetches can still run; ensure the catch
specifically references getServerSideProps/prisma.hackathon.findUnique so the
unguarded DB call is protected.
- Around line 74-77: Update the breadcrumb and route base paths to include the
missing "/earn/" prefix: change the breadcrumb entry in
src/pages/earn/hackathon/radar.tsx (the array item { name: 'Hackathons', url:
'/hackathon/all/' }) to '/earn/hackathon/all/' and similarly update the same
breadcrumb in the other hackathon pages (breakout.tsx, talent-olympics.tsx,
scribes.tsx, renaissance.tsx, redacted.tsx, mobius.tsx, cypherpunk.tsx); also
update HackathonSection.tsx by changing basePath = '/hackathon/all' to basePath
= '/earn/hackathon/all' so all links and canonical routes consistently include
the /earn/ prefix.

In `@src/pages/earn/hackathon/renaissance.tsx`:
- Around line 134-213: Wrap the entire getServerSideProps implementation in a
try/catch: catch any errors thrown by prisma.hackathon.findUnique and the two
queryClient.prefetchQuery calls, log the error, and return safe fallback props
(e.g., dehydratedState: dehydrate(new QueryClient()) or minimal stats/empty
tracks) so the page doesn't 500; keep the existing logic inside the try block
and ensure the catch returns the fallback props to allow client-side recovery.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 474b961a-7cf2-45c3-bca9-0600de114e6c

📥 Commits

Reviewing files that changed from the base of the PR and between 31f43bd and 3e7ce3c.

📒 Files selected for processing (12)
  • src/pages/earn/all/index.tsx
  • src/pages/earn/bounties/index.tsx
  • src/pages/earn/grants/index.tsx
  • src/pages/earn/hackathon/breakout.tsx
  • src/pages/earn/hackathon/cypherpunk.tsx
  • src/pages/earn/hackathon/mobius.tsx
  • src/pages/earn/hackathon/radar.tsx
  • src/pages/earn/hackathon/redacted.tsx
  • src/pages/earn/hackathon/renaissance.tsx
  • src/pages/earn/hackathon/scribes.tsx
  • src/pages/earn/hackathon/talent-olympics.tsx
  • src/pages/earn/projects/index.tsx
🚧 Files skipped from review as they are similar to previous changes (9)
  • src/pages/earn/hackathon/mobius.tsx
  • src/pages/earn/hackathon/scribes.tsx
  • src/pages/earn/bounties/index.tsx
  • src/pages/earn/all/index.tsx
  • src/pages/earn/hackathon/redacted.tsx
  • src/pages/earn/hackathon/breakout.tsx
  • src/pages/earn/hackathon/talent-olympics.tsx
  • src/pages/earn/projects/index.tsx
  • src/pages/earn/grants/index.tsx

Comment thread src/pages/earn/hackathon/cypherpunk.tsx Outdated
question: 'Where can I find developer resources for my project?',
answer:
'Check out <a href="https://www.colosseum.com/cypherpunk/resources" target="_blank">Colosseums Developer Resources page</a>. Youll find documentation, tools, tutorials, and everything you need to build on Solana.',
'Check out <a href="https://www.colosseum.com/cypherpunk/resources" target="_blank">Colosseum\'s Developer Resources page</a>. You\'ll find documentation, tools, tutorials, and everything you need to build on Solana.',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add rel to the new-tab FAQ link.

This anchor uses target="_blank" without rel="noopener noreferrer". Please add it before sanitizing the HTML string.

Suggested fix
-      'Check out <a href="https://www.colosseum.com/cypherpunk/resources" target="_blank">Colosseum\'s Developer Resources page</a>. You\'ll find documentation, tools, tutorials, and everything you need to build on Solana.',
+      'Check out <a href="https://www.colosseum.com/cypherpunk/resources" target="_blank" rel="noopener noreferrer">Colosseum\'s Developer Resources page</a>. You\'ll find documentation, tools, tutorials, and everything you need to build on Solana.',
📝 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
'Check out <a href="https://www.colosseum.com/cypherpunk/resources" target="_blank">Colosseum\'s Developer Resources page</a>. You\'ll find documentation, tools, tutorials, and everything you need to build on Solana.',
'Check out <a href="https://www.colosseum.com/cypherpunk/resources" target="_blank" rel="noopener noreferrer">Colosseum\'s Developer Resources page</a>. You\'ll find documentation, tools, tutorials, and everything you need to build on Solana.',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/earn/hackathon/cypherpunk.tsx` at line 363, Update the anchor HTML
string that currently contains target="_blank" (the string starting with 'Check
out <a href="https://www.colosseum.com/cypherpunk/resources"
target="_blank">...') to include rel="noopener noreferrer" before any
sanitization or rendering step so the link becomes <a href="..." target="_blank"
rel="noopener noreferrer">..., ensuring you modify the string at its declaration
in the Cypherpunk FAQ/FAQ content block.

Comment on lines +415 to +472
queryClient.prefetchQuery({
queryKey: ['tracks', slug],
queryFn: async () => {
const result = await prisma.bounties.findMany({
where: { Hackathon: { slug }, isPublished: true },
select: {
title: true,
token: true,
rewardAmount: true,
slug: true,
sponsor: {
select: {
name: true,
slug: true,
logo: true,
isVerified: true,
chapter: { select: { id: true } },
},
},
},
orderBy: { usdValue: 'desc' },
});
return result;
},
}),
queryClient.prefetchQuery({
queryKey: ['stats', slug],
queryFn: async () => {
const [totalListings, totalRewardAmount] = await Promise.all([
prisma.bounties.count({
where: {
hackathonId: hackathon.id,
isActive: true,
isArchived: false,
status: 'OPEN',
isPublished: true,
},
}),
prisma.bounties.aggregate({
_sum: { usdValue: true },
where: {
hackathonId: hackathon.id,
isActive: true,
isArchived: false,
status: 'OPEN',
isPublished: true,
},
}),
]);
return {
totalRewardAmount: totalRewardAmount._sum.usdValue || 0,
totalListings,
deadline: hackathon.deadline?.toISOString() ?? null,
startDate: hackathon.startDate?.toISOString() ?? null,
announceDate: hackathon.announceDate?.toISOString() ?? null,
};
},
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

query_file="$(fd -i 'hackathon.*' | head -n1)"

echo "== Query helper definitions =="
rg -n -A20 -B5 'trackDataQuery|statsDataQuery' "$query_file"

echo
echo "== Manual SSR prefetch =="
sed -n '414,472p' src/pages/earn/hackathon/cypherpunk.tsx

Repository: SuperteamDAO/earn

Length of output: 92


🏁 Script executed:

# Find the cypherpunk.tsx file and check its imports
find . -name "cypherpunk.tsx" -type f | head -5

Repository: SuperteamDAO/earn

Length of output: 103


🏁 Script executed:

# Search for trackDataQuery and statsDataQuery across the entire codebase
rg "trackDataQuery|statsDataQuery" --type ts --type tsx -n

Repository: SuperteamDAO/earn

Length of output: 89


🏁 Script executed:

# Also search for these in any files that might define queries
fd -type f -name "*query*" -o -name "*queries*" | head -20

Repository: SuperteamDAO/earn

Length of output: 232


🏁 Script executed:

# Search for trackDataQuery and statsDataQuery without type flags
rg "trackDataQuery|statsDataQuery" -n

Repository: SuperteamDAO/earn

Length of output: 2261


🏁 Script executed:

# Find query-related files
fd "query" -type f | head -20

Repository: SuperteamDAO/earn

Length of output: 232


🏁 Script executed:

# Read the imports section of cypherpunk.tsx
head -100 ./src/pages/earn/hackathon/cypherpunk.tsx | grep -A 50 "^import"

Repository: SuperteamDAO/earn

Length of output: 3539


🏁 Script executed:

# Read the query definitions from src/queries/hackathon.ts
cat -n src/queries/hackathon.ts

Repository: SuperteamDAO/earn

Length of output: 1228


🏁 Script executed:

# Read lines 415-472 of cypherpunk.tsx to see the SSR code
sed -n '410,475p' src/pages/earn/hackathon/cypherpunk.tsx

Repository: SuperteamDAO/earn

Length of output: 1943


These SSR prefetch queries use different data sources than the client queries, risking hydration mismatches.

The cache keys match (['tracks', slug] and ['stats', slug]), but the implementations diverge critically:

  • Client calls API endpoints (/api/hackathon/{slug} and /api/hackathon/public-stats/)
  • SSR queries Prisma directly with different filters and field selections

Since the cache keys are identical, React Query will treat the SSR-populated data as the answer to the client-side queries. If the API endpoints return different data shapes or values than the direct Prisma queries, the page will hydrate with stale/mismatched data, causing layout shifts and potential bugs after mount.

Extract shared query logic or ensure both code paths use identical data sources.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/earn/hackathon/cypherpunk.tsx` around lines 415 - 472, The SSR
prefetchQuery handlers for queryKey ['tracks', slug] and ['stats', slug] are
calling prisma.bounties directly and returning shapes that may differ from the
client-side API routes (/api/hackathon/{slug} and /api/hackathon/public-stats/),
risking hydration mismatches; fix by making the server-side prefetch use the
same data path and shape as the client: either call the same API endpoints from
the SSR (invoke the internal handlers used by /api/hackathon/{slug} and
/api/hackathon/public-stats/) or extract a shared data-fetching function used by
both the API routes and these prefetchQuery blocks so queryClient.prefetchQuery
(for ['tracks', slug] and ['stats', slug]) receives identical fields, filters,
and value conversions (e.g., usdValue, dates) as the client endpoints (ensure
hackathon, hackathon.id, and returned fields match exactly).

Comment on lines +74 to +77
{ name: 'Home', url: '/' },
{ name: 'Hackathons', url: '/hackathon/all/' },
{ name: 'Radar' },
]),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if /hackathon/all or /earn/hackathon/all is the actual Next.js route
fd --type f -e tsx -e ts -p "pages" "all" | grep -i hackathon

Repository: SuperteamDAO/earn

Length of output: 152


🏁 Script executed:

#!/bin/bash
# List the pages directory structure to understand routing
find src/pages -type f \( -name "*.tsx" -o -name "*.ts" \) | head -30

Repository: SuperteamDAO/earn

Length of output: 1152


🏁 Script executed:

#!/bin/bash
# Search for files in the pages/hackathon directory
ls -la src/pages/earn/hackathon/ 2>/dev/null || ls -la src/pages/hackathon/ 2>/dev/null || echo "Checking directory structure..."
fd "hackathon" src/pages --type d

Repository: SuperteamDAO/earn

Length of output: 842


🏁 Script executed:

#!/bin/bash
# Find references to /hackathon/all route
rg "hackathon/all" --type ts --type tsx -A 2 -B 2

Repository: SuperteamDAO/earn

Length of output: 89


🏁 Script executed:

#!/bin/bash
# Read the breadcrumb section in radar.tsx around lines 74-77
sed -n '70,80p' src/pages/earn/hackathon/radar.tsx

Repository: SuperteamDAO/earn

Length of output: 418


🏁 Script executed:

#!/bin/bash
# Search for all references to /hackathon/all in the codebase
rg "/hackathon/all" --type-add 'web:*.tsx' -t web -A 1 -B 1

Repository: SuperteamDAO/earn

Length of output: 2857


🏁 Script executed:

#!/bin/bash
# Check if all.tsx has the same issue
sed -n '1,100p' src/pages/earn/hackathon/all.tsx | grep -A 5 "Hackathons"

Repository: SuperteamDAO/earn

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Check the HackathonSection component for the full context
sed -n '1,50p' src/features/hackathon/components/HackathonSection.tsx

Repository: SuperteamDAO/earn

Length of output: 1458


🏁 Script executed:

#!/bin/bash
# Find where basePath is used in HackathonSection
rg "basePath.*=.*'/hackathon/all'" -A 3 -B 1

Repository: SuperteamDAO/earn

Length of output: 550


Fix breadcrumb and link URLs for hackathon pages to include /earn/ path prefix.

The breadcrumb at line 75 uses /hackathon/all/ but should be /earn/hackathon/all/ to match the actual route. The page's canonical URL correctly uses /earn/hackathon/all/, and the file location (src/pages/earn/hackathon/radar.tsx) confirms the route structure requires the /earn/ prefix.

This same issue exists in other hackathon detail pages (breakout.tsx, talent-olympics.tsx, scribes.tsx, renaissance.tsx, redacted.tsx, mobius.tsx, cypherpunk.tsx) and in HackathonSection.tsx where basePath = '/hackathon/all' should be '/earn/hackathon/all'.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/earn/hackathon/radar.tsx` around lines 74 - 77, Update the
breadcrumb and route base paths to include the missing "/earn/" prefix: change
the breadcrumb entry in src/pages/earn/hackathon/radar.tsx (the array item {
name: 'Hackathons', url: '/hackathon/all/' }) to '/earn/hackathon/all/' and
similarly update the same breadcrumb in the other hackathon pages (breakout.tsx,
talent-olympics.tsx, scribes.tsx, renaissance.tsx, redacted.tsx, mobius.tsx,
cypherpunk.tsx); also update HackathonSection.tsx by changing basePath =
'/hackathon/all' to basePath = '/earn/hackathon/all' so all links and canonical
routes consistently include the /earn/ prefix.

Comment on lines +78 to +90
{
'@context': 'https://schema.org',
'@type': 'Event',
name: 'Radar — Solana Global Hackathon',
description:
'Submit to exclusive bounty tracks of the Radar Solana Global Hackathon on Superteam Earn. Find development, design, and content tracks to earn crypto prizes.',
url: 'https://superteam.fun/earn/hackathon/radar/',
organizer: {
'@type': 'Organization',
name: 'Superteam Earn',
url: 'https://superteam.fun/',
},
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Event JSON-LD is missing startDate and endDate, making it ineligible for Google's Event rich results.

Google requires startDate (among name and location) for an Event to be eligible for enhanced search results; endDate is recommended. Both values (startDate and deadline) are already defined as constants in the component scope (lines 20–21) and are directly reachable inside the JSX.

✏️ Proposed fix
 {
   '@context': 'https://schema.org',
   '@type': 'Event',
   name: 'Radar — Solana Global Hackathon',
   description:
     'Submit to exclusive bounty tracks of the Radar Solana Global Hackathon on Superteam Earn. Find development, design, and content tracks to earn crypto prizes.',
+  startDate: startDate,
+  endDate: deadline,
   url: 'https://superteam.fun/earn/hackathon/radar/',
   organizer: {
     '@type': 'Organization',
     name: 'Superteam Earn',
     url: 'https://superteam.fun/',
   },
 },
📝 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
{
'@context': 'https://schema.org',
'@type': 'Event',
name: 'Radar — Solana Global Hackathon',
description:
'Submit to exclusive bounty tracks of the Radar Solana Global Hackathon on Superteam Earn. Find development, design, and content tracks to earn crypto prizes.',
url: 'https://superteam.fun/earn/hackathon/radar/',
organizer: {
'@type': 'Organization',
name: 'Superteam Earn',
url: 'https://superteam.fun/',
},
},
{
'@context': 'https://schema.org',
'@type': 'Event',
name: 'Radar — Solana Global Hackathon',
description:
'Submit to exclusive bounty tracks of the Radar Solana Global Hackathon on Superteam Earn. Find development, design, and content tracks to earn crypto prizes.',
startDate: startDate,
endDate: deadline,
url: 'https://superteam.fun/earn/hackathon/radar/',
organizer: {
'@type': 'Organization',
name: 'Superteam Earn',
url: 'https://superteam.fun/',
},
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/earn/hackathon/radar.tsx` around lines 78 - 90, The Event JSON-LD
is missing startDate and endDate so add them to the object: reference the
existing startDate constant for the "startDate" property and the deadline
constant for the "endDate" property inside the JSON-LD block (the object that
has '@type': 'Event' in the render of the Radar component). Ensure the property
names are startDate and endDate (ISO string values are already available via the
startDate and deadline constants).

Comment on lines +196 to +276
export const getServerSideProps: GetServerSideProps = async () => {
const slug = 'radar';
const queryClient = new QueryClient();

const hackathon = await prisma.hackathon.findUnique({ where: { slug } });

await Promise.all([
queryClient.prefetchQuery({
queryKey: ['tracks', slug],
queryFn: async () => {
const result = await prisma.bounties.findMany({
where: { Hackathon: { slug }, isPublished: true },
select: {
title: true,
token: true,
rewardAmount: true,
slug: true,
sponsor: {
select: {
name: true,
slug: true,
logo: true,
isVerified: true,
chapter: { select: { id: true } },
},
},
},
orderBy: { usdValue: 'desc' },
});
return result;
},
}),
queryClient.prefetchQuery({
queryKey: ['stats', slug],
queryFn: async () => {
if (!hackathon)
return {
totalRewardAmount: 0,
totalListings: 0,
deadline: null,
startDate: null,
announceDate: null,
};
const [totalListings, totalRewardAmount] = await Promise.all([
prisma.bounties.count({
where: {
hackathonId: hackathon.id,
isActive: true,
isArchived: false,
status: 'OPEN',
isPublished: true,
},
}),
prisma.bounties.aggregate({
_sum: { usdValue: true },
where: {
hackathonId: hackathon.id,
isActive: true,
isArchived: false,
status: 'OPEN',
isPublished: true,
},
}),
]);
return {
totalRewardAmount: totalRewardAmount._sum.usdValue || 0,
totalListings,
deadline: hackathon.deadline?.toISOString() ?? null,
startDate: hackathon.startDate?.toISOString() ?? null,
announceDate: hackathon.announceDate?.toISOString() ?? null,
};
},
}),
]);

return {
props: {
dehydratedState: dehydrate(queryClient),
},
};
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Unguarded Prisma call will cause a 500 if the database is unavailable.

prisma.hackathon.findUnique at line 200 is outside any try/catch. While errors thrown inside queryClient.prefetchQuery's queryFn are silently swallowed, this outer call is not protected. A transient DB error would surface as an unhandled exception in getServerSideProps, returning a 500 to the user instead of gracefully degrading to the existing client-side fetch path.

🛡️ Proposed fix — wrap gssp body in try/catch
 export const getServerSideProps: GetServerSideProps = async () => {
   const slug = 'radar';
   const queryClient = new QueryClient();
 
+  try {
     const hackathon = await prisma.hackathon.findUnique({ where: { slug } });
 
     await Promise.all([
       queryClient.prefetchQuery({ ... }),
       queryClient.prefetchQuery({ ... }),
     ]);
+  } catch (error) {
+    console.error('[radar] SSR prefetch failed, falling back to CSR', error);
+  }
 
   return {
     props: {
       dehydratedState: dehydrate(queryClient),
     },
   };
 };
📝 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
export const getServerSideProps: GetServerSideProps = async () => {
const slug = 'radar';
const queryClient = new QueryClient();
const hackathon = await prisma.hackathon.findUnique({ where: { slug } });
await Promise.all([
queryClient.prefetchQuery({
queryKey: ['tracks', slug],
queryFn: async () => {
const result = await prisma.bounties.findMany({
where: { Hackathon: { slug }, isPublished: true },
select: {
title: true,
token: true,
rewardAmount: true,
slug: true,
sponsor: {
select: {
name: true,
slug: true,
logo: true,
isVerified: true,
chapter: { select: { id: true } },
},
},
},
orderBy: { usdValue: 'desc' },
});
return result;
},
}),
queryClient.prefetchQuery({
queryKey: ['stats', slug],
queryFn: async () => {
if (!hackathon)
return {
totalRewardAmount: 0,
totalListings: 0,
deadline: null,
startDate: null,
announceDate: null,
};
const [totalListings, totalRewardAmount] = await Promise.all([
prisma.bounties.count({
where: {
hackathonId: hackathon.id,
isActive: true,
isArchived: false,
status: 'OPEN',
isPublished: true,
},
}),
prisma.bounties.aggregate({
_sum: { usdValue: true },
where: {
hackathonId: hackathon.id,
isActive: true,
isArchived: false,
status: 'OPEN',
isPublished: true,
},
}),
]);
return {
totalRewardAmount: totalRewardAmount._sum.usdValue || 0,
totalListings,
deadline: hackathon.deadline?.toISOString() ?? null,
startDate: hackathon.startDate?.toISOString() ?? null,
announceDate: hackathon.announceDate?.toISOString() ?? null,
};
},
}),
]);
return {
props: {
dehydratedState: dehydrate(queryClient),
},
};
};
export const getServerSideProps: GetServerSideProps = async () => {
const slug = 'radar';
const queryClient = new QueryClient();
try {
const hackathon = await prisma.hackathon.findUnique({ where: { slug } });
await Promise.all([
queryClient.prefetchQuery({
queryKey: ['tracks', slug],
queryFn: async () => {
const result = await prisma.bounties.findMany({
where: { Hackathon: { slug }, isPublished: true },
select: {
title: true,
token: true,
rewardAmount: true,
slug: true,
sponsor: {
select: {
name: true,
slug: true,
logo: true,
isVerified: true,
chapter: { select: { id: true } },
},
},
},
orderBy: { usdValue: 'desc' },
});
return result;
},
}),
queryClient.prefetchQuery({
queryKey: ['stats', slug],
queryFn: async () => {
if (!hackathon)
return {
totalRewardAmount: 0,
totalListings: 0,
deadline: null,
startDate: null,
announceDate: null,
};
const [totalListings, totalRewardAmount] = await Promise.all([
prisma.bounties.count({
where: {
hackathonId: hackathon.id,
isActive: true,
isArchived: false,
status: 'OPEN',
isPublished: true,
},
}),
prisma.bounties.aggregate({
_sum: { usdValue: true },
where: {
hackathonId: hackathon.id,
isActive: true,
isArchived: false,
status: 'OPEN',
isPublished: true,
},
}),
]);
return {
totalRewardAmount: totalRewardAmount._sum.usdValue || 0,
totalListings,
deadline: hackathon.deadline?.toISOString() ?? null,
startDate: hackathon.startDate?.toISOString() ?? null,
announceDate: hackathon.announceDate?.toISOString() ?? null,
};
},
}),
]);
} catch (error) {
console.error('[radar] SSR prefetch failed, falling back to CSR', error);
}
return {
props: {
dehydratedState: dehydrate(queryClient),
},
};
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/earn/hackathon/radar.tsx` around lines 196 - 276, Wrap the entire
getServerSideProps implementation (the async body that calls
prisma.hackathon.findUnique and performs queryClient.prefetchQuery) in a
try/catch around the code using QueryClient, prisma.hackathon.findUnique and the
prefetchQuery queryFns; on error catch and log it, then return a safe fallback
props object (e.g., dehydratedState: dehydrate(new QueryClient()) or minimal
stats/tracks defaults) so the page doesn’t throw a 500 and client-side fetches
can still run; ensure the catch specifically references
getServerSideProps/prisma.hackathon.findUnique so the unguarded DB call is
protected.

Comment on lines +134 to +213
export const getServerSideProps: GetServerSideProps = async () => {
const slug = 'renaissance';
const queryClient = new QueryClient();

const hackathon = await prisma.hackathon.findUnique({ where: { slug } });

await Promise.all([
queryClient.prefetchQuery({
queryKey: ['tracks', slug],
queryFn: async () => {
const result = await prisma.bounties.findMany({
where: { Hackathon: { slug }, isPublished: true },
select: {
title: true,
token: true,
rewardAmount: true,
slug: true,
sponsor: {
select: {
name: true,
slug: true,
logo: true,
isVerified: true,
chapter: { select: { id: true } },
},
},
},
orderBy: { usdValue: 'desc' },
});
return result;
},
}),
queryClient.prefetchQuery({
queryKey: ['stats', slug],
queryFn: async () => {
if (!hackathon)
return {
totalRewardAmount: 0,
totalListings: 0,
deadline: null,
startDate: null,
announceDate: null,
};
const [totalListings, totalRewardAmount] = await Promise.all([
prisma.bounties.count({
where: {
hackathonId: hackathon.id,
isActive: true,
isArchived: false,
status: 'OPEN',
isPublished: true,
},
}),
prisma.bounties.aggregate({
_sum: { usdValue: true },
where: {
hackathonId: hackathon.id,
isActive: true,
isArchived: false,
status: 'OPEN',
isPublished: true,
},
}),
]);
return {
totalRewardAmount: totalRewardAmount._sum.usdValue || 0,
totalListings,
deadline: hackathon.deadline?.toISOString() ?? null,
startDate: hackathon.startDate?.toISOString() ?? null,
announceDate: hackathon.announceDate?.toISOString() ?? null,
};
},
}),
]);

return {
props: {
dehydratedState: dehydrate(queryClient),
},
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing error handling in getServerSideProps – unhandled Prisma errors cause 500s

There is no try/catch around any of the Prisma calls. A transient DB error or connection failure will throw, crashing the entire page into a 500 response. Wrap the body in a try/catch and fall back to empty props so the page still renders (client-side fetches will recover):

🛡️ Proposed fix
 export const getServerSideProps: GetServerSideProps = async () => {
   const slug = 'renaissance';
   const queryClient = new QueryClient();
 
+  try {
     const hackathon = await prisma.hackathon.findUnique({ where: { slug } });
 
     await Promise.all([
       queryClient.prefetchQuery({ ... }),
       queryClient.prefetchQuery({ ... }),
     ]);
+  } catch (_err) {
+    // Prefetch failed; page renders with client-side fetches as fallback
+  }
 
   return {
     props: {
       dehydratedState: dehydrate(queryClient),
     },
   };
 };
📝 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
export const getServerSideProps: GetServerSideProps = async () => {
const slug = 'renaissance';
const queryClient = new QueryClient();
const hackathon = await prisma.hackathon.findUnique({ where: { slug } });
await Promise.all([
queryClient.prefetchQuery({
queryKey: ['tracks', slug],
queryFn: async () => {
const result = await prisma.bounties.findMany({
where: { Hackathon: { slug }, isPublished: true },
select: {
title: true,
token: true,
rewardAmount: true,
slug: true,
sponsor: {
select: {
name: true,
slug: true,
logo: true,
isVerified: true,
chapter: { select: { id: true } },
},
},
},
orderBy: { usdValue: 'desc' },
});
return result;
},
}),
queryClient.prefetchQuery({
queryKey: ['stats', slug],
queryFn: async () => {
if (!hackathon)
return {
totalRewardAmount: 0,
totalListings: 0,
deadline: null,
startDate: null,
announceDate: null,
};
const [totalListings, totalRewardAmount] = await Promise.all([
prisma.bounties.count({
where: {
hackathonId: hackathon.id,
isActive: true,
isArchived: false,
status: 'OPEN',
isPublished: true,
},
}),
prisma.bounties.aggregate({
_sum: { usdValue: true },
where: {
hackathonId: hackathon.id,
isActive: true,
isArchived: false,
status: 'OPEN',
isPublished: true,
},
}),
]);
return {
totalRewardAmount: totalRewardAmount._sum.usdValue || 0,
totalListings,
deadline: hackathon.deadline?.toISOString() ?? null,
startDate: hackathon.startDate?.toISOString() ?? null,
announceDate: hackathon.announceDate?.toISOString() ?? null,
};
},
}),
]);
return {
props: {
dehydratedState: dehydrate(queryClient),
},
};
export const getServerSideProps: GetServerSideProps = async () => {
const slug = 'renaissance';
const queryClient = new QueryClient();
try {
const hackathon = await prisma.hackathon.findUnique({ where: { slug } });
await Promise.all([
queryClient.prefetchQuery({
queryKey: ['tracks', slug],
queryFn: async () => {
const result = await prisma.bounties.findMany({
where: { Hackathon: { slug }, isPublished: true },
select: {
title: true,
token: true,
rewardAmount: true,
slug: true,
sponsor: {
select: {
name: true,
slug: true,
logo: true,
isVerified: true,
chapter: { select: { id: true } },
},
},
},
orderBy: { usdValue: 'desc' },
});
return result;
},
}),
queryClient.prefetchQuery({
queryKey: ['stats', slug],
queryFn: async () => {
if (!hackathon)
return {
totalRewardAmount: 0,
totalListings: 0,
deadline: null,
startDate: null,
announceDate: null,
};
const [totalListings, totalRewardAmount] = await Promise.all([
prisma.bounties.count({
where: {
hackathonId: hackathon.id,
isActive: true,
isArchived: false,
status: 'OPEN',
isPublished: true,
},
}),
prisma.bounties.aggregate({
_sum: { usdValue: true },
where: {
hackathonId: hackathon.id,
isActive: true,
isArchived: false,
status: 'OPEN',
isPublished: true,
},
}),
]);
return {
totalRewardAmount: totalRewardAmount._sum.usdValue || 0,
totalListings,
deadline: hackathon.deadline?.toISOString() ?? null,
startDate: hackathon.startDate?.toISOString() ?? null,
announceDate: hackathon.announceDate?.toISOString() ?? null,
};
},
}),
]);
} catch (_err) {
// Prefetch failed; page renders with client-side fetches as fallback
}
return {
props: {
dehydratedState: dehydrate(queryClient),
},
};
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/earn/hackathon/renaissance.tsx` around lines 134 - 213, Wrap the
entire getServerSideProps implementation in a try/catch: catch any errors thrown
by prisma.hackathon.findUnique and the two queryClient.prefetchQuery calls, log
the error, and return safe fallback props (e.g., dehydratedState: dehydrate(new
QueryClient()) or minimal stats/empty tracks) so the page doesn't 500; keep the
existing logic inside the try block and ensure the catch returns the fallback
props to allow client-side recovery.

@devanshk404 devanshk404 marked this pull request as draft May 8, 2026 06:06
devanshk404 and others added 5 commits May 8, 2026 12:37
…e divergence

useServerTimeSync calls Date.now() which differs between SSR and hydration,
causing structural mismatches in the FEATURED badge and green dot conditionals.
Added mounted state so structural time-dependent elements only render after
hydration; suppressHydrationWarning for attribute/text-level differences.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ation

suppressHydrationWarning on the <a> element caused React 18 to skip
reconciling its children during hydration, producing a fresh-render that
mismatched the server DOM (shown as `+ <div>` structural error). The
mounted-state fix already handles the only real structural conditionals
(FEATURED badge, green dot); suppressHydrationWarning on the Link was
causing more harm than good.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…<a> nesting

The sponsor <Link> was nested inside the card <Link>, both rendering as <a>.
Browsers auto-correct invalid <a>-inside-<a> HTML, which React cannot hydrate
against, causing the recurring hydration mismatch error. Replaced inner link
with <span role="link"> using router.push on click/Enter keydown.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ages

- Normalize useGrants queryKey optional params to null (region/sponsor/skill
  ?? null) so SSR prefetch key matches client key and hydration doesn't miss
- Use explicit null instead of undefined in grants/index.tsx prefetch queryKey
- Remove debug console.log statements from cypherpunk.tsx
- Add rel="noopener noreferrer" to target="_blank" anchor in cypherpunk FAQ
- Add startDate and endDate to radar.tsx Event JSON-LD schema
- Wrap radar.tsx and renaissance.tsx getServerSideProps in try/catch so a DB
  error returns a safe fallback instead of a 500
- Fix breadcrumb URL /hackathon/all/ → /earn/hackathon/all/ across all 8
  hackathon pages (breakout, cypherpunk, mobius, radar, redacted, renaissance,
  scribes, talent-olympics)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
User.location is nullable; calling .toLowerCase() on null threw
TypeError: Cannot read properties of null (reading 'toLowerCase')
causing the page to 500. Optional chaining fixes it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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