From 5ce91e711c30ece7373498239d41822ef606d2ac Mon Sep 17 00:00:00 2001
From: Rashad Karanouh <11599358+rashad@users.noreply.github.com>
Date: Mon, 22 Jun 2026 18:54:21 +0400
Subject: [PATCH] fix(website): render partner marketplace dynamically to stop
profile 404s (#21963)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Fixes #21962
## Root cause
Partner data is materialized **at build time** from the live partners
API, and a build-time fetch failure is silently swallowed
(`fetch-live-marketplace-partners.ts` → `catch → return []`). One root
cause surfaces in two places:
- **All profile links 404 (the reported issue).**
`profile/[slug]/page.tsx` enumerates slugs in `generateStaticParams()` —
a build-time fetch — under the `[locale]` layout's inherited
`dynamicParams = false`. If that build-time fetch fails or returns
empty, **zero slugs are generated**, and because `generateStaticParams`
never re-runs at runtime and `dynamicParams=false` disables on-demand
generation, **every** `/partners/profile/[slug]` 404s until the next
deploy — even though the marketplace returns 20 partners client-side.
- **`/partners/list` intermittently renders empty.** The list page is
statically prerendered; the same build-time failure bakes an empty
marketplace and freezes it in the OpenNext/R2 cache.
This only reproduces on deployed builds: local dev renders on demand,
the env vars are present, and the partners API is reachable.
## Fix
Two route-segment config changes, no data-layer rewrite:
| File | Change | Effect |
|---|---|---|
| `(site)/partners/profile/[slug]/page.tsx` | `export const
dynamicParams = true` | Any slug renders on-demand at runtime where the
API is reachable. `generateStaticParams` becomes best-effort prewarm
instead of a 404 trap. Genuinely missing slugs still `notFound()`. |
| `(site)/partners/list/page.tsx` | `export const dynamic =
'force-dynamic'` | List is fetched at runtime, never baked empty at
build. The explicit `next: { revalidate: 300 }` on `/s/partners`
survives `force-dynamic` (`patch-fetch.js` only forces no-store when
there is *no* explicit fetch config), so responses stay cached and are
served stale on transient blips. |
## Verification
- `oxlint` + `oxfmt --check`: clean on both files.
- `jest src/partners-marketplace`: 36/36 pass.
- End-to-end behavior (static-vs-dynamic rendering) is a build/deploy
concern with no meaningful unit test — needs a deploy to confirm against
the live marketplace.
## Note / follow-up (out of scope)
Edge case left deliberately: if a real partner's *first-ever* request
lands during an API outage, its on-demand `notFound()` could cache for
~300s. Closing that means making the slug lookup distinguish "fetch
failed" from "not found" — a larger change than this fix.
---
.../src/app/[locale]/(site)/partners/list/page.tsx | 2 ++
.../src/app/[locale]/(site)/partners/profile/[slug]/page.tsx | 4 ++--
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/packages/twenty-website/src/app/[locale]/(site)/partners/list/page.tsx b/packages/twenty-website/src/app/[locale]/(site)/partners/list/page.tsx
index 7b9dc26e67..14321ca3ff 100644
--- a/packages/twenty-website/src/app/[locale]/(site)/partners/list/page.tsx
+++ b/packages/twenty-website/src/app/[locale]/(site)/partners/list/page.tsx
@@ -18,6 +18,8 @@ import { Menu } from '@/sections/menu';
export const generateMetadata = buildRouteMetadata('partnersList');
+export const dynamic = 'force-dynamic';
+
export default async function PartnersMarketplacePage({
params,
}: {
diff --git a/packages/twenty-website/src/app/[locale]/(site)/partners/profile/[slug]/page.tsx b/packages/twenty-website/src/app/[locale]/(site)/partners/profile/[slug]/page.tsx
index e2f05d411c..3c8471ab8b 100644
--- a/packages/twenty-website/src/app/[locale]/(site)/partners/profile/[slug]/page.tsx
+++ b/packages/twenty-website/src/app/[locale]/(site)/partners/profile/[slug]/page.tsx
@@ -14,8 +14,8 @@ import { Menu } from '@/sections/menu';
type PartnerProfileParams = { locale: string; slug: string };
-// The parent [locale] segment sets dynamicParams=false, so each partner slug
-// must be enumerated here too or it 404s.
+export const dynamicParams = true;
+
export async function generateStaticParams(): Promise> {
const partners = await fetchLiveMarketplacePartners();
return partners.map((partner) => ({ slug: partner.slug }));