Skip to content

fix SSR of program marketplace pages#4042

Open
general-adhoc wants to merge 8 commits into
program-marketplace-categoriesfrom
marketplace-ssr-followup
Open

fix SSR of program marketplace pages#4042
general-adhoc wants to merge 8 commits into
program-marketplace-categoriesfrom
marketplace-ssr-followup

Conversation

@general-adhoc

@general-adhoc general-adhoc commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

Release Notes

  • New Features

    • Added dedicated marketplace pages for browsing all programs and specific categories
    • Launched individual program detail pages with optimized SEO metadata
  • Performance Improvements

    • Implemented server-side caching for faster marketplace page loads
    • Optimized initial data handling to reduce client-side data fetching
  • UI Updates

    • Simplified program card design with improved category and rewards display

@vercel

vercel Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
dub Ready Ready Preview Jun 16, 2026 11:08pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

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

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ad5bd998-373a-4c9a-9dcd-394782468e2e

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

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR migrates the external marketplace from an app.dub.co subdomain rewrite to dedicated top-level /marketplace Next.js routes. Middleware is updated to pass requests through instead of rewriting. The old catch-all page and MarketplaceExternalRouter are deleted. New pages are added for home, all-programs, category, and program detail. MarketplaceExternalListPage gains server-side data fetching with unstable_cache, and the SWR client is seeded with that SSR data. ProgramCard is converted from a client component to a server-renderable component.

Changes

Marketplace Route Migration and SSR Optimization

Layer / File(s) Summary
Middleware: pass-through for /marketplace routes
apps/web/middleware.ts, apps/web/lib/middleware/app.ts
Removes fullPath-based rewrites to app.dub.co for /marketplace paths in both root middleware and AppMiddleware, replacing them with NextResponse.next().
Route structure: delete catch-all, add dedicated pages and analytics
apps/web/app/app.dub.co/marketplace/[[...slug]]/page.tsx, apps/web/app/marketplace/layout.tsx, apps/web/app/marketplace/page.tsx, apps/web/app/marketplace/all/page.tsx, apps/web/app/marketplace/c/[categorySlug]/page.tsx, apps/web/app/marketplace/[programSlug]/page.tsx, apps/web/ui/program-marketplace/external/marketplace-external-router.tsx
Deletes the old catch-all page and MarketplaceExternalRouter. Adds dedicated home, all-programs, category, and program detail pages with ISR/dynamic config, generateMetadata, and generateStaticParams. Adds DubAnalytics to the shared layout.
Server-side list data fetching with unstable_cache
apps/web/ui/program-marketplace/external/marketplace-external-list-page.tsx, apps/web/lib/marketplace/parse-public-marketplace-query.ts
Converts MarketplaceExternalListPage to async, accepting searchParams. Adds an unstable_cache-wrapped helper that concurrently fetches programs, total count, and filter counts, keyed by parsed query params with 1-hour revalidation. Fixes fixedCategory assignment to use nullish coalescing.
SWR client hydration with SSR initialData
apps/web/ui/program-marketplace/external/marketplace-external-list-page-client.tsx
Adds the MarketplaceExternalListInitialData type and initialData prop. Derives shouldUseInitialData to seed SWR fallbackData and skip revalidateOnMount. Introduces resolvedFilterCounts so the filter sidebar renders immediately from SSR data.
ProgramCard: convert to server component
apps/web/ui/program-marketplace/program-card.tsx
Removes use client and client-only imports. Makes the card a single <Link>. Adds getProgramCategoryLabel, hiddenCategoryLabels for overflow badge tooltips, and MarketplaceProgramCardRewards as a non-interactive inline rewards renderer.

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant Middleware as Next.js Middleware
  participant ListPage as MarketplaceExternalListPage (Server)
  participant Cache as unstable_cache
  participant DB as Program Fetchers
  participant Client as MarketplaceExternalListPageClient (SWR)

  Browser->>Middleware: GET /marketplace/all?search=foo
  Middleware-->>Browser: NextResponse.next()
  Browser->>ListPage: render
  ListPage->>Cache: parsePublicMarketplaceQuery(searchParams) → key
  Cache->>DB: fetch programs + totalCount + filterCounts
  DB-->>Cache: results
  Cache-->>ListPage: { programs, totalCount, filterCounts }
  ListPage->>Client: initialData + params
  Client->>Client: shouldUseInitialData check
  Client-->>Browser: render with SSR fallbackData (no loading flash)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • steven-tey
  • devkiran

Poem

🐇 Hippity hop, a new path is born,
No more rewrites through subdomains worn!
The marketplace pages now stand on their own,
With SSR data seeds freshly sown.
The card hops server-side, light as a breeze —
This rabbit approves, with the greatest of ease! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main objective of the PR—fixing SSR of program marketplace pages through architectural restructuring.
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
  • Commit unit tests in branch marketplace-ssr-followup

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.

