fix: improve SEO for listing and hackathon pages#1349
Conversation
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>
|
@devanshk404 is attempting to deploy a commit to the Superteam Team on Vercel. A member of the Team first needs to authorize it. |
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughThis PR adds server-side React Query prefetching and dehydration to multiple earn pages, sets a 60s ChangesQuery Hooks & App Hydration
Server-Side Query Prefetching (listings / grants / projects / bounties)
Hackathon pages: SEO, JSON‑LD, and server prefetching
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 | 🟠 MajorQuery key mismatch will prevent SSR hydration from working.
The
queryKeyuses rawundefinedvalues for optional parameters (region,sponsor,skill), but the server-side prefetch ingrants/index.tsxpasses throughJSON.parse(JSON.stringify(...))which convertsundefinedtonullin 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 explicitnullvalues in queryKey for consistency.The
queryKeyusesundefinedvalues that will be converted tonullduring JSON serialization. For clarity and to match the client-side hook pattern (after the fix I suggested foruseGrants.ts), use explicitnullvalues.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 likelyall/index.tsx), differing only in thetabparameter. 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:dehydratedStatereturned but not in interface.Same issue as in
projects/index.tsx. Consider addingdehydratedStateto 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 includedehydratedStatebut it's returned fromgetServerSideProps.The
GetServerSideProps<ProjectsPageProps>generic constrains the return type, butdehydratedStateis returned without being declared in the interface. While this works at runtime (Next.js passes it to_app.tsxviapageProps), 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
📒 Files selected for processing (16)
src/features/grants/hooks/useGrants.tssrc/features/listings/hooks/useListings.tssrc/pages/_app.tsxsrc/pages/earn/all/index.tsxsrc/pages/earn/bounties/index.tsxsrc/pages/earn/grants/index.tsxsrc/pages/earn/hackathon/all.tsxsrc/pages/earn/hackathon/breakout.tsxsrc/pages/earn/hackathon/cypherpunk.tsxsrc/pages/earn/hackathon/mobius.tsxsrc/pages/earn/hackathon/radar.tsxsrc/pages/earn/hackathon/redacted.tsxsrc/pages/earn/hackathon/renaissance.tsxsrc/pages/earn/hackathon/scribes.tsxsrc/pages/earn/hackathon/talent-olympics.tsxsrc/pages/earn/projects/index.tsx
| await queryClient.prefetchQuery({ | ||
| queryKey: [ | ||
| 'listings', | ||
| 'all', | ||
| 'all', | ||
| 'All', | ||
| 'open', | ||
| 'Date', | ||
| 'asc', | ||
| null, | ||
| null, | ||
| null, | ||
| false, | ||
| ], |
There was a problem hiding this comment.
🧩 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 tsxRepository: 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 -40Repository: 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.tsxRepository: 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 -nRepository: 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.
| return { | ||
| props: { | ||
| potentialSession: cookieExists, | ||
| dehydratedState: JSON.parse(JSON.stringify(dehydrate(queryClient))), | ||
| }, | ||
| }; |
There was a problem hiding this comment.
🧩 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.tsxRepository: 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/listingsRepository: 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.tsxRepository: 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 -200Repository: 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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
- 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>
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>
There was a problem hiding this comment.
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
📒 Files selected for processing (12)
src/pages/earn/all/index.tsxsrc/pages/earn/bounties/index.tsxsrc/pages/earn/grants/index.tsxsrc/pages/earn/hackathon/breakout.tsxsrc/pages/earn/hackathon/cypherpunk.tsxsrc/pages/earn/hackathon/mobius.tsxsrc/pages/earn/hackathon/radar.tsxsrc/pages/earn/hackathon/redacted.tsxsrc/pages/earn/hackathon/renaissance.tsxsrc/pages/earn/hackathon/scribes.tsxsrc/pages/earn/hackathon/talent-olympics.tsxsrc/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
| question: 'Where can I find developer resources for my project?', | ||
| answer: | ||
| '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">Colosseum\'s Developer Resources page</a>. You\'ll find documentation, tools, tutorials, and everything you need to build on Solana.', |
There was a problem hiding this comment.
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.
| '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.
| 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, | ||
| }; | ||
| }, | ||
| }), |
There was a problem hiding this comment.
🧩 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.tsxRepository: SuperteamDAO/earn
Length of output: 92
🏁 Script executed:
# Find the cypherpunk.tsx file and check its imports
find . -name "cypherpunk.tsx" -type f | head -5Repository: SuperteamDAO/earn
Length of output: 103
🏁 Script executed:
# Search for trackDataQuery and statsDataQuery across the entire codebase
rg "trackDataQuery|statsDataQuery" --type ts --type tsx -nRepository: 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 -20Repository: SuperteamDAO/earn
Length of output: 232
🏁 Script executed:
# Search for trackDataQuery and statsDataQuery without type flags
rg "trackDataQuery|statsDataQuery" -nRepository: SuperteamDAO/earn
Length of output: 2261
🏁 Script executed:
# Find query-related files
fd "query" -type f | head -20Repository: 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.tsRepository: 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.tsxRepository: 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).
| { name: 'Home', url: '/' }, | ||
| { name: 'Hackathons', url: '/hackathon/all/' }, | ||
| { name: 'Radar' }, | ||
| ]), |
There was a problem hiding this comment.
🧩 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 hackathonRepository: 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 -30Repository: 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 dRepository: 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 2Repository: 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.tsxRepository: 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 1Repository: 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.tsxRepository: SuperteamDAO/earn
Length of output: 1458
🏁 Script executed:
#!/bin/bash
# Find where basePath is used in HackathonSection
rg "basePath.*=.*'/hackathon/all'" -A 3 -B 1Repository: 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.
| { | ||
| '@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/', | ||
| }, | ||
| }, |
There was a problem hiding this comment.
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.
| { | |
| '@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).
| 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), | ||
| }, | ||
| }; | ||
| }; |
There was a problem hiding this comment.
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.
| 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.
| 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), | ||
| }, | ||
| }; |
There was a problem hiding this comment.
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.
| 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.
…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>
Summary
<Meta>to/hackathon/all(had none); updated all 8 individual hackathon page titles to[Name] — Submission Tracks | Superteam Earnwith specific descriptions replacing the old generic copygetServerSidePropsprefetches the default view directly via Prisma, dehydrates into page props,HydrationBoundaryin_app.tsxrehydrates on client,staleTime: 60sprevents immediate re-fetchPages changed
/hackathon/all/hackathon/radar,/breakout,/cypherpunk,/mobius,/redacted,/renaissance,/scribes,/talent-olympics/earn/grantsgetServerSidePropswith Prisma prefetch/earn/all,/earn/bounties,/earn/projectsgetServerSidePropswith Prisma prefetchuseListings,useGrantsstaleTime: 60 * 1000_app.tsxHydrationBoundaryTechnical notes
undefined → nullto surviveJSON.parse/JSON.stringifyserialization through Next.js propsDateobjects serialized withJSON.parse(JSON.stringify(...))before passing throughgetServerSidePropscategory: All,status: open) is SSR'd; tab switches and pagination remain client-sideTest plan
/earn/grants,/earn/all,/earn/bounties— listing/grant cards should be visible in raw HTML/earn/grants— no/api/grantsrequest should fire on initial load (data comes from SSR)<title>tag on all/earn/hackathon/*pages matches[Name] — Submission Tracks | Superteam Earn🤖 Generated with Claude Code
Summary by CodeRabbit
Performance
SEO