Skip to content

feat: SSR homepage listings for SEO with ISR#1343

Open
devanshk404 wants to merge 10 commits into
SuperteamDAO:mainfrom
devanshk404:feat/ssr-homepage-listings
Open

feat: SSR homepage listings for SEO with ISR#1343
devanshk404 wants to merge 10 commits into
SuperteamDAO:mainfrom
devanshk404:feat/ssr-homepage-listings

Conversation

@devanshk404

@devanshk404 devanshk404 commented Mar 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Pre-renders homepage listing cards on the server so search engines see full content without JavaScript
  • Fixes hydration errors caused by nested <a> tags (invalid HTML) in ListingCard
  • Switches from getServerSideProps to getStaticProps with ISR (revalidate: 60s) for ~50ms response times via CDN vs ~1.5s per-request DB queries
  • Fixes critical bug where missing await on buildListingQuery caused all DB records to be fetched with no filters or limit

Changes

src/pages/earn/index.tsx

  • Replaced getServerSideProps with getStaticProps + revalidate: 60
  • Added ssrTimestamp to props to stabilize time-based rendering
  • Fixed missing await on buildListingQuery (was fetching all listings with no filters/limit)

src/features/listings/components/ListingCard.tsx

  • Fixed nested <a> tag issue (invalid HTML) using the stretched link pattern
    • Outer <Link><div> with absolutely-positioned <Link> overlay (z-0)
    • Content moved into relative z-[1] div, sitting above the stretched link
    • Inner sponsor <Link> is now a valid non-nested anchor
  • Accepts ssrTimestamp prop for consistent time calculations between server and client

src/features/listings/types/index.ts + ListingsSection.tsx

  • Added ssrTimestamp?: number to ListingTabsProps and threads it through to ListingCard

scripts/test-ssr.mjs

  • Test script to verify SSR is working and report performance (node scripts/test-ssr.mjs)

Test plan

  • Run node scripts/test-ssr.mjs — should show listings found in raw HTML and response time
  • View page source (Ctrl+U) on /earn — listing titles should be present in HTML
  • Disable JavaScript in browser — listings should still be visible
  • Verify no hydration errors in browser console
  • Confirm listing cards are clickable and sponsor links open correctly

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Performance

    • Migrated /earn page to static generation with pre-loaded listings for faster initial load times
    • Improved deadline timing consistency across listing cards
  • Bug Fixes

    • Fixed avatar link display in bottom bar when user not authenticated
    • Corrected regional URL paths in schema metadata
  • New Features

    • Added automated health checks for server-side rendering on /earn page

Review Change Stack

devanshk404 and others added 3 commits March 18, 2026 02:11
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>
@vercel

vercel Bot commented Mar 18, 2026

Copy link
Copy Markdown

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

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Mar 18, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR integrates SSR data into the /earn page by refactoring ListingCard to accept server-rendered timestamps, correcting regional organization schema URLs, and improving null-safety for optional chapter and avatar link fields across the application.

Changes

SSR Integration & Component Updates

Layer / File(s) Summary
ListingCard Component Refactor for SSR
src/features/listings/components/ListingCard.tsx
Adds optional ssrTimestamp prop, derives single now value from ssrTimestamp ?? serverTime() for consistent deadline calculations, restructures layout from outer Link to overlay Link with z-index layering, removes sponsor link event propagation, updates deadline dot condition to use precomputed isBeforeDeadline, and adjusts closing markup.
JSON-LD Regional Organization Schema
src/utils/json-ld.ts
Corrects Organization url paths in generateRegionalOrganizationSchema and generateSuperteamChaptersSchema from ${baseUrl}regions/... to ${baseUrl}earn/regions/..., and broadens internal SuperteamInput type to allow icons and link as nullable string | null.
Null Handling for Optional Chapter & Avatar Fields
src/pages/index.tsx, src/features/navbar/components/BottomBar.tsx
Updates HomePageProps chapter type definitions to use string | null instead of string | undefined for icons and link, normalizes chaptersForSchema and chaptersForGeographies mappings to use nullish coalescing operator, and fixes avatar link href and pointer-events state when user username is missing.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • scutuatua-crypto
  • a20hek