@general-adhoc general-adhoc marked this pull request as ready for review June 16, 2026 06:39
@general-adhoc

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review, any regressions, bugs, performance issues?

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch PR details
gh pr view 4042 --repo dubinc/dub --json title,body,files,additions,deletions,state,headRefName

echo "---FILES CHANGED---"
gh pr diff 4042 --repo dubinc/dub

Length of output: 31502


🏁 Script executed:

#!/bin/bash
echo "=== middleware.ts - check if fullPath is still referenced ==="
cat apps/web/middleware.ts

echo ""
echo "=== MarketplaceExternalShell title prop ==="
cat apps/web/ui/program-marketplace/external/marketplace-external-shell.tsx 2>/dev/null || echo "NOT FOUND"

echo ""
echo "=== slugToCategory in urls.ts ==="
fd -e ts "urls.ts" apps/web/ui/program-marketplace --exec cat {}

echo ""
echo "=== getPublicNetworkProgramFilterCounts signature ==="
fd -e ts "get-public-network-program-filter-counts.ts" apps/web/lib --exec cat {}

echo ""
echo "=== getPublicNetworkPrograms / count signatures ==="
fd -e ts "get-public-network-programs.ts" apps/web/lib --exec cat {}

echo ""
echo "=== parsePublicMarketplaceQuery full ==="
cat apps/web/lib/marketplace/parse-public-marketplace-query.ts

echo ""
echo "=== MarketplaceExternalProgramPage - does it still exist? ==="
fd -e tsx "marketplace-external-program-page.tsx" apps/web --exec cat {}

echo ""
echo "=== MarketplaceExternalHomePage ==="
fd -e tsx "marketplace-external-home-page.tsx" apps/web --exec cat {}

echo ""
echo "=== getMarketplaceProgramHref / getMarketplaceCanonicalUrl ==="
fd -e ts "urls.ts" apps/web/ui/program-marketplace --exec grep -n "getMarketplaceProgramHref\|getMarketplaceCanonicalUrl\|slugToCategory\|getMarketplacePathFromSlug" {}

echo ""
echo "=== app.ts middleware - full file ==="
cat apps/web/lib/middleware/app.ts

echo ""
echo "=== Check if fullPath is used elsewhere in middleware.ts ==="
grep -n "fullPath" apps/web/middleware.ts

echo ""
echo "=== generateMarketplaceProgramStaticParams ==="
fd -e tsx -e ts "marketplace-program-page.tsx" apps/web --exec cat {}

echo ""
echo "=== ProgramCategory variant prop ==="
fd -e tsx "program-category.tsx" apps/web --exec cat {}

echo ""
echo "=== MarketplaceExternalFilterSidebar - does it handle empty arrays? ==="
fd -e tsx "marketplace-external-filter-sidebar.tsx" apps/web --exec cat {}

echo ""
echo "=== Check reexports/imports of MarketplaceExternalRouter - any remaining refs? ==="
rg -rn "marketplace-external-router\|MarketplaceExternalRouter" apps/web/

Length of output: 37048



@general-adhoc Here's the full review of PR #4042:


Overall Assessment

The restructuring is well-designed. Moving from a single [[...slug]] catch-all to explicit route segments (distinct page.tsx files per route) is the right call for Next.js SSR — it enables per-route revalidate/dynamic strategies, proper generateStaticParams, and removes the client-side routing component (MarketplaceExternalRouter). The shouldUseInitialData pattern to avoid redundant SWR fetches on mount is clean.

No critical bugs found. A few items worth reviewing:


🔴 Potential Issue — Visual Regression: Category Page <h1> Title

File: apps/web/ui/program-marketplace/external/marketplace-external-list-page.tsx

Previously, the category title was passed as JSX with a line break:

// Before
title={
  categoryMeta ? (
    <>
      {categoryMeta.label} partner
      <br />
      programs
    </>
  ) : undefined
}

Now it's a plain string:

// After
title={categoryMeta ? `${categoryMeta.label} partner programs` : undefined}

The <h1> in MarketplaceExternalShell uses text-[50px] and leading-[1.1]. The home page title still gets a <br /> (via the shell's default), but category pages no longer do. Depending on label length (e.g., "E-commerce" → "E-commerce partner programs"), the title may now render on a single very long line rather than wrapping intentionally. Worth a visual check on a category page.


🟡 UX Regression — Category "+N" Tooltip on Touch Devices

File: apps/web/ui/program-marketplace/program-card.tsx

The Radix <Tooltip> (styled, keyboard/mouse/screen-reader accessible) was replaced by a native title attribute:

// After
<div
  className="text-content-subtle -ml-1.5 flex size-6 ..."
  title={hiddenCategoryLabels.join(", ")}
>
  +{program.categories.length - 1}
</div>

Native title attributes don't fire on touch devices. Users on mobile can never see the hidden category names. The comment in the file correctly explains why (<Tooltip> uses a Radix trigger that's invalid HTML inside an <a>), so this is an intentional trade-off — but consider replacing the entire card with a non-<a> container + useRouter push if hover-card interactivity is important, or accepting this as a known limitation.


