Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion apps/web/app/(ee)/api/marketplace/programs/count/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getPublicNetworkProgramsCount } from "@/lib/fetchers/get-public-network-programs";
import { parsePublicMarketplaceQuery } from "@/lib/marketplace/parse-public-marketplace-query";
import { PUBLIC_MARKETPLACE_CACHE_HEADERS } from "@/lib/marketplace/public-marketplace-api";
import { NextResponse } from "next/server";

// GET /api/marketplace/programs/count - public marketplace program count
Expand All @@ -14,5 +15,7 @@ export async function GET(req: Request) {
search,
});

return NextResponse.json(count);
return NextResponse.json(count, {
headers: PUBLIC_MARKETPLACE_CACHE_HEADERS,
});
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getPublicNetworkProgramFilterCounts } from "@/lib/fetchers/get-public-network-program-filter-counts";
import { parsePublicMarketplaceQuery } from "@/lib/marketplace/parse-public-marketplace-query";
import { PUBLIC_MARKETPLACE_CACHE_HEADERS } from "@/lib/marketplace/public-marketplace-api";
import { NextResponse } from "next/server";

// GET /api/marketplace/programs/filter-counts - public marketplace filter counts
Expand All @@ -12,5 +13,7 @@ export async function GET(req: Request) {
rewardType,
});

return NextResponse.json(filterCounts);
return NextResponse.json(filterCounts, {
headers: PUBLIC_MARKETPLACE_CACHE_HEADERS,
});
}
5 changes: 4 additions & 1 deletion apps/web/app/(ee)/api/marketplace/programs/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getPublicNetworkPrograms } from "@/lib/fetchers/get-public-network-programs";
import { parsePublicMarketplaceQuery } from "@/lib/marketplace/parse-public-marketplace-query";
import { PUBLIC_MARKETPLACE_CACHE_HEADERS } from "@/lib/marketplace/public-marketplace-api";
import { NextResponse } from "next/server";

// GET /api/marketplace/programs - public marketplace program list
Expand All @@ -9,5 +10,7 @@ export async function GET(req: Request) {

const programs = await getPublicNetworkPrograms(params);

return NextResponse.json(programs);
return NextResponse.json(programs, {
headers: PUBLIC_MARKETPLACE_CACHE_HEADERS,
});
}
86 changes: 0 additions & 86 deletions apps/web/app/app.dub.co/marketplace/[[...slug]]/page.tsx

This file was deleted.

