fix(website): force-dynamic partner profiles to stop OpenNext 404 cache (#22120)

## Summary

Partner profile pages (`/partners/profile/[slug]`) returned **404 on
every slug** on OpenNext/Cloudflare while `/partners/list` showed live
partners from the same API.

PR #21963 fixed the list with `export const dynamic = 'force-dynamic'`
but only added `dynamicParams = true` on profiles. That is not
sufficient on OpenNext — the Worker kept serving **cached prerendered
404s** even when `TWENTY_PARTNERS_API_KEY` was present at runtime.

This PR mirrors the list page: **`force-dynamic` on the profile route**,
plus a small lint guard so both marketplace routes stay dynamic.

## Root cause

Partner data is fetched server-side from
`https://partners.twenty.com/s/partners` using
`TWENTY_PARTNERS_API_KEY`. That key is a **Wrangler runtime secret**
(not in `dev.env` / `prod.env`, not available during CI build — by repo
convention).

| Route | Before | Behavior |
|-------|--------|----------|
| `/partners/list` | `force-dynamic` (#21963) | Fetches at request time
on Worker → works |
| `/partners/profile/[slug]` | static + `dynamicParams = true` | Build
prewarm often empty; OpenNext served cached 404 |

## Fix

- Add `export const dynamic = 'force-dynamic'` to
`profile/[slug]/page.tsx` (keep `dynamicParams = true`).
- Add `scripts/check-partners-marketplace-routes.mjs` — fails lint if
list or profile drop `force-dynamic`.
- Wire guard into `project.json` `lint` target (runs before existing
`check-conventions.mjs`).

**No infra changes.** We intentionally did not add a GitHub Actions
secret for the API key — that would contradict the documented pattern
(`wrangler secret put` only).

## Verification

- [x] `node scripts/check-partners-marketplace-routes.mjs` → OK
- [x] `npx jest src/partners-marketplace` → 36/36 pass
- [x] Deployed to **dev** (`deploy-website`, env `dev`, ref
`rk-partner-profile-404`)
- [x] `curl -sI
https://twenty-main.com/partners/profile/atlasprods-technologies-llp` →
**HTTP 200**
- [x] Browser: list → profile link loads

## Test plan

- [ ] CI lint + tests green
- [ ] After merge: deploy prod when ready (`environment: prod`, confirm
`website`)
- [ ] Spot-check `https://twenty.com/partners/profile/<slug>` → 200

## Out of scope

- Build-time `generateStaticParams` prewarm (would need a separate infra
discussion; not required once profiles are `force-dynamic`)
- Per-slug `/s/partner-by-slug` endpoint (optional perf follow-up)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22120?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Rashad Karanouh
2026-06-24 20:48:03 +04:00
committed by GitHub
parent 8830ef89bd
commit 500f441807
3 changed files with 34 additions and 1 deletions
+1 -1
View File
@@ -39,7 +39,7 @@
"executor": "nx:run-commands",
"options": {
"cwd": "{projectRoot}",
"command": "node scripts/check-conventions.mjs && npx oxlint -c .oxlintrc.json . && npx oxfmt --check ."
"command": "node scripts/check-partners-marketplace-routes.mjs && node scripts/check-conventions.mjs && npx oxlint -c .oxlintrc.json . && npx oxfmt --check ."
},
"configurations": {
"fix": {
@@ -0,0 +1,31 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
const routes = [
'src/app/[locale]/(site)/partners/list/page.tsx',
'src/app/[locale]/(site)/partners/profile/[slug]/page.tsx',
];
const failures = [];
for (const relativePath of routes) {
const fullPath = path.join(root, relativePath);
const source = fs.readFileSync(fullPath, 'utf8');
if (!/export const dynamic = 'force-dynamic';/.test(source)) {
failures.push(
`${relativePath}: must export dynamic = 'force-dynamic' (OpenNext runtime fetch; build has no TWENTY_PARTNERS_API_KEY).`,
);
}
}
if (failures.length > 0) {
console.error('check-partners-marketplace-routes: FAILED');
for (const failure of failures) {
console.error(` ${failure}`);
}
process.exit(1);
}
console.log('check-partners-marketplace-routes: OK');
@@ -14,6 +14,8 @@ import { Menu } from '@/sections/menu';
type PartnerProfileParams = { locale: string; slug: string };
export const dynamic = 'force-dynamic';
export const dynamicParams = true;
export async function generateStaticParams(): Promise<Array<{ slug: string }>> {