fix: improve SEO for listing and hackathon pages#1349
fix: improve SEO for listing and hackathon pages#1349devanshk404 wants to merge 2 commits intoSuperteamDAO:mainfrom
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. |
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 11 minutes and 3 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (8)
WalkthroughThis PR introduces server-side React Query prefetching across multiple earn pages, adds Changes
Sequence DiagramsequenceDiagram
participant Browser as Browser (Client)
participant Server as Next.js Server
participant QueryClient as QueryClient
participant Database as Prisma / Database
participant HB as HydrationBoundary
Browser->>Server: Request page (e.g., /earn/all)
activate Server
Server->>QueryClient: new QueryClient()
activate QueryClient
Server->>Database: buildListingQuery() + prisma.bounties.findMany()
activate Database
Database-->>Server: Bounty records
deactivate Database
Server->>Server: reorderFeaturedOngoing(results)
Server->>QueryClient: prefetchQuery(queryKey, transformedData)
QueryClient-->>Server: Query cached
deactivate QueryClient
Server->>Server: dehydrateQueryClient()
Server-->>Browser: HTML + dehydratedState prop
deactivate Server
activate Browser
Browser->>HB: Render with dehydratedState
activate HB
HB->>HB: Hydrate queries from dehydratedState
HB-->>Browser: Interactive page with cached data
deactivate HB
deactivate Browser
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 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>
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