48 changes: 48 additions & 0 deletions apps/web/app/marketplace/[programSlug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { getNetworkProgram } from "@/lib/fetchers/get-network-program";
import { MarketplaceExternalProgramPage } from "@/ui/program-marketplace/external/marketplace-external-program-page";
import { generateMarketplaceProgramStaticParams } from "@/ui/program-marketplace/pages/marketplace-program-page";
import {
getMarketplaceCanonicalUrl,
getMarketplaceProgramHref,
} from "@/ui/program-marketplace/utils/urls";
import { constructMetadata } from "@dub/utils";
import { Metadata } from "next";

export const revalidate = 3600;

export async function generateStaticParams() {
const programParams = await generateMarketplaceProgramStaticParams();

return programParams.map(({ slug }) => ({ programSlug: slug[0] }));
}

export async function generateMetadata(props: {
params: Promise<{ programSlug: string }>;
}): Promise<Metadata> {
const { programSlug } = await props.params;

const program = await getNetworkProgram({ slug: programSlug });

return constructMetadata({
title: program ? program.name : "Program Details",
description:
program?.description ||
(program
? `Join the ${program.name} affiliate program on Dub's Partner Network.`
: `Browse and apply to the best SaaS affiliate programs on Dub's Partner Network.`),
image: program
? program.marketplaceHeaderImage || program.logo || undefined
: undefined,
canonicalUrl: getMarketplaceCanonicalUrl(
getMarketplaceProgramHref(programSlug),
),
});
}

export default async function MarketplaceProgramDetailPage(props: {
params: Promise<{ programSlug: string }>;
}) {
const { programSlug } = await props.params;

return <MarketplaceExternalProgramPage programSlug={programSlug} />;
}
26 changes: 26 additions & 0 deletions apps/web/app/marketplace/all/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { MarketplaceExternalListPage } from "@/ui/program-marketplace/external/marketplace-external-list-page";
import { getMarketplaceCanonicalUrl } from "@/ui/program-marketplace/utils/urls";
import { constructMetadata } from "@dub/utils";
import { Metadata } from "next";

// Rendered dynamically so the actual (filtered/sorted/paged) view is produced
// server-side and hydrates in place — no fallback swap.
export const dynamic = "force-dynamic";

export function generateMetadata(): Metadata {
return constructMetadata({
title: "All Affiliate Programs",
description: "Browse all affiliate programs on Dub.",
canonicalUrl: getMarketplaceCanonicalUrl("/marketplace/all"),
});
}

export default async function MarketplaceAllPage(props: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
const searchParams = await props.searchParams;

return (
<MarketplaceExternalListPage slug={["all"]} searchParams={searchParams} />
);
}
58 changes: 58 additions & 0 deletions apps/web/app/marketplace/c/[categorySlug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { PROGRAM_CATEGORIES_MAP } from "@/lib/network/program-categories";
import { MarketplaceExternalListPage } from "@/ui/program-marketplace/external/marketplace-external-list-page";
import {
getMarketplaceCanonicalUrl,
slugToCategory,
} from "@/ui/program-marketplace/utils/urls";
import { constructMetadata } from "@dub/utils";
import { Metadata } from "next";
import { notFound } from "next/navigation";

// Rendered dynamically so the actual (filtered/sorted/paged) view is produced
// server-side and hydrates in place — no fallback swap.
export const dynamic = "force-dynamic";

export async function generateMetadata(props: {
params: Promise<{ categorySlug: string }>;
}): Promise<Metadata> {
const year = new Date().getFullYear();
const { categorySlug } = await props.params;
const category = slugToCategory(categorySlug);

if (!category) {
return constructMetadata({ title: "Programs" });
}

const categoryMeta = PROGRAM_CATEGORIES_MAP[category];
const label = categoryMeta?.label ?? category.replaceAll("_", " ");

return constructMetadata({
title: `Best ${label} Affiliate Programs in ${year}`,
description:
categoryMeta?.listPageDescription ??
`Browse and apply to ${label.toLowerCase()} affiliate programs on Dub's Partner Network.`,
canonicalUrl: getMarketplaceCanonicalUrl(`/marketplace/c/${categorySlug}`),
});
}

export default async function MarketplaceCategoryPage(props: {
params: Promise<{ categorySlug: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
const { categorySlug } = await props.params;
const searchParams = await props.searchParams;

const category = slugToCategory(categorySlug);

if (!category) {
notFound();
}

return (
<MarketplaceExternalListPage
slug={["c", categorySlug]}
fixedCategory={category}
searchParams={searchParams}
/>
);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { MarketplaceExternalHeader } from "@/ui/program-marketplace/external/marketplace-external-header";
import { Analytics as DubAnalytics } from "@dub/analytics/react";
import { Footer } from "@dub/ui";
import { PropsWithChildren } from "react";

Expand All @@ -7,6 +8,15 @@ export default function MarketplaceExternalLayout({
}: PropsWithChildren) {
return (
<div className="flex min-h-screen flex-col bg-white">
<DubAnalytics
apiHost="/_proxy/dub"
cookieOptions={{
domain: process.env.VERCEL === "1" ? ".dub.co" : "localhost",
}}
domainsConfig={{
refer: "refer.dub.co",
}}
/>
<MarketplaceExternalHeader />
<div className="relative flex flex-1 flex-col">
<MarketplaceExternalGridLines />
Expand Down
20 changes: 20 additions & 0 deletions apps/web/app/marketplace/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { MarketplaceExternalHomePage } from "@/ui/program-marketplace/external/marketplace-external-home-page";
import { getMarketplaceCanonicalUrl } from "@/ui/program-marketplace/utils/urls";
import { constructMetadata } from "@dub/utils";
import { Metadata } from "next";

export const revalidate = 3600;

export function generateMetadata(): Metadata {
const year = new Date().getFullYear();

return constructMetadata({
title: `Best SaaS Affiliate Programs in ${year}`,
description: `Browse and apply to the best SaaS affiliate programs on Dub's Partner Network.`,
canonicalUrl: getMarketplaceCanonicalUrl("/marketplace"),
});
}

export default function MarketplaceHomePage() {
return <MarketplaceExternalHomePage />;
}
9 changes: 9 additions & 0 deletions apps/web/app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DEFAULT_PARTNER_GROUP } from "@/lib/zod/schemas/groups";
import {
getMarketplaceAllHref,
getMarketplaceCanonicalUrl,
Expand Down Expand Up @@ -62,6 +63,14 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
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

some: {
slug: DEFAULT_PARTNER_GROUP.slug,
applicationFormPublishedAt: {
not: null,
},
},
},
},
select: {
slug: true,
Expand Down
8 changes: 8 additions & 0 deletions apps/web/lib/fetchers/get-network-program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ export const getNetworkProgram = cache(async ({ slug }: { slug: string }) => {
addedToMarketplaceAt: {
not: null,
},
groups: {
some: {
slug: DEFAULT_PARTNER_GROUP.slug,
applicationFormPublishedAt: {
not: null,
},
},
},
},
include: {
groups: {
Expand Down
26 changes: 21 additions & 5 deletions apps/web/lib/marketplace/parse-public-marketplace-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,37 @@ function pickString(value: string | string[] | undefined) {
return typeof value === "string" ? value : undefined;
}

export function parsePublicMarketplaceQuery(
searchParams: Record<string, string | string[] | undefined> = {},
function toQueryInput(
searchParams: Record<string, string | string[] | undefined>,
fixedCategory?: Category,
) {
const input = {
return {
rewardType: pickString(searchParams.rewardType),
search: pickString(searchParams.search),
sortBy: pickString(searchParams.sortBy),
sortOrder: pickString(searchParams.sortOrder),
page: pickString(searchParams.page),
pageSize: EXTERNAL_MARKETPLACE_PAGE_SIZE,
...(fixedCategory ? { category: fixedCategory } : {}),
category: fixedCategory ?? pickString(searchParams.category),
};
}

const parsed = getPublicNetworkProgramsQuerySchema.safeParse(input);
export function isValidPublicMarketplaceQuery(
searchParams: Record<string, string | string[] | undefined> = {},
fixedCategory?: Category,
) {
return getPublicNetworkProgramsQuerySchema.safeParse(
toQueryInput(searchParams, fixedCategory),
).success;
}

export function parsePublicMarketplaceQuery(
searchParams: Record<string, string | string[] | undefined> = {},
fixedCategory?: Category,
) {
const parsed = getPublicNetworkProgramsQuerySchema.safeParse(
toQueryInput(searchParams, fixedCategory),
);

if (parsed.success) {
return parsed.data;
Expand Down
7 changes: 7 additions & 0 deletions apps/web/lib/marketplace/public-marketplace-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Cache the public marketplace endpoints at the edge so repeated queries (each
// filter combination keys on its URL) are absorbed by the CDN instead of hitting
// the DB. Short browser TTL, longer edge TTL — same pattern as `/api/links/metatags`.
export const PUBLIC_MARKETPLACE_CACHE_HEADERS = {
"Cache-Control": "public, max-age=300",
"Vercel-CDN-Cache-Control": "s-maxage=3600, stale-while-revalidate=86400",
};
Loading
Loading