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. Review in cubic --- .../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 }));