## What
The twenty-infra **"Deploy Website"** workflow has failed every run
since 06-26 — at OpenNext's R2 incremental-cache step, **not** the
build:
```
Failed to provision remote R2 bucket "twenty-website-cache-dev"
for binding "NEXT_INC_CACHE_R2_BUCKET":
Failed to check whether bucket exists: … Premature close
```
## Root cause
`@opennextjs/cloudflare`'s `ensureR2Bucket()` calls the Cloudflare SDK's
`r2.buckets.get()`. On **Node 24** (which the deploy pins) undici
truncates the **gzip-compressed** Cloudflare API response → `Premature
close`. The SDK's own retries don't help (it's systematic, not flaky),
and pre-creating the bucket doesn't help (it always `.get()`s first).
This is **pre-existing and unrelated to the multi-locale change**
(#22257): the *Build Worker* step succeeds, and the identical error
appears on 06-26 runs (two days before that merged).
## Fix
opennext **1.20.0** fixes this precisely — it passes `defaultHeaders: {
"Accept-Encoding": "identity" }` to the Cloudflare SDK client, so the
API returns **uncompressed** responses (no decompression → no premature
close). **Node 24 is kept**, and no twenty-infra change is needed — the
deploy runs `twenty`'s own `npx opennextjs-cloudflare`, so bumping the
dep here is enough.
`^1.0.0 → ^1.20.0`. **1.20.0, not the latest 1.20.1**, because the
repo's `npmMinimalAgeGate: 3d` still quarantines 1.20.1 (published
06-26); 1.20.0 (06-25) is past the gate and carries the same fix.
## Verification
- Resolved to `1.20.0`; confirmed `Accept-Encoding: identity` is in the
installed `dist/cli/utils/ensure-r2-bucket.js`.
- `nx typecheck twenty-website` green (OpenNext config API unchanged
across the bump).
- Diff is just `package.json` + `yarn.lock`.
⚠️ Full confirmation needs a **Deploy Website** run (exercises the build
+ the R2 provision step), which I can't trigger — please re-run it after
merge.
## What
The marketing site now serves every language the **documentation** ships
— 14 locales (`en, fr, ar, cs, de, es, it, ja, ko, pt, ro, ru, tr, zh`),
up from 3 (`en, es, fr`).
## How
- **Single source of truth.** `WEBSITE_LOCALE_LIST` derives directly
from `DOCUMENTATION_SUPPORTED_LANGUAGES` (`twenty-shared/constants`).
Add a documentation language → it flows to the website automatically.
- **Off `APP_LOCALES` entirely.** The website locale type is now
`DocumentationSupportedLanguage` (short codes), so a locale **is** its
URL segment — no short↔full mapping, and no `pt-BR`/`zh-CN` ambiguity to
resolve.
- **Removed the indirection this exposed** (it only existed because
`AppLocale` was a superset of the deployed set):
- `locale-to-url-segment` / `locale-by-url-segment` (locale == segment)
- `get-locale-messages` pass-through → callers read `MESSAGES_BY_LOCALE`
directly
- the `messages-by-locale` runtime guard → a total
`Record<DocumentationSupportedLanguage, Messages>` (a missing catalog is
now a **compile** error, not a runtime throw)
- `isWebsiteLocale` → a `string → DocumentationSupportedLanguage` type
guard
- the vestigial language-code `split('-')` in `locale-display-name`
## Catalogs
- Renamed `es-ES → es`, `fr-FR → fr`; added 11 new locales (untranslated
for now → **English fallback**).
- `crowdin-website.yml` switched to `%two_letters_code%`.
- Regenerating catalogs also synced `en.po` with current source
(`Boolean` / `Date & Time` / removed `Fields widget` from the
already-merged #22249).
- `ci-website` is unchanged — no `lingui:compile` step added; catalogs
stay committed.
## Testing
- `typecheck` · `lint` (check-conventions + oxlint + oxfmt) · 347/347
tests — all green. PR CI runs exactly lint + typecheck + test.
## Follow-up (out of repo)
Enable the 11 languages on **Crowdin project 4** so `website-i18n-pull`
backfills real translations. Until then, the new locales render with
English fallback (correct behavior).
## What
Reworks the **"Layout" step** visual of the product-page stepper (the
floating record-layout-editor scene) to match the Figma references and
read as a premium, layered composition. Data is unchanged — this is
purely the look.
## Figma alignment
- **Fields editor** — the inline-edit field row now keeps the field icon
+ a bordered input + blue **Done** + the eye / ⋮ (instead of a floating
box); **"New fields"** renders as a uniform field row; section headers
get an overflow menu; field types read `Boolean` / `Date & Time`.
Dropped the redundant "Fields widget" header label.
- **"General" widget** — header chevron, the real `anonymousFelix`
avatar on Account Owner (reusing the shared asset the other
product-feature visuals use), a **Revenue** row, and a money-bag icon
for the currency field.
## Premium composition
- The nav, record, and Fields editor read as one elevated **z-stack**
(back → front): the nav is pushed right so its edge tucks under the
record (lowest z-index), the record sits above it, and the editor stays
in front — each casting a progressively stronger shadow so the depth
ordering is unmistakable.
- The **"New record / Enrich / Edit actions"** bar is centered over the
nav–record seam (`translateX(-50%)`) to tie the layered stack together
at the top.
## Testing
- typecheck · oxfmt · oxlint · check-conventions — green.
- Verified visually with headless Playwright on `/product` (Layout
step).
<img width="697" height="734" alt="image"
src="https://github.com/user-attachments/assets/58ae7ffb-13d1-4310-bbdc-864589f8bc43"
/>
## What
The sticky menu now adopts the color scheme of whatever section sits
beneath it as you scroll, across every page. Background, logo, and
buttons ease between schemes for a smooth handoff.
## How
- **Declarative contract** — sections opt in with a `data-menu-surface`
attribute (`SectionShell`, the footer, and the product hero's mobile
sections); `useActiveSurfaceScheme` tracks which surface sits under the
menu's bottom edge (`MENU_HEIGHT_PX`) and reports its `data-scheme`.
- **One provider** — `MenuStyleProvider` lives in the `(site)` layout so
every page adapts; the Menu resolves its scheme as `override ??
activeScheme ?? prop`. SSR seeds the prop to each page's first section,
so there's no mount flash.
- **Hero handoff** — the product hero keeps the menu via a per-frame
override only while its track still covers the nav band
(`controlsMenu`); it releases to the observer *before* the track clears
the bar, so the menu stays opaque on exit and never flashes the halftone
backdrop rising behind it. The menu's `backdrop-filter` was removed (a
no-op over the opaque menu, and a GPU compositing artifact).
## Also in this PR
- **Menu folder reorg** to the `product-feature` convention:
`components/`, `effect-components/`, `data/`, and `types/` (one domain
type per file).
- **`MENU_HEIGHT_PX` token** replacing the literal `64` that was
duplicated across four files (menu row, the hero's scroll model and
component, the observer).
- **`findActiveSurfaceScheme`** extracted as a pure, unit-tested
function (inclusive top / exclusive bottom, first-match, no-match,
null-scheme).
- **Mobile AI-section fix** — the mobile hero sections now declare the
surface contract, so the menu adapts dark over the AI block on mobile
(it previously stayed light). Their color is driven from `data-scheme`
(single source) rather than a parallel prop.
## Testing
- Jest — 16 tests (scroll model + surface-selection util).
- Headless Playwright (mobile 390px) — menu `light` over the intro →
`dark` over the AI section; section colors unchanged (`#fff` /
`rgb(20,20,20)`).
- Desktop unaffected — the mobile sections are `display:none` (zero
rect), so the observer skips them and the hero override path is
untouched.
- Gates green — typecheck, oxlint, oxfmt, check-conventions.
## Summary
Reworks the product page's closing section, and in doing so makes every
signoff closer width-driven.
### 1. Replace the Demo section with a register Signoff (`6a5f0c34bf`)
The product page ended on a static `AppPreview` mockup ("Try it live").
Swap it for the shared `Signoff` closer — a focused register CTA, the
same component the customers / why-twenty / partners pages use — so the
page ends by driving sign-ups instead of re-showing the product.
- New `ProductSignoff`: heading "Start moving faster today.", a
supporting line, and the **Get started + Talk to us** pair (matching
`CustomersCatalogSignoff`). Uses `SITE_URLS.appWelcome` per the
site-urls rule rather than inlining the URL.
- Removes the now-unused `product-demo` section and its 192KB background
asset.
### 2. Make Signoff headings width-driven (`0365c0d2ea`)
The signoff headings forced their two-line break with a literal `\n` in
the translated string — against the site's typography principle (fluid
type + `text-wrap: balance`, no `<br>`).
- `Signoff` now carries one default **`615px`** heading measure; all
five closers drop `\n`.
- The width was **measured, not guessed**: each heading was rendered
headlessly in the real production fonts (Aleo 300 body, Host Grotesk 300
accent — each heading mixes both) at the desktop size with `text-wrap:
balance`, then I found the common window where every heading breaks
identically: whyTwenty `[565–820]`, customers `[425–800]`, partner
`[380–730]`, product `[380–660]` — `615` sits in all of them. Each was
then verified to reproduce its exact current break at 615px.
- **Affects 4 other pages** (why-twenty, customers, customers/[slug],
partners) — all verified to render identically, and headings now reflow
on narrow viewports instead of being pinned by `\n`.
- Drops the one-off `headingMaxWidth` prop (now unused).
## Test plan
- oxfmt, oxlint, check-conventions, typecheck — all green.
- Headless measurement confirms all five signoff headings reproduce
their current desktop break at 615px (no visual regression).
- Visually confirmed the product closer and the four sibling signoffs.
Rebuilds the product-feature **DashboardVisual** to mirror
twenty-front's dashboard widgets, with colors traced to twenty-front's
actual source rather than eyeballed.
## Widgets
- **Bar — "Deals by month"**: single-series `blue8` (twenty-front's
`GRAPH_DEFAULT_COLOR`), dashed `4 4` gridlines, value labels, nice
rounded Y-ticks. Replaces the old stacked bar (stacked bars aren't used
in twenty-front).
- **Donut — "Deals by stage"**: the real opportunity pipeline
(New/Screening/Meeting/Proposal/Customer), each segment colored by that
stage option's own color from the metadata
(`red/purple/sky/turquoise/yellow`), with a center total and a paginated
horizontal legend.
- **KPIs**: big-number cards (Revenue YTD / Avg deal size / Win rate).
## Responsive — `mediaUp('md')`
The dashboard is the full-width spotlight tile, whose frame is short
below md and grows to 420px at md+. So the layout keys off md: below it
collapses to **2-up KPIs + a full-width bar** (donut and the 3rd KPI
hidden, smaller breadcrumb), and the spotlight frame's mobile min-height
is bumped so the bar has room; at md+ the full 3-KPI + side-by-side
layout returns. The donut caps at its size and shrinks with its
container.
## Notes
- Follows the `product-feature` conventions (per-visual folder,
one-export-per-file, split types).
- `Tiles.tsx`: one-line spotlight mobile min-height bump (only the
dashboard uses the spotlight tile).
- typecheck + lint + build all green.
<img width="1148" height="638" alt="image"
src="https://github.com/user-attachments/assets/1eb8f120-88c2-45d1-adf9-ec00abe11006"
/>
## Context
On the partners marketplace, the eyebrow shown below each partner's name
(on both the list card and the profile header) displayed the served
regions (e.g. `APAC`, `EUROPE`). The partner's actual location was
buried in the "Where & how" facts list. The location is more useful at a
glance, so this swaps the two.
## Changes
**List card (`PartnerCard`)**
- The eyebrow below the name now shows the partner's city and country
instead of the first served region.
**Profile page**
- `PartnerProfileHeader`: the eyebrow near the title now shows the real
location (city, country).
- `PartnerFactsList`: the served regions move into the "Where & how"
section as a `Regions` row, taking the place of the now-redundant "Based
in" row.
The `Regions` chip rows on the card are unchanged.
https://claude.ai/code/session_01FQRRfvPpmNsjAAnZeDc5sy
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FQRRfvPpmNsjAAnZeDc5sy)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22224?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. -->
## Summary
Two desktop/tablet polish refinements to the product page hero's hero→AI
transition (`product-hero`).
### 1. Delay the Ask-AI panel reveal to after the wipe (`b638d600`)
The Ask-AI side panel used to reveal *during* the dark "wipe up" (keyed
to the morph `0.45 → 0.70`), so it competed with the rising black edge
and piled onto the heaviest motion phase — rough on weaker laptops.
It's now keyed to raw scroll progress over `[0.60, 0.70]` — the
previously-idle hold after the wipe settles at `0.55` — so it slides in
on its own beat once the eye has followed the black up. `morphProgress`
is pinned at `1` across the whole post-wipe stretch, so leaving the
morph clock was the only way to time the panel *after* the wipe. The
conversation playback moves with it, so the chat doesn't stream while
the panel is still `width: 0`.
### 2. Keep the AI heading to three lines below desktop (`8ae69a4b`)
The AI heading is the page's longest line. Below `md`, only the mobile
copy renders, where the measure was frozen at `360px` while the heading
font scales fluidly toward `md` — so on tablet widths the title crammed
onto four lines, recovering only at `921px` when the `672px` measure
kicks in.
Adds an `sm`-breakpoint measure step (`560px`) scoped to the AI heading;
the intro heading and the desktop measuring path keep their existing
`360/672` steps.
## Test plan
- `product-hero-scroll-model` jest suite updated and green (11/11),
including a new post-morph panel-timing test.
- Lint (`check-conventions` + `oxlint` + `oxfmt`) and typecheck green.
- Visually confirmed: the panel reveals after the wipe settles, and the
AI title holds three lines across tablet widths.
## What
Reworks the website's **ProductStepper** (the scroll-driven *Data model
/ Automation / Layout* section) to be frameless and to render all three
scenes faithfully to twenty-front — real icons, labels, colors,
structure, and connectors.
## Changes
**Frameless + scaling**
- Removed the shared white card/header; the three scenes now sit
directly on the dark dot-grid stage.
- Unified every scene on one `StageFit` primitive (fixed design box →
scaled to fit), pixel-identical at full width and uniform on smaller
screens.
**Data model scene**
- Real object schema (3 Standard + 2 Custom objects, real relation
fields); replaced an invented "Investors" object with the real
**Employment History** custom object (`IconBriefcase`, Company + Person
relations) from the server seed.
- Clean spanning-tree connections; removed the Standard/Custom badge;
fixed card sizing + edge centering.
**Workflow scene**
- Real action labels + a logical flow: *Record is Created → Filter →
Search Records → AI Agent → (Update Record · Send Email · Create
Record)*. Dropped the iterator (a loop construct shown without a loop
body).
- Per-action icon colors matching twenty-front (trigger blue, flow
green, record gray, send-email red, AI agent pink) on a gray tile.
- Rebuilt the node to twenty-front's real anatomy and the connectors
(source circle → `getBezierPath` → arrow marker) verbatim.
**Layout scene**
- Real workspace sidebar: real Tabler object icons, exact labels, the
true default sidebar (6 objects + Workflows folder),
`getIconTileColorShades` tile colors; dropped invented entries.
- Record overview + Fields editor with correct field-type labels (Links,
True/False, Date and Time); legibility + spacing tuning.
**Icons** — replaced every hand-drawn approximation with real
`@tabler/icons-react` / twenty-front object icons.
**Misc** — smoother step-to-step transitions (translate + easing
tokens).
## Testing
Marketing visual; `lint` / `typecheck` / `build` green, and each scene
reviewed visually against twenty-front.
https://github.com/user-attachments/assets/997e1b95-55c0-401a-93a4-c70545577057
Reorganizes the flat `sections/stepper/` (30 files — two distinct
steppers plus shared code) into two product-feature-style sections, and
moves the shared code to the shared layers.
- **`sections/home-stepper/`** — the home-page stepper. Renamed
`Stepper` → `HomeStepper` (and the home components → `HomeStepperLottie`
/ `HomeStepperSteps` / `HomeStepperVisualFrame`) for symmetry with
`ProductStepper`. Shell at the root + `components/`/`data/`/`utils/` +
barrel.
- **`sections/product-stepper/`** — the product-page stepper, same
structure.
The 3 files both steppers shared can't live in a shared *section* —
`check-conventions` forbids a section importing another section. So they
moved to the shared layers:
- `StepperProgressRail`, `StepperSwipeDeck` → `ui/`
- `useBreakpointStepSync` → `platform/motion`
Both consumer pages repointed (`@/sections/home-stepper`,
`@/sections/product-stepper`); the row-gap allowlist in
`check-conventions.mjs` updated to the new paths; explanatory comments
stripped across the moved files (CSS-in-template comments and `'use
client'` kept).
Pure reorganization — no behavior change. typecheck + lint + build all
green.
- Rework `TasksVisual` to match twenty-front's tasks tab: per-group
bordered cards with row dividers, `TODO`/`DONE` headers, the `+ Add
task` button, and twenty-front's checkbox, row, and target-chip styling.
- Enable the checkbox to toggle a task between `TODO` and `DONE`.
Marketing data kept intuitive (single assignee, full dates, action-led
titles).
<img width="859" height="608" alt="image"
src="https://github.com/user-attachments/assets/085c225f-b121-44e6-aaec-553c02fd2367"
/>
Markup/CSS tidy-up of `ImportVisual` — no visual change.
- Collapse the redundant `Grid` wrapper into `Root` (now
`flex-direction: column`) and drop the no-op `justify-content: center`
(the child was already full-width).
- Hoist the duplicated `font-size: previewFontSize(md)` to the mapping
`Row` (the `sm` example keeps its override).
<img width="574" height="604" alt="image"
src="https://github.com/user-attachments/assets/dcc72567-5c13-4d54-884a-f3120cc5a345"
/>
- Rework `EmailsVisual` to match twenty-front's emails tab: bordered
thread card with dividers, `Inbox` header sizing, and twenty-front's
row/sender/date colors and hard-clipped sender names (kept `+ Compose`).
- Replace the "not shared" row with four fully-populated shared threads
(subjects/bodies sourced from twenty-server's dev seed), two
participants each, full `MMM D, YYYY` dates.
<img width="575" height="607" alt="image"
src="https://github.com/user-attachments/assets/68d1fd6d-e030-4f3c-b775-687f7e1ab173"
/>
- Rework `FilesVisual` to match twenty-front's files tab: bordered file
card with dividers, `All` header sizing, and twenty-front's row
background and date color (kept the bordered `+ Add file`).
- Four files, one per category (pdf/xlsx/pptx/png), with full `MMM D,
YYYY` dates. File-icon colors already matched `useFileIconColors`.
## Summary
Follow-up to #22120. Removes the `check-partners-marketplace-routes.mjs`
lint guard and its `project.json` wiring — the profile fix is just
`force-dynamic` on the page; the extra script is not needed.
## Changes
- Delete
`packages/twenty-website/scripts/check-partners-marketplace-routes.mjs`
- Restore `project.json` lint command to run only
`check-conventions.mjs` (as before #22120)
## Context
The guard was added in #22120 but the removal commit did not land before
merge. This PR cleans that up. No runtime behavior change.
## Test plan
- [ ] `nx lint twenty-website` (or CI) passes without the removed script
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22121?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. -->
## 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. -->
The website's Cloudflare build (`opennextjs-cloudflare` / Turbopack) was
failing with `_defineProperty is not a function` while linaria/wyw
evaluates `twenty-ui/dist/theme.cjs` at build time. It regressed in
#21946, whose twenty-ui build rework changed the emitted `theme.cjs` so
the theme objects ship as runtime object spreads (`{ ...THEME_COMMON
}`).
**Cause:** wyw evaluates modules in Node through `next/babel`, which
pulls in `preset-env` + `transform-runtime`. Those re-lower the runtime
spreads into `@babel/runtime` helpers imported as ESM; wyw then
`require()`s that ESM module in a CJS context where the export is not
callable, so `_defineProperty` fails.
**Fix:** wyw runs in Node and needs no downleveling, so replace
`next/babel` with minimal presets (`@babel/preset-typescript`,
`@babel/preset-react`, `@wyw-in-js/babel-preset`, plus
`@babel/plugin-transform-export-namespace-from`), matching
twenty-front's wyw config. No `@babel/runtime` helpers get injected.
Kept on the website side so twenty-ui keeps react/react-dom as peer deps
(#21946).
Note: no blocking PR check runs the website production build, so this is
best validated via the website preview build or the twenty-infra deploy.
Applies the PipelineVisual folder structure to every other file in
`product-feature`.
- Each feature visual (Import, Tasks, Files, Emails, Dashboard,
Contacts) becomes an `XVisual/` folder: shell + `index.ts` barrel, with
`components/`, `data/`, `types/` (one export per file), and `utils/` as
applicable. `BarChart`/`DonutChart` move into `DashboardVisual`
(exclusive to it); `RecordTabHeader` stays shared.
- The section's non-visual files get the same treatment: `components/`
(Tiles, TileVisual, TileContent, ScrollEntrance, RecordTabHeader),
`data/`, `types/`, `utils/`. `ProductFeature.tsx` stays the section
shell.
- Drop dead code: unused `WindowChrome` and the now-orphaned
`product-feature-scene` token.
No behavior change — `index.ts` barrels keep all import paths stable.
Typecheck, check-conventions, oxlint, and unit tests pass locally.
Reworked the product page's `PipelineVisual` to match twenty-front's
kanban — board header, card layout/fields, hover states, and font
weights.
Also loaded and pinned Inter (twenty-front's product font) for the
app-preview mockups, which previously fell back to the system font.
<img width="879" height="625" alt="image"
src="https://github.com/user-attachments/assets/396f0a90-114b-47f9-92a6-2e74d732a91e"
/>
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.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21963?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. -->
## Problem
On the home feature cards, the visual frame is capped at `max-width:
411px` (the scene's design width) and centered. Below ~411px-wide cards
this is invisible, but once a card grows past 411px (wider viewports)
the dark scene stops filling and the **card's light background shows on
both sides** of the visual.
At 1200px everything looks correct because the cards are narrower than
411px and the cap is never engaged; the issue only appears as the
viewport widens.
## Fix
`FeatureCard.tsx`, one file:
- **Remove the `max-width: 411px` cap** (and the now-dead `margin: 0
auto`) from `CardImageFrame` so the frame fills the card width at every
breakpoint. `useScaleToFit` then scales the 411×508 scene up to match —
it's a CSS transform on DOM, so it stays crisp; no raster upscaling.
- **Even out the card gutter** — `CardImage` padding `8px → 16px` (top +
sides) so the visual's inset matches the content's 16px inset instead of
stepping in. Bottom stays `0` (the content block's 16px provides the
bottom gutter).
The visual scenes themselves are untouched — this is purely the
frame/container.
## Before
<img width="1477" height="681" alt="image"
src="https://github.com/user-attachments/assets/731f1ef7-e761-468e-b7aa-a5a06f8ac790"
/>
## After
<img width="1473" height="705" alt="image"
src="https://github.com/user-attachments/assets/99d036a6-d3ad-4682-99c8-f283b5b95171"
/>
Two tweaks to the product-feature tiles section:
- **Spotlight visual moved to the top** — the spotlight tile now renders
its visual above its content, matching the layout of the regular grid
cells.
- **Uniform tile background** — every tile now uses the same neutral
gray background; removed the per-tile `TILE_MUTED` alternating toggle.
The rebuilt-site cutover dropped several `next.config.ts` blocks the
previous twenty-website had:
- **`deploymentId`** — required by `open-next.config` skew protection;
deploys were failing with *"Deployment ID should be set in the Next
config when skew protection is enabled"*.
- **Security headers** (HSTS, CSP `frame-ancestors`, X-Frame-Options, …)
+ immutable asset `Cache-Control`.
- **Redirects** — www→apex canonicalization and content redirects (docs,
legal, case-studies, partners, why-twenty).
Legacy raw locale-code redirects were intentionally not restored (the
site only serves en/fr/es; those URLs never existed for other locales).
Pairs with twentyhq/twenty-infra#741, which builds twenty-ui before the
worker build.
Renaming the package so any further PRs directed to the website are
targeted to the reworked code instead of diverging. Once merged, I will
start preparing this for deployment to dev to test before releasing to
prod. Any improvements will also be applied to this package.
I avoided making significant changes to API routes so nothing breaks,
but will test it thoroughly today to confirm. That said, everything is
ported - double checked.
Big diff PR, impossible to review, but last one! No more rebuilds.
## Summary
Adds an internal support endpoint to regenerate a customer's enterprise
key
when they've lost the one issued at activation. The key payload is
deterministic
from the Stripe subscription, so this re-emits an equivalent valid key
without
any new state.
`GET /api/enterprise/reissue/<subscriptionId>/<secret>`
- Guarded by a shared secret (`ENTERPRISE_ADMIN_API_SECRET`), compared
in
constant time and fail-closed when unset.
- Looks up the subscription in Stripe (for the licensee) and signs the
key with
`signEnterpriseKey()`, reading `ENTERPRISE_JWT_PRIVATE_KEY` from the
environment — the private key is never accepted from the request.
- No subscription-status gate: the key alone grants nothing. Feature
access
still requires a validity token, which `/api/enterprise/validate` only
issues
after re-checking the subscription is active.
## Notes / follow-ups
- The admin secret travels in the URL path, so it can land in
server/proxy/CDN
access logs — rotate `ENTERPRISE_ADMIN_API_SECRET` if logs are ever
exposed.
- No audit logging yet; worth adding (who reissued which subscription,
when).
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21660?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. -->
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
## Summary (twenty-website — partner application form)
- **Required fields**: website URL, city, hourly rate, minimum project
are now required (client step-gate + server zod) with `*` markers. The
final step validates before POSTing, so empty required fields fail fast
client-side instead of round-tripping.
- **Technical skills reworked to *complement* "What you cover"** (the
service categories) rather than duplicate them — now a small shown set +
a larger searchable-only pool of tools / technologies / industries.
Field hint clarifies the intent.
- **No competitor CRMs** in suggestions (Salesforce/HubSpot/Attio
removed); a guard test fails if one ever reappears. Migrations surface
as a generic "CRM migration".
- `Form.TagInput` gains an optional `searchPool` prop (autocomplete-only
entries, not rendered as chips).
Companion to the app-side PR #21709.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21710?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. -->
## Context
AWS Inspector flags the `prod-twenty` image (built from current main)
with 16 findings, and Dependabot alert 174 flags esbuild. This PR fixes
the OpenSSL scanner findings and the esbuild CVE. The typeorm bump
(CVE-2025-60542) was **pulled out of this PR** — see "typeorm status"
below.
## Changes
### Strip `/usr/local/include/node` from runtime stages
(`twenty-server`, `twenty-app-dev`)
15 OpenSSL CVEs (June 9 advisory, incl. CRITICAL CVE-2026-34182) are all
detected via **Node's bundled OpenSSL dev headers**: 3 GENERIC
`openssl/openssl` 3.5.6 detections per CVE at
`/usr/local/include/node/openssl/archs/linux-x86_64/{asm,asm_avx2,no-asm}/include/openssl/opensslv.h`.
The headers are only needed by node-gyp and native addons are compiled
in the build stages — nothing compiles at runtime. Dropping them clears
all 45 detection instances and permanently ends this class of finding
(third occurrence: 3.5.5 → 3.5.6 → 3.5.7). None of these CVEs are
reachable through Node (no CMS/PKCS#7 API, `pfx` is operator-supplied,
Node's QUIC uses ngtcp2, ASN.1 issues need ~2GB inputs).
**Follow-up (~June 17, 2026):** the `node` binary itself still
statically links OpenSSL 3.5.6 — invisible to the scanner after this PR
and unreachable in practice, but the real fix is bumping the pinned
`node:24-alpine` digest once the [announced June 17 Node.js security
releases](https://nodejs.org/en/blog/vulnerability/june-2026-security-releases)
ship a 24.x linking OpenSSL ≥ 3.5.7 (verify via
`deps/openssl/openssl/VERSION.dat` on the release tag — 24.16.0 is still
on 3.5.6). A dated TODO sits next to the cleanup in the Dockerfile.
### esbuild dev-server CORS CVE (Dependabot alert 174,
GHSA-67mh-4wv8-2f99)
`@lingui/cli@5.1.2` (pins `esbuild ^0.21.5`) was the last parent
resolving a vulnerable esbuild (≤ 0.24.2 lets any website send requests
to the dev server and read responses). Instead of a resolution override,
this bumps the lockstepped **lingui suite 5.1.2 → 5.9.5** (within-major;
lingui adopted `esbuild ^0.25.1` in 5.4.1), which:
- removes `esbuild@0.21.5` and all its platform packages from the
lockfile with no forced ranges;
- drops the `@lingui/core` lockstep resolution (its comment marked it
droppable on the next coordinated lingui bump — the tree now resolves a
single `@lingui/core@5.9.5`);
- `@lingui/swc-plugin` stays at `^5.11.0` (peers on `@lingui/core: 5`;
its 6.x line targets lingui 6).
**lingui 5.9.5 behavioral fallout handled here:**
- Translation functions now **throw without an active locale** (5.1.2
fell back silently). The global `i18n` singleton that backs server-side
`` t`…` `` calls only had a messages compiler set, never an activated
locale → activate the source locale in `I18nService.loadTranslations()`,
mirrored in the server jest setup (unit tests bypass Nest bootstrap).
- `msg`/`t` placeholders are now strictly typed (reject
`null`/`undefined`/`unknown`) → one server call site and 16 twenty-front
files adapted with minimal nullish-coalescing fixes that preserve
rendering.
- `.po`/compiled-catalog churn from the new extractor/compiler
(reference reordering, sorted keys — verified content-identical on
unchanged `.po` inputs) is intentionally not committed: the scheduled
i18n workflows regenerate those.
## typeorm status (pulled out)
typeorm 0.3.20 → 0.3.26 was originally in this PR but **made workspace
metadata sync intermittently lossy**: `example-app-postcard` failed
twice with a *different* field missing from the synced PostCard object
each run, and one integration shard's `DataSeedWorkspaceCommand` died
with "Could not find flat entity with universal identifier …" — versus
zero such failures on recent main. Local runs (db reset + seed, group-by
integration suite 19/19) pass, so it is a nondeterministic
CI-load-sensitive regression that needs dedicated debugging (typeorm
changed LIMIT/OFFSET 0 semantics, lazy count for `getManyAndCount`,
upsert WHERE construction, and topological-sort internals in that
range). The resolutions comment documents this as the blocker;
CVE-2025-60542 is MySQL-driver-only (`sqlstring`), so Postgres-only
Twenty is not exposed in the meantime.
## Verification
- `npx nx typecheck twenty-server` / `twenty-front` — clean (no cache)
- `npx nx test twenty-server` — full suite green
- `lingui:extract` + `lingui:compile` — clean for twenty-server /
twenty-emails / twenty-front
- `oxfmt --check` — clean for both packages
- Lockfile diff: lingui 5.9.5 entries, `esbuild@0.21.5` +
`@esbuild/*@0.21.5` platform packages removed, no typeorm changes
## What
After a partner submits the application wizard, the success screen now
offers an inline Cal.com booking widget so they can book an intro call
on the spot — keeping the partner process high-touch.
- Inline Cal.com embed on the application success step, prefilled with
the applicant's name/email (company in the notes)
- Wide `month_view` layout; the success modal widens to ~960px (the
4-step form and mobile are unchanged)
- Event-type-details panel hidden via `cal('ui', { hideEventTypeDetails:
true })` so it's just calendar + times, on its own `partner-intro` Cal
namespace (isolated from the ContactCal embed)
- "I'll book later" escape hatch; backend / submission path untouched
## Why
The warmest moment is right after someone opts in. Today the success
screen only shows a Close button — this turns that moment into a
scheduled conversation.
## How
- `PartnerIntroCalEmbed` — thin wrapper over `@calcom/embed-react`
(already a dependency), reusing the existing `ContactCal` embed pattern
- `buildPartnerIntroPrefill` — pure mapping of applicant fields → Cal
prefill
- `PartnerApplicationSuccess` — presentational success view (heading +
subtitle + embed + dismiss)
- The wizard reports submitted-state up (`onSubmittedChange`) so the
modal widens only on the booking step
- New Lingui copy + regenerated catalogs (en/es/fr)
## Test plan
- `npx jest PartnerApplication` — green (prefill mapping, embed
link/layout/prefill, success view)
- `npx nx typecheck twenty-website` — clean
- `npx oxlint -c .oxlintrc.json` / `npx oxfmt --check` — clean
- Manual: submit the wizard → success step shows the wide booking
calendar, prefilled, dark theme, no event-details panel; "I'll book
later" closes the modal
## Notes
- Frontend only — no backend, schema, or submission-path change
- Cal link `rashad-twenty/partner-intro` lives as a constant in
`config.ts`
- Branch is currently behind `main`; happy to rebase before review
Fixes the **high-severity** `serialize-javascript` RCE advisory
(RegExp.flags / Date.prototype.toISOString, patched in **7.0.5**).
- Bumps the direct dep in `twenty-website` `^6.0.2 → ^7.0.5`.
- Only consumer is `src/lib/seo/JsonLd.tsx` (default-export API,
unchanged in v7 — the major only drops old Node support).
- `twenty-website` typecheck passes; lockfile regenerated under hardened
mode (`--immutable --check-cache` clean).