🟡 Minor — filterCounts Briefly Shows Initial Data After Filter Change

File: apps/web/ui/program-marketplace/external/marketplace-external-list-page-client.tsx

// filterCounts always uses initialData.filterCounts as fallbackData
fallbackData: initialData.filterCounts,

When the user changes a filter (URL changes), shouldUseInitialData becomes false and revalidateOnMount becomes true, triggering a fresh SWR fetch. During that fetch window, filterCounts will show initialData.filterCounts (the server-rendered state) — not the previous fetched counts — because fallbackData takes precedence over stale SWR cache. keepPreviousData: true would help here, but it's only useful after the first successful non-fallback fetch. The practical impact is a brief sidebar count flicker when switching from a filtered URL back to the initial URL state. Low severity.


🟡 Minor — unstable_cache Wrapping Functions Already Using react.cache

File: apps/web/ui/program-marketplace/external/marketplace-external-list-page.tsx

const getMarketplaceListData = unstable_cache(
  async (params) => {
    const [programs, totalCount, filterCounts] = await Promise.all([
      getPublicNetworkPrograms(params),    // uses react.cache internally
      getPublicNetworkProgramsCount(params), // uses react.cache internally
      getPublicNetworkProgramFilterCounts(params), // uses react.cache internally
    ]);
    ...
  },
  ["marketplace-external-list"],
  { revalidate: 3600 },
);

This is correct — react.cache deduplicates within a single request; unstable_cache persists across requests. No conflict. ✅


✅ Items Verified as Correct

Item Status
fullPath removed from middleware.ts destructuring Not used elsewhere — clean ✅
Next.js route precedence (all/, c/[slug] before [programSlug]) Static routes take priority over dynamic segments ✅
generateStaticParams mapping { slug: [program.slug] }{ programSlug: slug[0] } Correct ✅
slugToCategory excluding reserved slugs ("all", "popular", "c") Correctly returns null for reserved slugs ✅
/marketplace/popular fallback path App middleware redirects first; if leaked, MarketplaceExternalProgramPage redirects to home ✅
shouldUseInitialData param comparison Both server and client use parsePublicMarketplaceQuery; defaults are consistent ✅
No remaining references to deleted MarketplaceExternalRouter Confirmed clean removal ✅
DubAnalytics cookieOptions.domain VERCEL === "1" is set on all Vercel deployments ✅
MarketplaceExternalProgramPage still exists and is properly wired
ProgramCategory variant="default" without onClick/href Renders <div> — valid inside <Link>

Summary

No regressions that would break core functionality. The two items most worth double-checking before merge are:

  1. The category page <h1> line break — quick visual check on a category URL
  2. The +N categories tooltip — acceptable for now if the team is OK with mobile users not seeing hidden category names
✅ Action performed

Full review finished.

@general-adhoc

general-adhoc commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai The category page <h1> break change was intentional as some versions of the page were 1-line and other versions (like category pages) were multiple lines causing CLS on switching pages

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@general-adhoc That makes perfect sense — keeping a consistent single-line layout across all page variants prevents the CLS caused by the <h1> height changing between the home/all pages (no <br />) and category pages (with <br />). Thanks for the context, withdrawing that concern.


✏️ Learnings added
Learnt from: general-adhoc
URL: https://github.com/dubinc/dub/pull/4042

Timestamp: 2026-06-16T16:06:30.348Z
Learning: In the Dub marketplace (`apps/web/ui/program-marketplace/external/marketplace-external-list-page.tsx`), the `<h1>` title for category pages was intentionally changed from a JSX multi-line format (using `<br />`) to a plain template-string (e.g., `${categoryMeta.label} partner programs`) to prevent Cumulative Layout Shift (CLS) during client-side navigation between page variants that had inconsistent title heights (1-line vs multi-line).

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@general-adhoc That makes complete sense — normalizing all titles to a single line eliminates the height jump between pages during client-side navigation, which is exactly the right call for CLS. Withdrawing that concern.


✏️ Learnings added
Learnt from: general-adhoc
URL: https://github.com/dubinc/dub/pull/4042

Timestamp: 2026-06-16T16:06:37.275Z
Learning: In the Dub marketplace external pages (apps/web/ui/program-marketplace/external/marketplace-external-list-page.tsx), the `<h1>` title in MarketplaceExternalShell intentionally uses a plain template string (e.g., `${categoryMeta.label} partner programs`) without JSX line breaks. The previous multi-line approach with `<br />` caused CLS (Cumulative Layout Shift) when navigating between pages that had different title heights (1-line vs multi-line). The single-line approach is the intentional fix.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment thread apps/web/app/sitemap.ts
addedToMarketplaceAt: {
not: null,
},
groups: {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

unifying detail page and sitemap public marketplace eligibility with the following locations:

  • get-public-network-programs.ts
  • get-public-network-program-filter-counts.ts
  • api/network/programs/count/route.ts

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