Poem

🐰 A timestamp upon the card does land,
SSR data flows through the land,
Links dance in z-indexed grace,
Nulls are handled with care and pace,
Schemas point to /earn/ with pride!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: SSR homepage listings for SEO with ISR' directly and accurately summarizes the main change: implementing server-side rendering of homepage listings with incremental static regeneration for SEO purposes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

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

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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (3)
src/features/listings/hooks/useListings.ts (1)

35-35: Align ListingsParams.initialListings with 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 | undefined instead of property?: Type" and "Use readonly properties 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-level import type syntax for GetStaticProps.

♻️ 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 type over inline import { 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 new ListingTabsProps fields.

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 | undefined instead of property?: Type" and "Use readonly properties 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

📥 Commits

Reviewing files that changed from the base of the PR and between 066cb31 and 0c7979e.

📒 Files selected for processing (6)
  • scripts/test-ssr.mjs
  • src/features/listings/components/ListingCard.tsx
  • src/features/listings/components/ListingsSection.tsx
  • src/features/listings/hooks/useListings.ts
  • src/features/listings/types/index.ts
  • src/pages/earn/index.tsx

Comment thread scripts/test-ssr.mjs
Comment thread scripts/test-ssr.mjs Outdated
Comment on lines 71 to 73
const now = ssrTimestamp ?? serverTime();
const isBeforeDeadline = dayjs(now).isBefore(dayjs(deadline));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment thread src/features/listings/components/ListingCard.tsx Outdated
Comment thread src/pages/earn/index.tsx Outdated
@vercel

vercel Bot commented Mar 23, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
earn Ready Ready Preview May 6, 2026 5:25am

Request Review

devanshk404 and others added 2 commits April 15, 2026 11:46
…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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/pages/earn/index.tsx (1)

192-203: ⚠️ Potential issue | 🟠 Major

Don’t mask ISR failures with zeroed homepage data.

This catch still hides the regeneration failure, and the hard-coded 0 totals now have a visible side effect: they override the new banner fallback path and can publish Reach 0+ / Join 0+ others until 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-undefined prop shape for totalSponsors.

Line 23 introduces the new prop with ?:, which makes omission indistinguishable from an intentional undefined. This repo prefers explicit Type | undefined for 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 | undefined instead of property?: Type for 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-level import type for these type-only imports.

Both changed imports use the inline import { type ... } form. This codebase standard prefers top-level import 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 type over inline import { 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0c7979e and cfef822.

📒 Files selected for processing (5)
  • src/features/home/components/Banner/SponsorBanner.tsx
  • src/features/home/components/Banner/index.tsx
  • src/features/listings/components/ListingCard.tsx
  • src/pages/earn/index.tsx
  • src/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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
src/features/navbar/components/BottomBar.tsx (1)

163-168: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Inconsistent guard condition for pointerEvents vs href.

href falls back to '#' when user?.username is absent, but pointerEvents is set to 'auto' whenever user is truthy. A user object can exist without a populated username (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

ssrTimestamp still freezes the time-based UI after hydration.

When SSR provides ssrTimestamp, every render keeps using that fixed value, so deadlineText and 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 win

The stretched-link overlay is still underneath the card content.

With the overlay at z-0 and the wrapper at z-[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 win

Make ssrTimestamp explicit 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 | undefined here (and include null only if that is part of the real upstream contract).

As per coding guidelines, "Use property: Type | undefined instead of property?: Type for 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

📥 Commits

Reviewing files that changed from the base of the PR and between cfef822 and a3d4e95.

📒 Files selected for processing (5)
  • next.config.ts
  • src/features/listings/components/ListingCard.tsx
  • src/features/navbar/components/BottomBar.tsx
  • src/pages/index.tsx
  • src/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

Comment thread src/features/navbar/components/BottomBar.tsx Outdated
Comment thread src/pages/index.tsx Outdated
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>
devanshk404 and others added 2 commits May 16, 2026 21:08
- 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants