feat: SSR homepage listings for SEO with ISR#1343
Conversation
Fetch default listings in getServerSideProps for unauthenticated visitors so Googlebot receives real listing content in the HTML payload instead of skeleton loaders. - Thread initialListings prop from page → ListingsSection → useListings - Use React Query initialData + initialDataUpdatedAt to seed the default view without triggering a redundant network request - Skip SSR data for returning users (potentialSession) to preserve personalized For You behaviour Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix nested <a> tags (invalid HTML) in ListingCard using stretched link pattern - Outer Link changed to div with absolutely-positioned Link overlay - Inner sponsor Link is now a valid non-nested anchor - Add ssrTimestamp prop to stabilize time-based rendering between server and client - Fix missing await on buildListingQuery in getServerSideProps causing all DB records to be fetched with no filters or limit (2389 listings instead of 10) - Switch from getServerSideProps to getStaticProps with ISR (revalidate: 60s) reducing response time from ~3.6s to ~50ms in production via CDN caching - Add scripts/test-ssr.mjs to verify SSR is working and report performance 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. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThis PR integrates SSR data into the ChangesSSR Integration & Component Updates
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 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: 5
🧹 Nitpick comments (3)
src/features/listings/hooks/useListings.ts (1)
35-35: AlignListingsParams.initialListingswith explicit undefined typing.For consistency with project TS contracts, avoid optional property syntax here as well.
♻️ Suggested change
interface ListingsParams { context: ListingContext; tab: ListingTab; category: string; status?: ListingStatus; sortBy?: ListingSortOption; order?: OrderDirection; region?: string; sponsor?: string; skill?: string; authenticated?: boolean; - initialListings?: Listing[] | null; + readonly initialListings: readonly Listing[] | null | undefined; }As per coding guidelines: "Use
property: Type | undefinedinstead ofproperty?: Type" and "Usereadonlyproperties for object types by default in TypeScript."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/listings/hooks/useListings.ts` at line 35, The ListingParams interface uses optional property syntax for initialListings; change it to an explicit undefined union and make the array readonly by replacing "initialListings?: Listing[] | null" with "initialListings: readonly Listing[] | null | undefined" (or "Listing[] | null | undefined" if readonly not desired) and update any spots using ListingParams.initialListings accordingly to accept undefined instead of relying on optionality.src/pages/earn/index.tsx (1)
3-3: Use top-levelimport typesyntax forGetStaticProps.♻️ Suggested change
-import { type GetStaticProps } from 'next'; +import type { GetStaticProps } from 'next';As per coding guidelines: "Use import type whenever you are importing a type" and "Prefer top-level
import typeover inlineimport { type ... }syntax."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/earn/index.tsx` at line 3, Replace the inline type import syntax with a top-level type import for GetStaticProps: find the import that currently reads "import { type GetStaticProps } from 'next';" and change it to use the top-level "import type" form so the symbol GetStaticProps is imported via "import type { GetStaticProps } from 'next'".src/features/listings/types/index.ts (1)
111-112: Use explicit| undefined(and readonly) for the newListingTabsPropsfields.These new props are currently optional and mutable, which diverges from the repo’s TS contract style.
♻️ Suggested change
export interface ListingTabsProps { type: ListingContext; potentialSession?: boolean; region?: string; sponsor?: string; skill?: string; category?: string; defaultTab?: 'bounties' | 'projects'; - initialListings?: Listing[] | null; - ssrTimestamp?: number; + readonly initialListings: readonly Listing[] | null | undefined; + readonly ssrTimestamp: number | undefined; }As per coding guidelines: "Use
property: Type | undefinedinstead ofproperty?: Type" and "Usereadonlyproperties for object types by default in TypeScript."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/listings/types/index.ts` around lines 111 - 112, Update the ListingTabsProps fields to follow the project's TS conventions: replace optional mutable declarations with explicit | undefined and make object/array types readonly; specifically change initialListings?: Listing[] | null to readonly initialListings: readonly Listing[] | null | undefined and change ssrTimestamp?: number to readonly ssrTimestamp: number | undefined so both properties are explicit about undefined and are readonly where applicable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@scripts/test-ssr.mjs`:
- Around line 17-30: After the fetch completes (the response stored in res),
fail fast on non-2xx responses by checking res.ok (or res.status) and throwing
or exiting with a non-zero status before computing ttfb/total and printing "Page
loaded"; include the actual status and optionally res.statusText or a short
res.text() in the error message to avoid false-positive SSR passes. Update the
block around the fetch/response handling (references: res, start, ttfb, total,
html) to bail out when res.ok is false and only proceed to compute timings and
log success for 2xx responses.
- Around line 86-87: Remove the bogus dbTime calculation and its unused
variable: delete the line that assigns dbTime (const dbTime = Date.now() - start
- (Date.now() - start);) and ensure no other code references dbTime; leave the
existing performance log (console.log(`\n⚡ Performance: Page response took
${Date.now() - start}ms total`)) unchanged.
In `@src/features/listings/components/ListingCard.tsx`:
- Around line 105-110: The overlay Link element currently uses className
"absolute inset-0 z-0 ..." while the content wrapper uses "relative z-[1] ...",
causing the overlay to sit behind interactive content and breaking
stretched-click; update the stacking so the Link sits above content (for
example, increase the Link's z-index to a higher value like z-10 or lower the
content wrapper's z to z-0) for both occurrences (the Link with
href={`/earn/listing/${slug}`} and the corresponding wrapper div with "relative
z-[1] ...") so clicks hit the overlay as intended.
- Around line 71-73: ssrTimestamp is being used as a permanent "now" which
freezes relative-time UI after hydration; change to initialize a stateful now
value from ssrTimestamp (fallback to serverTime()) and update it on the client
with a timer so deadlines and relative times tick. Concretely: replace the const
now = ssrTimestamp ?? serverTime() with a useState(() => ssrTimestamp ??
serverTime()) named e.g. now, add a useEffect that starts an interval (e.g. 1s
or 1min depending on granularity) to setNow(serverTime() or Date.now()) and
clears the interval on unmount, and update all places using now (including the
isBeforeDeadline calculation and the code around lines 77-87) to read from this
stateful now so the UI updates after hydration.
In `@src/pages/earn/index.tsx`:
- Around line 182-191: The catch block in getStaticProps is silently swallowing
errors (the block that returns props with potentialSession: false /
initialListings: null / ssrTimestamp: null); update that catch to capture the
exception (e) and either rethrow it to surface build/ISR failures or at minimum
log the full error before returning so failures are visible in production
logs—modify the catch in getStaticProps to use the caught error, call your app
logger or console.error with a descriptive message including the error, or
rethrow the error to fail the build.
---
Nitpick comments:
In `@src/features/listings/hooks/useListings.ts`:
- Line 35: The ListingParams interface uses optional property syntax for
initialListings; change it to an explicit undefined union and make the array
readonly by replacing "initialListings?: Listing[] | null" with
"initialListings: readonly Listing[] | null | undefined" (or "Listing[] | null |
undefined" if readonly not desired) and update any spots using
ListingParams.initialListings accordingly to accept undefined instead of relying
on optionality.
In `@src/features/listings/types/index.ts`:
- Around line 111-112: Update the ListingTabsProps fields to follow the
project's TS conventions: replace optional mutable declarations with explicit |
undefined and make object/array types readonly; specifically change
initialListings?: Listing[] | null to readonly initialListings: readonly
Listing[] | null | undefined and change ssrTimestamp?: number to readonly
ssrTimestamp: number | undefined so both properties are explicit about undefined
and are readonly where applicable.
In `@src/pages/earn/index.tsx`:
- Line 3: Replace the inline type import syntax with a top-level type import for
GetStaticProps: find the import that currently reads "import { type
GetStaticProps } from 'next';" and change it to use the top-level "import type"
form so the symbol GetStaticProps is imported via "import type { GetStaticProps
} from 'next'".
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 45f91da2-f2b7-4da0-8e01-4f5cd5f4413a
📒 Files selected for processing (6)
scripts/test-ssr.mjssrc/features/listings/components/ListingCard.tsxsrc/features/listings/components/ListingsSection.tsxsrc/features/listings/hooks/useListings.tssrc/features/listings/types/index.tssrc/pages/earn/index.tsx
| const now = ssrTimestamp ?? serverTime(); | ||
| const isBeforeDeadline = dayjs(now).isBefore(dayjs(deadline)); | ||
|
|
There was a problem hiding this comment.
ssrTimestamp currently freezes relative-time UI after hydration.
Using ssrTimestamp as a permanent source for now prevents deadline text from updating while the user stays on the page.
🐛 Suggested change
+import { useEffect, useState } from 'react';
+
const { serverTime } = useServerTimeSync();
- const now = ssrTimestamp ?? serverTime();
+ const [hasHydrated, setHasHydrated] = useState(false);
+ useEffect(() => {
+ setHasHydrated(true);
+ }, []);
+ const now = !hasHydrated && ssrTimestamp ? ssrTimestamp : serverTime();
const isBeforeDeadline = dayjs(now).isBefore(dayjs(deadline));Also applies to: 77-87
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/listings/components/ListingCard.tsx` around lines 71 - 73,
ssrTimestamp is being used as a permanent "now" which freezes relative-time UI
after hydration; change to initialize a stateful now value from ssrTimestamp
(fallback to serverTime()) and update it on the client with a timer so deadlines
and relative times tick. Concretely: replace the const now = ssrTimestamp ??
serverTime() with a useState(() => ssrTimestamp ?? serverTime()) named e.g. now,
add a useEffect that starts an interval (e.g. 1s or 1min depending on
granularity) to setNow(serverTime() or Date.now()) and clears the interval on
unmount, and update all places using now (including the isBeforeDeadline
calculation and the code around lines 77-87) to read from this stateful now so
the UI updates after hydration.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…ment
Both generateSuperteamChaptersSchema and generateRegionalOrganizationSchema
were generating URLs like /regions/{slug}/ which return 404s. The actual
pages live at /earn/regions/{slug}/, invalidating all regional chapter
Organization entities in Google's eyes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fetch totalUsers and totalSponsors in getServerSideProps and pass them as props so Googlebot sees real numbers in the HTML instead of 0+. Pre-populate React Query cache with initialData so the sidebar banner also avoids a 0+ flash on first client render. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/pages/earn/index.tsx (1)
192-203:⚠️ Potential issue | 🟠 MajorDon’t mask ISR failures with zeroed homepage data.
This catch still hides the regeneration failure, and the hard-coded
0totals now have a visible side effect: they override the new banner fallback path and can publishReach 0+/Join 0+ othersuntil the next successful rebuild. For ISR, rethrowing after logging is usually safer because Next keeps serving the last good page.🛠️ Proposed fix
- } catch { - return { - props: { - potentialSession: false, - initialListings: null, - ssrTimestamp: null, - totalUsers: 0, - totalSponsors: 0, - }, - revalidate: 60, - }; + } catch (error) { + console.error('getStaticProps(/earn) failed', error); + throw error; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/earn/index.tsx` around lines 192 - 203, The catch block in getStaticProps currently swallows ISR failures and returns zeroed homepage data (potentialSession, initialListings, ssrTimestamp, totalUsers, totalSponsors), which can publish misleading "0+" banners; change the catch to accept an error (e.g., catch (err)), log the error with your logger (processLogger.error or console.error) including context, and rethrow the error instead of returning hard-coded props so Next serves the last-good page/ISR behavior rather than publishing zeros.
🧹 Nitpick comments (3)
src/features/home/components/Banner/SponsorBanner.tsx (1)
132-137: Resolve the sponsor count once before the conditional render.The same fallback expression is evaluated twice here, and the second usage needs
data!.totalSponsors!to satisfy TypeScript. Hoisting it to a local keeps the guard and displayed value in sync and removes the non-null assertions.♻️ Proposed fix
export function HomeSponsorBanner({ totalUsers, totalSponsors, }: HomeSponsorBannerProps) { const { data } = useQuery(sponsorCountQuery); + const resolvedTotalSponsors = totalSponsors ?? data?.totalSponsors; + return ( @@ - {(totalSponsors ?? data?.totalSponsors) != null && ( + {resolvedTotalSponsors != null && ( <p className="relative ml-[0.6875rem] text-[0.8rem] text-black md:text-[0.875rem]"> Join{' '} - {roundToNearestTenth( - totalSponsors ?? data!.totalSponsors!, - )?.toLocaleString('en-us')} + {roundToNearestTenth(resolvedTotalSponsors).toLocaleString( + 'en-us', + )} + others </p> )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/home/components/Banner/SponsorBanner.tsx` around lines 132 - 137, Hoist the resolved sponsor count into a local constant (e.g., resolvedSponsorCount) before the JSX conditional so both the guard and rendered value use the same expression; compute it from totalSponsors ?? data?.totalSponsors, then use resolvedSponsorCount in the conditional and pass it to roundToNearestTenth(resolvedSponsorCount)?.toLocaleString('en-us') to remove the duplicate fallback and eliminate the data!.totalSponsors! non-null assertions in SponsorBanner.tsx.src/features/home/components/Banner/index.tsx (1)
21-24: Use the repo’s explicit-undefinedprop shape fortotalSponsors.Line 23 introduces the new prop with
?:, which makes omission indistinguishable from an intentionalundefined. This repo prefers explicitType | undefinedfor new TypeScript properties.♻️ Proposed fix
interface BannerCarouselProps { readonly totalUsers?: number | null; - readonly totalSponsors?: number | null; + readonly totalSponsors: number | null | undefined; }As per coding guidelines, "Use
property: Type | undefinedinstead ofproperty?: Typefor TypeScript type definitions to force explicit property passing and prevent bugs from accidentally omitting required properties."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/home/components/Banner/index.tsx` around lines 21 - 24, Update the BannerCarouselProps type to use the repo's explicit-undefined style for the totalSponsors prop: replace the optional modifier on totalSponsors with an explicit union including undefined (i.e., totalSponsors: number | null | undefined) so omission is distinguishable from an intentional undefined; locate and edit the BannerCarouselProps interface in src/features/home/components/Banner/index.tsx (the interface and the totalSponsors symbol) and ensure any callers pass the prop explicitly or handle the explicit undefined type.src/pages/earn/index.tsx (1)
3-3: Prefer top-levelimport typefor these type-only imports.Both changed imports use the inline
import { type ... }form. This codebase standard prefers top-levelimport type.♻️ Proposed fix
-import { type GetStaticProps } from 'next'; +import type { GetStaticProps } from 'next'; @@ -import { type Listing } from '@/features/listings/types'; +import type { Listing } from '@/features/listings/types';As per coding guidelines, "Use import type whenever you are importing a type" and "Prefer top-level
import typeover inlineimport { type ... }syntax."Also applies to: 28-28
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/earn/index.tsx` at line 3, Replace inline type imports like "import { type GetStaticProps } from 'next'" with top-level type imports using "import type { GetStaticProps } from 'next'"; scan the file for any other occurrences of the inline "import { type ... }" form (e.g., the other type-only import referenced at line 28) and convert them to "import type { ... }" so all type-only imports use the top-level import type syntax.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/pages/earn/index.tsx`:
- Around line 192-203: The catch block in getStaticProps currently swallows ISR
failures and returns zeroed homepage data (potentialSession, initialListings,
ssrTimestamp, totalUsers, totalSponsors), which can publish misleading "0+"
banners; change the catch to accept an error (e.g., catch (err)), log the error
with your logger (processLogger.error or console.error) including context, and
rethrow the error instead of returning hard-coded props so Next serves the
last-good page/ISR behavior rather than publishing zeros.
---
Nitpick comments:
In `@src/features/home/components/Banner/index.tsx`:
- Around line 21-24: Update the BannerCarouselProps type to use the repo's
explicit-undefined style for the totalSponsors prop: replace the optional
modifier on totalSponsors with an explicit union including undefined (i.e.,
totalSponsors: number | null | undefined) so omission is distinguishable from an
intentional undefined; locate and edit the BannerCarouselProps interface in
src/features/home/components/Banner/index.tsx (the interface and the
totalSponsors symbol) and ensure any callers pass the prop explicitly or handle
the explicit undefined type.
In `@src/features/home/components/Banner/SponsorBanner.tsx`:
- Around line 132-137: Hoist the resolved sponsor count into a local constant
(e.g., resolvedSponsorCount) before the JSX conditional so both the guard and
rendered value use the same expression; compute it from totalSponsors ??
data?.totalSponsors, then use resolvedSponsorCount in the conditional and pass
it to roundToNearestTenth(resolvedSponsorCount)?.toLocaleString('en-us') to
remove the duplicate fallback and eliminate the data!.totalSponsors! non-null
assertions in SponsorBanner.tsx.
In `@src/pages/earn/index.tsx`:
- Line 3: Replace inline type imports like "import { type GetStaticProps } from
'next'" with top-level type imports using "import type { GetStaticProps } from
'next'"; scan the file for any other occurrences of the inline "import { type
... }" form (e.g., the other type-only import referenced at line 28) and convert
them to "import type { ... }" so all type-only imports use the top-level import
type syntax.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: e1ef6289-9ff6-4e8e-9a1a-b16b0b526d88
📒 Files selected for processing (5)
src/features/home/components/Banner/SponsorBanner.tsxsrc/features/home/components/Banner/index.tsxsrc/features/listings/components/ListingCard.tsxsrc/pages/earn/index.tsxsrc/utils/json-ld.ts
✅ Files skipped from review due to trivial changes (1)
- src/utils/json-ld.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/features/listings/components/ListingCard.tsx
- Fix navbar BottomBar avatar rendering href="/earn/t/undefined" during SSR when no user session; guard href and setColor with username check - Fix chapters icons/link serialization error on / by replacing undefined with null in getServerSideProps; update SuperteamInput type to accept null - Remove unused useTokenLookup call from ListingCard that caused ReferenceError on /earn - Remove invalid browserToTerminal option from next.config.ts logging config Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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/navbar/components/BottomBar.tsx (1)
163-168:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winInconsistent guard condition for
pointerEventsvshref.
hreffalls back to'#'whenuser?.usernameis absent, butpointerEventsis set to'auto'wheneveruseris truthy. A user object can exist without a populatedusername(e.g., partially-registered account), leaving a clickable link that navigates to'#'.🐛 Proposed fix
<Link href={user?.username ? `/earn/t/${user.username}` : '#'} style={{ ...linkStyle, - pointerEvents: user ? 'auto' : 'none', + pointerEvents: user?.username ? 'auto' : 'none', }} >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/navbar/components/BottomBar.tsx` around lines 163 - 168, BottomBar's Link uses different guards: href checks user?.username but pointerEvents checks user, allowing clicks when user exists but username is missing; change the pointerEvents condition to match the href guard (use user?.username) and ensure any interactive behavior (e.g., onClick/navigation) is disabled when username is falsy so the link isn't clickable for partially-registered users; update the Link element's style logic (referencing linkStyle and pointerEvents) and any click handlers to use user?.username as the single source of truth.
♻️ Duplicate comments (2)
src/features/listings/components/ListingCard.tsx (2)
70-76:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
ssrTimestampstill freezes the time-based UI after hydration.When SSR provides
ssrTimestamp, every render keeps using that fixed value, sodeadlineTextand the green status dot stop advancing while the user stays on the page.🐛 Suggested fix
+import { useEffect, useState } from 'react'; + const { serverTime } = useServerTimeSync(); - const now = ssrTimestamp ?? serverTime(); + const [now, setNow] = useState(() => ssrTimestamp ?? serverTime()); + + useEffect(() => { + const intervalId = window.setInterval(() => { + setNow(serverTime()); + }, 60_000); + + return () => { + window.clearInterval(intervalId); + }; + }, [serverTime]); + const isBeforeDeadline = dayjs(now).isBefore(dayjs(deadline));Also applies to: 214-216
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/listings/components/ListingCard.tsx` around lines 70 - 76, ssrTimestamp is freezing time-based UI because now is always set to ssrTimestamp; change the logic to start with ssrTimestamp for the initial render but switch to client time after hydration by using a local state (e.g., nowState) initialized to ssrTimestamp ?? serverTime() and updated in a useEffect (or interval) to serverTime()/Date.now() so the derived values isBeforeDeadline, targetDate, and formattedDeadline (which feed deadlineText and the green status dot) advance on the client; apply the same pattern where ssrTimestamp is used (also around the code referenced at lines ~214-216) and ensure timeAgoShort receives the updated nowState rather than the static ssrTimestamp.
102-107:⚠️ Potential issue | 🟠 Major | ⚡ Quick winThe stretched-link overlay is still underneath the card content.
With the overlay at
z-0and the wrapper atz-[1], clicks on the visible content won't reach the listing link. The overlay needs to sit above the passive content, while the sponsor link stays above the overlay.🐛 Suggested fix
<Link href={`/earn/listing/${slug}`} - className="absolute inset-0 z-0 rounded-md" - aria-label={title ?? ''} + className="absolute inset-0 z-10 rounded-md" + aria-label={title ? `Open listing: ${title}` : 'Open listing'} /> - <div className="relative z-[1] flex w-full items-center justify-between"> + <div className="relative z-0 flex w-full items-center justify-between"> @@ <Link href={`/earn/s/${sponsor?.slug}`} - className="flex w-min items-center gap-1 hover:underline" + className="relative z-20 flex w-min items-center gap-1 hover:underline" >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/listings/components/ListingCard.tsx` around lines 102 - 107, The stretched Link overlay (the Link element with href={`/earn/listing/${slug}`} and className including z-0) is underneath the card content (the surrounding div with className "relative z-[1]"), preventing clicks; change the stacking so the overlay sits above the passive content but below the sponsor link: raise the overlay z-index (e.g., remove z-0 and give it a higher z like z-10 or z-[10]) and lower the card content z (or remove z-[1]), then ensure the sponsor link element has an even higher z (e.g., z-20) so it remains clickable above the overlay; update the className values on the Link, the wrapper div (the one with "relative z-[1]"), and the sponsor link to reflect this ordering.
🧹 Nitpick comments (1)
src/features/listings/components/ListingCard.tsx (1)
44-50: ⚡ Quick winMake
ssrTimestampexplicit in the props contract.This prop is part of the SSR/hydration path, so making it optional lets an intermediate caller drop it without a type error. Prefer
ssrTimestamp: number | undefinedhere (and includenullonly if that is part of the real upstream contract).As per coding guidelines, "Use
property: Type | undefinedinstead ofproperty?: Typefor TypeScript type definitions to force explicit property passing and prevent bugs from accidentally omitting required properties."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/listings/components/ListingCard.tsx` around lines 44 - 50, The ListingCard prop type should declare ssrTimestamp explicitly as possibly undefined instead of using the optional shorthand; update the ListingCard props signature (the destructured params for the ListingCard component) to use ssrTimestamp: number | undefined rather than ssrTimestamp?: number, and ensure any callers that intentionally omit the value pass undefined explicitly (or update their call sites to provide a number) so the property is always explicitly present per the coding guideline.
🤖 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/features/navbar/components/BottomBar.tsx`:
- Around line 155-158: The call to setColor in BottomBar passes an empty string
when user?.username is falsy, which makes setColor compare router.asPath to
`/earn` and produce a false-positive active state; change the call so it never
passes '' — either inline the conditional and only call setColor when
user?.username exists, or pass a safe non-matching placeholder (e.g. a sentinel
string that cannot equal any real route) instead of '' so
setColor(user?.username ? `/earn/t/${user.username}/` : '<NO_MATCH>',
router.asPath) avoids matching `/earn` for unauthenticated users.
In `@src/pages/index.tsx`:
- Around line 24-25: The properties currently declared as optional (icons?:
string | null; link?: string | null) are actually always present after
normalization in getServerSideProps (using "?? null"), so remove the optional
"?" and declare them as icons: string | null and link: string | null in the
relevant type/interface (also update the other occurrences mentioned around
lines 33–34); ensure any component/consumer signatures use the non-optional form
to match the runtime shape produced by getServerSideProps.
---
Outside diff comments:
In `@src/features/navbar/components/BottomBar.tsx`:
- Around line 163-168: BottomBar's Link uses different guards: href checks
user?.username but pointerEvents checks user, allowing clicks when user exists
but username is missing; change the pointerEvents condition to match the href
guard (use user?.username) and ensure any interactive behavior (e.g.,
onClick/navigation) is disabled when username is falsy so the link isn't
clickable for partially-registered users; update the Link element's style logic
(referencing linkStyle and pointerEvents) and any click handlers to use
user?.username as the single source of truth.
---
Duplicate comments:
In `@src/features/listings/components/ListingCard.tsx`:
- Around line 70-76: ssrTimestamp is freezing time-based UI because now is
always set to ssrTimestamp; change the logic to start with ssrTimestamp for the
initial render but switch to client time after hydration by using a local state
(e.g., nowState) initialized to ssrTimestamp ?? serverTime() and updated in a
useEffect (or interval) to serverTime()/Date.now() so the derived values
isBeforeDeadline, targetDate, and formattedDeadline (which feed deadlineText and
the green status dot) advance on the client; apply the same pattern where
ssrTimestamp is used (also around the code referenced at lines ~214-216) and
ensure timeAgoShort receives the updated nowState rather than the static
ssrTimestamp.
- Around line 102-107: The stretched Link overlay (the Link element with
href={`/earn/listing/${slug}`} and className including z-0) is underneath the
card content (the surrounding div with className "relative z-[1]"), preventing
clicks; change the stacking so the overlay sits above the passive content but
below the sponsor link: raise the overlay z-index (e.g., remove z-0 and give it
a higher z like z-10 or z-[10]) and lower the card content z (or remove z-[1]),
then ensure the sponsor link element has an even higher z (e.g., z-20) so it
remains clickable above the overlay; update the className values on the Link,
the wrapper div (the one with "relative z-[1]"), and the sponsor link to reflect
this ordering.
---
Nitpick comments:
In `@src/features/listings/components/ListingCard.tsx`:
- Around line 44-50: The ListingCard prop type should declare ssrTimestamp
explicitly as possibly undefined instead of using the optional shorthand; update
the ListingCard props signature (the destructured params for the ListingCard
component) to use ssrTimestamp: number | undefined rather than ssrTimestamp?:
number, and ensure any callers that intentionally omit the value pass undefined
explicitly (or update their call sites to provide a number) so the property is
always explicitly present per the coding guideline.
🪄 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: 4c5c9e86-a3f2-4652-b152-6e53f3a7c180
📒 Files selected for processing (5)
next.config.tssrc/features/listings/components/ListingCard.tsxsrc/features/navbar/components/BottomBar.tsxsrc/pages/index.tsxsrc/utils/json-ld.ts
💤 Files with no reviewable changes (1)
- next.config.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/utils/json-ld.ts
Keep getStaticProps (ISR) over getServerSideProps from main; merge all four props (initialListings, ssrTimestamp, totalUsers, totalSponsors). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Resolve merge conflict: drop ProIntroDialog (removed upstream)
- Log errors in getStaticProps catch block instead of silently swallowing
- Fix ssrTimestamp freezing relative-time UI after hydration in ListingCard
- Fix overlay link z-index so card clicks hit the listing link correctly
- Fix setColor('') false-positive active state in BottomBar for unauthenticated users
- Remove broken dbTime calc in test-ssr.mjs; add fail-fast on non-2xx
- Remove optional ? from always-present icons/link fields in pages/index.tsx
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
<a>tags (invalid HTML) inListingCardgetServerSidePropstogetStaticPropswith ISR (revalidate: 60s) for ~50ms response times via CDN vs ~1.5s per-request DB queriesawaitonbuildListingQuerycaused all DB records to be fetched with no filters or limitChanges
src/pages/earn/index.tsxgetServerSidePropswithgetStaticProps+revalidate: 60ssrTimestampto props to stabilize time-based renderingawaitonbuildListingQuery(was fetching all listings with no filters/limit)src/features/listings/components/ListingCard.tsx<a>tag issue (invalid HTML) using the stretched link pattern<Link>→<div>with absolutely-positioned<Link>overlay (z-0)relative z-[1]div, sitting above the stretched link<Link>is now a valid non-nested anchorssrTimestampprop for consistent time calculations between server and clientsrc/features/listings/types/index.ts+ListingsSection.tsxssrTimestamp?: numbertoListingTabsPropsand threads it through toListingCardscripts/test-ssr.mjsnode scripts/test-ssr.mjs)Test plan
node scripts/test-ssr.mjs— should show listings found in raw HTML and response timeCtrl+U) on/earn— listing titles should be present in HTML🤖 Generated with Claude Code
Summary by CodeRabbit
Performance
Bug Fixes
New Features