Skip to content

Commit 2d550c7

Browse files
committed
fix SSR of program marketplace pages
1 parent 26ac991 commit 2d550c7

13 files changed

Lines changed: 374 additions & 205 deletions

File tree

apps/web/app/app.dub.co/marketplace/[[...slug]]/page.tsx

Lines changed: 0 additions & 86 deletions
This file was deleted.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { getNetworkProgram } from "@/lib/fetchers/get-network-program";
2+
import { MarketplaceExternalProgramPage } from "@/ui/program-marketplace/external/marketplace-external-program-page";
3+
import { generateMarketplaceProgramStaticParams } from "@/ui/program-marketplace/pages/marketplace-program-page";
4+
import {
5+
getMarketplaceCanonicalUrl,
6+
getMarketplaceProgramHref,
7+
} from "@/ui/program-marketplace/utils/urls";
8+
import { constructMetadata } from "@dub/utils";
9+
import { Metadata } from "next";
10+
11+
export const revalidate = 3600;
12+
13+
export async function generateStaticParams() {
14+
const programParams = await generateMarketplaceProgramStaticParams();
15+
16+
return programParams.map(({ slug }) => ({ programSlug: slug[0] }));
17+
}
18+
19+
export async function generateMetadata(props: {
20+
params: Promise<{ programSlug: string }>;
21+
}): Promise<Metadata> {
22+
const { programSlug } = await props.params;
23+
24+
const program = await getNetworkProgram({ slug: programSlug });
25+
26+
return constructMetadata({
27+
title: program ? program.name : "Program Details",
28+
description:
29+
program?.description ||
30+
(program
31+
? `Join the ${program.name} affiliate program on Dub's Partner Network.`
32+
: `Browse and apply to the best SaaS affiliate programs on Dub's Partner Network.`),
33+
image: program
34+
? program.marketplaceHeaderImage || program.logo || undefined
35+
: undefined,
36+
canonicalUrl: getMarketplaceCanonicalUrl(
37+
getMarketplaceProgramHref(programSlug),
38+
),
39+
});
40+
}
41+
42+
export default async function MarketplaceProgramDetailPage(props: {
43+
params: Promise<{ programSlug: string }>;
44+
}) {
45+
const { programSlug } = await props.params;
46+
47+
return <MarketplaceExternalProgramPage programSlug={programSlug} />;
48+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { MarketplaceExternalListPage } from "@/ui/program-marketplace/external/marketplace-external-list-page";
2+
import { getMarketplaceCanonicalUrl } from "@/ui/program-marketplace/utils/urls";
3+
import { constructMetadata } from "@dub/utils";
4+
import { Metadata } from "next";
5+
6+
// Rendered dynamically so the actual (filtered/sorted/paged) view is produced
7+
// server-side and hydrates in place — no fallback swap.
8+
export const dynamic = "force-dynamic";
9+
10+
export function generateMetadata(): Metadata {
11+
return constructMetadata({
12+
title: "All Programs",
13+
description: "Browse all partner programs on Dub.",
14+
canonicalUrl: getMarketplaceCanonicalUrl("/marketplace/all"),
15+
});
16+
}
17+
18+
export default async function MarketplaceAllPage(props: {
19+
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
20+
}) {
21+
const searchParams = await props.searchParams;
22+
23+
return (
24+
<MarketplaceExternalListPage slug={["all"]} searchParams={searchParams} />
25+
);
26+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { PROGRAM_CATEGORIES_MAP } from "@/lib/network/program-categories";
2+
import { MarketplaceExternalListPage } from "@/ui/program-marketplace/external/marketplace-external-list-page";
3+
import {
4+
getMarketplaceCanonicalUrl,
5+
slugToCategory,
6+
} from "@/ui/program-marketplace/utils/urls";
7+
import { constructMetadata } from "@dub/utils";
8+
import { Metadata } from "next";
9+
import { notFound } from "next/navigation";
10+
11+
// Rendered dynamically so the actual (filtered/sorted/paged) view is produced
12+
// server-side and hydrates in place — no fallback swap.
13+
export const dynamic = "force-dynamic";
14+
15+
export async function generateMetadata(props: {
16+
params: Promise<{ categorySlug: string }>;
17+
}): Promise<Metadata> {
18+
const { categorySlug } = await props.params;
19+
const category = slugToCategory(categorySlug);
20+
21+
if (!category) {
22+
return constructMetadata({ title: "Programs" });
23+
}
24+
25+
const categoryMeta = PROGRAM_CATEGORIES_MAP[category];
26+
const label = categoryMeta?.label ?? category.replaceAll("_", " ");
27+
28+
return constructMetadata({
29+
title: `${label} Programs`,
30+
description:
31+
categoryMeta?.listPageDescription ??
32+
`Partner programs in ${label.toLowerCase()}.`,
33+
canonicalUrl: getMarketplaceCanonicalUrl(`/marketplace/c/${categorySlug}`),
34+
});
35+
}
36+
37+
export default async function MarketplaceCategoryPage(props: {
38+
params: Promise<{ categorySlug: string }>;
39+
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
40+
}) {
41+
const { categorySlug } = await props.params;
42+
const searchParams = await props.searchParams;
43+
44+
const category = slugToCategory(categorySlug);
45+
46+
if (!category) {
47+
notFound();
48+
}
49+
50+
return (
51+
<MarketplaceExternalListPage
52+
slug={["c", categorySlug]}
53+
fixedCategory={category}
54+
searchParams={searchParams}
55+
/>
56+
);
57+
}

apps/web/app/app.dub.co/marketplace/layout.tsx renamed to apps/web/app/marketplace/layout.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { MarketplaceExternalHeader } from "@/ui/program-marketplace/external/marketplace-external-header";
2+
import { Analytics as DubAnalytics } from "@dub/analytics/react";
23
import { Footer } from "@dub/ui";
34
import { PropsWithChildren } from "react";
45

@@ -7,6 +8,15 @@ export default function MarketplaceExternalLayout({
78
}: PropsWithChildren) {
89
return (
910
<div className="flex min-h-screen flex-col bg-white">
11+
<DubAnalytics
12+
apiHost="/_proxy/dub"
13+
cookieOptions={{
14+
domain: process.env.VERCEL === "1" ? ".dub.co" : "localhost",
15+
}}
16+
domainsConfig={{
17+
refer: "refer.dub.co",
18+
}}
19+
/>
1020
<MarketplaceExternalHeader />
1121
<div className="relative flex flex-1 flex-col">
1222
<MarketplaceExternalGridLines />

apps/web/app/marketplace/page.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { MarketplaceExternalHomePage } from "@/ui/program-marketplace/external/marketplace-external-home-page";
2+
import { getMarketplaceCanonicalUrl } from "@/ui/program-marketplace/utils/urls";
3+
import { constructMetadata } from "@dub/utils";
4+
import { Metadata } from "next";
5+
6+
export const revalidate = 3600;
7+
8+
export function generateMetadata(): Metadata {
9+
const year = new Date().getFullYear();
10+
11+
return constructMetadata({
12+
title: `Best SaaS affiliate programs in ${year}`,
13+
description: `Browse and apply to the best SaaS affiliate programs on Dub's Partner Network.`,
14+
canonicalUrl: getMarketplaceCanonicalUrl("/marketplace"),
15+
});
16+
}
17+
18+
export default function MarketplaceHomePage() {
19+
return <MarketplaceExternalHomePage />;
20+
}

apps/web/lib/marketplace/parse-public-marketplace-query.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ export function parsePublicMarketplaceQuery(
1818
sortOrder: pickString(searchParams.sortOrder),
1919
page: pickString(searchParams.page),
2020
pageSize: EXTERNAL_MARKETPLACE_PAGE_SIZE,
21-
...(fixedCategory ? { category: fixedCategory } : {}),
21+
// Prefer the route-fixed category (category pages), but also honor an
22+
// explicit `?category=` — the client sends it on category-page refetches.
23+
// An invalid value just fails safeParse and falls through to no category.
24+
category: fixedCategory ?? pickString(searchParams.category),
2225
};
2326

2427
const parsed = getPublicNetworkProgramsQuerySchema.safeParse(input);

apps/web/lib/middleware/app.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ export async function AppMiddleware(req: NextRequest) {
2727
);
2828
}
2929

30+
// Keep app.dub.co/marketplace public and let it resolve the top-level marketplace route.
3031
if (path === "/marketplace" || path.startsWith("/marketplace/")) {
31-
return NextResponse.rewrite(new URL(`/app.dub.co${fullPath}`, req.url));
32+
return NextResponse.next();
3233
}
3334

3435
const user = await getUserViaToken(req);

apps/web/middleware.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const config = {
3434
};
3535

3636
export default async function middleware(req: NextRequest, ev: NextFetchEvent) {
37-
const { domain, path, key, fullKey, fullPath, searchParamsObj } = parse(req);
37+
const { domain, path, key, fullKey, searchParamsObj } = parse(req);
3838

3939
// Axiom logging
4040
logger.info(...transformMiddlewareRequest(req));
@@ -94,7 +94,7 @@ export default async function middleware(req: NextRequest, ev: NextFetchEvent) {
9494
);
9595
}
9696

97-
return NextResponse.rewrite(new URL(`/app.dub.co${fullPath}`, req.url));
97+
return NextResponse.next();
9898
}
9999

100100
if (isValidUrl(fullKey)) {

0 commit comments

Comments
 (0)