Commit Graph

513 Commits

Author SHA1 Message Date
Thomas des Francs 91b6cbd320 Update website release notes through 2.26 (#23736)
## Summary

- Add weekly, user-facing release notes from 2.1 through 2.26.
- Highlight completed, generally available product features and exclude
Labs or rollout-gated work.
- Update the Releases menu preview to the latest 2.26 entry.

## Before/After

Before: production ends at 2.0. After: the changelog includes weekly
releases through 2.26.

<img width="2268" height="720" alt="before-after"
src="https://github.com/user-attachments/assets/7ebf0154-596a-4846-b148-3d9aeff7cf0e"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23736?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. -->
2026-08-04 11:16:15 +02:00
Félix Malfait c25c0f4698 Add Enterprise plan and competitor pricing comparison pages (#23588)
Reworks the pricing surface in one PR: a new Enterprise tier on the
pricing page, five competitor pricing comparison pages, and the pricing
page section that links to them.

## Enterprise tier (pricing page)

- Third plan on both the cloud and self-hosting views: "from $50k /year"
with a "Talk to sales" CTA opening the existing contact modal.
- Cloud bullets: single-tenant isolation, IP allow-listing, SCIM
provisioning, dedicated support & SLA. Self-host: SCIM, air-gapped
deployment, LTS releases.
- Comparison table gains an Enterprise column (inherits Organization
values unless a row overrides) plus an Enterprise category of rows.
- New halftone building icon generated with the /halftone studio to
match the plan icon family.
- Pro and Organization pricing unchanged ($9/$19, in sync with Stripe).

## Competitor comparison pages

-
`/compare-pricing/{hubspot,salesforce,attio,pipedrive,microsoft-dynamics}`:
one shared template driven by a data file per vendor.
- Feature-by-feature cost table (competitor price + unlocking tier +
source link per claim, "checked on July 30, 2026" note, "spotted an
inaccuracy" link), a side-by-side bill for a 20-person team using the
PlanCard visual language with a savings badge cloned from the billing
toggle's -25% chip, and a one-line "fair play" note per vendor.
- Prices are 2026 list prices researched from vendors' public pricing
pages, billed annually. Routes registered in the website route registry
(indexed, sitemap, hreflang).

## Pricing page hub

- The salesfarce section intro becomes "Compare the real cost" with the
five comparison links using the footer's hover-marker link style. The
parody widget is unchanged.

Locale catalogs untouched for the i18n bot. Typecheck, lint, and the
website test suite pass.

Worth a second pair of eyes: the Pipedrive plan names/prices (2025
rebrand; their pricing page blocks fetchers) and the Salesforce "+30% of
spend" support/sandbox figures.
2026-07-30 18:54:30 +02:00
Rashad Karanouh 7fe59bf42d feat(website): send the referring partner with a client brief (#23351)
**Pairs with #23344** (`twenty-partners` v1.4.1), which adds the
`referredByPartner` relation and the Discord notification. This PR is
the sender; that one is the receiver.

**Merge #23344 first.** Its schema is a non-strict `z.object`, so an
unknown `partnerSlug` is stripped rather than rejected — shipping this
one first degrades silently (attribution dropped) rather than breaking,
but there is no reason to. Until #23344 is deployed, this field goes
nowhere.

No dependency in the other direction and no shared files: #23344 is
entirely inside `packages/twenty-apps`, this is entirely inside
`packages/twenty-website`.

## What this does

A visitor can reach the client brief form from two places: the
marketplace listing page, or a specific partner's profile. Until now
both produced an identical payload, so the partner whose page drove the
lead was lost.

This sends the partner's slug along with the brief when the form was
opened from a profile page. #23344 resolves it to a Partner record and
links it to the created Opportunity.

## How it flows

`PartnerProfileCtas` links to `/partners/brief?partner=<slug>` →
`page.tsx` reads and normalizes the param → prop threaded through
`ClientBriefPageContent` → `ClientBriefWizard` →
`buildClientBriefRequestBody`.

The slug is inert context, never a form field, so the wizard reducer and
`ClientBriefState` are untouched.

The three CTAs on `/partners/list` (`MarketplaceHeader`,
`MarketplaceMatchCard`, `MarketplaceBriefPrompt`) stay bare — a brief
from the listing page has no referring partner, and the notification
labels it "Marketplace listing".

## Why `normalizePartnerSlug` exists

`clientBriefRequestSchema` is a `z.strictObject`. Forwarding a malformed
`?partner=` value straight into the body would fail validation for the
**entire request** and lose the brief — a bad trade for an attribution
field the visitor never saw.

So the param is normalized at the boundary: array-valued params take the
first entry, and anything not matching `[a-z0-9-]{1,100}` is dropped to
`undefined` rather than passed on. The charset mirrors the app's
`slugify` helper, which is what produced the slugs in the first place.

## Testing

8 new cases — 6 for the normalizer (well-formed, absent, empty, bad
charset, over-long, repeated param) and 2 for the schema. Suite: 456
passing, up exactly 8 from a 448 baseline. `oxlint` and `oxfmt --check`
clean; `next build` compiles with no type errors.

Verified in a browser rather than asserted: opening a partner profile,
clicking "Submit a brief", and completing the wizard produces

```json
{"firstName":"Jane","lastName":"","email":"…","companyName":"NetZero Test Co","need":"…","partnerSlug":"netzero-systems"}
```

on `POST /api/client-brief` → 200. `LocalizedLink` preserves the query
string across locale prefixing (`localize-href.test.ts:20` already
covers this; confirmed live on the FR route).

## Deliberately not included

- **CTA-level attribution.** Which of the three listing-page CTAs was
used is not tracked. That is click analytics, a different concern from
partner attribution.
- **Length bounds on the other brief fields.** `country`, `seatCount`,
`timeline`, `budgetRange` and `companyName` remain unbounded, as they
were before this PR. Worth tightening, but pre-existing and out of scope
here.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23351?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. -->
2026-07-28 09:46:14 +00:00
github-actions[bot] f834b020b6 i18n - website translations (#23348)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23348?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: github-actions <github-actions@twenty.com>
2026-07-27 11:30:09 +00:00
github-actions[bot] 5948c167a0 i18n - website translations (#23196)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23196?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: github-actions <github-actions@twenty.com>
2026-07-27 09:55:17 +00:00
Abdullah. cada1ef6d7 feat(website): live community stats, drop hard-coded fallback (#23047)
## Problem

The menu's GitHub star and Discord member counts almost always render
the hard-coded snapshot (49.6K / 6.6K, frozen June 2026), not live
numbers. The render-time fetches run unauthenticated from Cloudflare
Workers, whose egress IPs are shared across tenants; GitHub's
unauthenticated quota is 60 req/hr per IP, so the call is effectively
always rate-limited. Live prod today shows the frozen 49.6K GitHub count
next to a live Discord count, confirming only GitHub is affected.

## Change

- GitHub fetch sends `Authorization: Bearer $GITHUB_STATS_TOKEN` when
the env var is set, moving it onto its own 5,000 req/hr quota. Discord
keeps the public invite endpoint, which works fine from Cloudflare's
IPs.
- The hard-coded fallback is deleted rather than refreshed.
`CommunityStats` fields are now `number | null`, resolved live ->
last-good -> null, and the menu renders an icon-only chip when a count
is genuinely unavailable. A fake number can never ship.
- Last-good values persist in the worker's existing OpenNext R2 bucket
(`NEXT_INC_CACHE_R2_BUCKET`, key `community-stats/latest.json` outside
the `incremental-cache/` prefix), so a third-party outage shows the
numbers from the previous refresh. No new infrastructure.
- Revalidation tightens from 1h to 15min (~4 GitHub calls/hour, shared
cache entry across pages).
- `MenuSocial`/`MenuDrawer` import `formatCompactCount` from its module
directly: the community barrel now re-exports server-only code, and a
client value-import would pull `getCloudflareContext` into the client
bundle.

## Rollout

- twentyhq/twenty-infra#795 passes the built-in Actions token at build
time so prerendered pages ship with a real star count from the first
request after deploy.
- One manual step: set the `GITHUB_STATS_TOKEN` secret (fine-grained
PAT, public read-only, no permissions) on the twenty-website-dev and
twenty-website-prod workers. Until it exists, behavior degrades to
today's minus the fake numbers.

## Tests

5 unit tests cover the resolution ladder: live wins, cache fills a
failed fetch, null on cold-cache failure, both-fail serves cache without
overwriting, nothing written when nothing succeeded. Verified against
the dev server: menu renders live 53.3K / 6.9K.
2026-07-24 12:44:05 +05:00
Rashad Karanouh 6b99bcea7f feat(partners): require Twenty experience on apply, drop Cal success (#23223)
## Summary
- Add a dedicated **Experience** step to the partner apply wizard
(milestones, ≥200-char narrative, proof URL) before commercials
- Stop collecting `applicationNotes`; rename Expertise chrome away from
“experience”
- Replace the post-submit Cal.com embed with a review-and-reach-out
thank-you so unqualified inbound no longer books intros automatically

**Companion PR (app):** #23224 — Partner schema, submit persistence,
triage views, Tally CSV mapper (`twenty-partners` v1.4.0). Land the app
PR with or before this one.

## Test plan
- [ ] Open apply modal: wizard order is identity → profile → expertise →
experience → commercials
- [ ] Experience step blocks continue without ≥1 milestone, narrative
≥200 chars, and a valid https URL
- [ ] Submit creates/updates Partner with the three experience fields
(with #23224 deployed)
- [ ] Success screen has no Cal embed / book-later CTA
- [ ] `npx jest --config=jest.config.mjs partner-application` passes
locally (71 tests)

---------

Co-authored-by: Abdullah <125115953+mabdullahabaid@users.noreply.github.com>
2026-07-24 09:40:22 +05:00
Rashad Karanouh 32041ce2e0 fix(website): restore condensed marketplace list card (#23161)
## Summary

The condensed marketplace list-card design (sans-serif partner name,
description excerpt, a single scope line, and a "View profile →" text
CTA) was the intended glowup card, shipped by #22471/#22402. During the
#23016 resync, the `PartnerCard.tsx` merge conflict was resolved in
favor of main's rich card, so the condensed design never actually landed
on `main`.

This PR restores the condensed design, ported onto main's current data
layer and helpers — none of #23016's profile/case-study/matching work is
touched.

## What changed

- `PartnerCard.tsx` — rewritten to render the condensed layout
(sans-serif `PartnerName`, `CardIntro` description excerpt, `CardFoot`
with a single `ScopeLine` and a mono `CardCta` "View profile →" text
link, no chip rows, no money row, no LinkedIn icon). Reuses main's data
layer and helpers directly: `richTextExcerpt`,
`resolvePartnerScopeCards`, `PartnerAvatar`, `titleCaseFallback`, and
the shared `CardFrame` shell (same entrance-animation/hover idiom
already used by `MarketplaceMatchCard`). No new helper files were needed
— everything the condensed design requires already exists on `main`.
- `PartnerChipRow.tsx` — deleted (zero remaining importers after the
port; grep confirmed only self-reference).
- `PartnerMoneyRow.tsx` — deleted (zero remaining importers after the
port; grep confirmed only self-reference).

Kept untouched: `fetch-live-marketplace-partners.ts`,
`marketplace-partner.ts`, `get-marketplace-partners.ts`,
`marketplace-partners-source.ts`, all
`PartnerProfile*`/case-study/matching files, `MarketplaceGrid.tsx`
(already has the `repeat(N, minmax(0, 1fr))` grid columns on main — no
change needed), and the shared label helpers
`PARTNER_SCOPE_LABELS`/`SERVED_GEO_LABELS`/`SPOKEN_LANGUAGE_LABELS`
(still used by `FilterBar.tsx`/`PartnerReachFacts.tsx`).

Website-only change — no partners-app version bump, no `.po` catalog
changes.

## Test plan

- [x] `npx nx typecheck twenty-website` — clean
- [x] `npx oxlint -c .oxlintrc.json .` (twenty-website) — 0 warnings, 0
errors
- [x] `npx oxfmt --check .` (twenty-website) — clean
- [x] `npx jest partners-marketplace` — 12 suites / 54 tests passing
- [x] Verified locally against the running dev server
(`/partners/list`): 16 partner cards render with the condensed layout
(sans-serif name, excerpt, single scope line, "View profile →"), no chip
rows/money rows/LinkedIn icons, real partner data renders correctly
(e.g. 01GROWTH, Inc)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23161?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. -->
2026-07-23 09:35:47 +05:00
github-actions[bot] cf5e6b7bad i18n - website translations (#23096)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23096?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: github-actions <github-actions@twenty.com>
2026-07-21 17:45:30 +02:00
Rashad Karanouh 6742cfe861 Marketplace glowup — live partner profiles, case studies & matching (website) (#23016)
Rebuilds the partners marketplace on live CRM-backed partner data: real
profiles, case studies, matching/scope cards, and a "match me" entry
point in the grid.

## What changed
- Marketplace grid and partner cards now fetch, rank, and filter live
partner data instead of static fixtures
- Partner profile pages render live profile data, including services,
portfolio/case studies, and clients
- Partner scope/matching cards on the profile page, plus a
`MarketplaceMatchCard` as the first tile in the marketplace grid,
routing into the client-brief flow
- Rich CTA rail on partner profiles (calendar link, website, socials)
built from live partner links
- Markdown rendering (`react-markdown`) for partner descriptions and
case study bodies, including proper heading rendering
- Minor route/sitemap adjustments to support the live-data pages

## Architecture / notes
This branch was 463 commits behind `main` and was resynced via a single
merge (not rebase) to avoid re-resolving the same conflicts repeatedly.
Several of the branch's earlier commits (client-brief wizard,
`MarketplaceBriefPrompt`, `MarketplaceMatchCard`'s base styling,
`PricingEngagementBand`) had already landed on `main` independently, in
some cases refactored into shared components (`EngagementBand`,
`MarketplaceCardFrame`, `createWebhookForwardingRoute`) — those
conflicts were resolved by taking `main`'s already-shipped version.
`PartnerCard.tsx` had diverged into two different designs (`main` gained
chip rows / money row / LinkedIn icon; this branch gained the live
case-study/portfolio data model with markdown descriptions and
structured partner links); the resolution keeps this branch's data model
(`description` as markdown, `links`/`linkUrls`) while adopting `main`'s
card layout, adapting field references accordingly.
`PartnerProfileCtas.tsx` keeps this branch's richer link-rail
implementation since it's the one that matches the live data model
already wired into `PartnerProfile.tsx`.

This is the website counterpart to app PR #22929 (glowup, v1.3.0),
already deployed to prod, and supersedes the closed drafts #22471 and
#22402.

Lint, format, targeted marketplace/client-brief jest tests, and `nx
typecheck twenty-website` all pass after the merge.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23016?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. -->
2026-07-21 11:03:46 +02:00
martmull 8d84a0b9f3 feat(app): allow non-admin developers to claim and list marketplace apps (#22621)
## Context

Follow-up to #22609. Lets a non-admin developer claim ownership of a
public Twenty app they published to npm, then request a marketplace
listing that a server admin reviews. Marketplace state is per-instance
for now.

## Claiming

- Developer tab gets a **Claim an application** section: look up an
unclaimed npm app by package name or universal identifier.
- Ownership is proven with GitHub OAuth against the package's npm
provenance (trusted publishing): the connected account must own the
GitHub account or organization the package was published from.
- Errors from the GitHub callback come back as a code and are shown
inline with a link to the relevant documentation.
- The old one-click claim stays admin-only.
- A **Sync catalog** button triggers a catalog refresh instead of
waiting for the hourly cron.
- Gated behind the `IS_APP_CLAIMING_ENABLED` feature flag.

## Listing requests

- Catalog-synced apps are created **unlisted**; a data migration unlists
previously auto-listed unclaimed npm apps (owned or vetted rows are left
untouched).
- Owners request a listing from the Distribution tab (logo + description
required); a server admin approves or rejects it from a **Listing
requests** section in the Admin Panel.

## Screenshots

<img width="1512" height="829" alt="image"
src="https://github.com/user-attachments/assets/788d4362-97c4-4e42-810c-ef1f11517bec"/>
<img width="1512" height="829" alt="image"
src="https://github.com/user-attachments/assets/d6246190-c82a-4f64-87be-3bb668527645"/>
<img width="1512" height="828" alt="image"
src="https://github.com/user-attachments/assets/21a8dad4-610b-4d1f-8948-b9acab40d373"/>
<img width="1512" height="829" alt="image"
src="https://github.com/user-attachments/assets/58246130-41f7-451e-ae7f-57bd21d04bb6"/>

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-07-20 15:43:40 +00:00
github-actions[bot] 80ff1716e5 i18n - website translations (#23005)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23005?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: github-actions <github-actions@twenty.com>
2026-07-17 21:20:33 +02:00
Rashad Karanouh ccbd3b6c46 Client brief wizard — /partners/brief (#22291)
## Brief — website (Release ① of the brief + glowup rollout)

Public client-brief wizard at `/partners/brief`, plus the marketplace
entry points (match-me card, brief prompt/link, brief CTAs across
partner surfaces).

**Backend already shipped:** the `submit-client-brief` logic function
merged in #22290 (v1.2.0) and is live in prod, so this PR is
**website-only** and needs no app deploy.

Rebased onto current `main` (was ~341 commits behind); lint + format
pass locally, typecheck/tests via CI.

### Release sequence (do not break)
1. **① Brief website — THIS PR.** Independent; backend already live in
prod. → merge → website deploy.
2. **② Glowup app → prod** (#22470). Rebase onto `main` (SDK 2.21),
apply deterministic-id handling, bump 1.2.10 → 1.3.0, `deploy` +
`install` on `partner-twenty-com`, set new app variables, refresh
partners-doc. **This is the gate for ③.**
3. **③ Glowup website** (#22471) — only **after ② is LIVE on prod** (it
reads the new partner links / services / case-study objects). → merge →
website deploy.
4. Reconcile #22637 (partners-traffic-web) with ③ — both touch
`partners-marketplace/*`.

Draft — do not merge until vetted.
2026-07-17 17:13:16 +00:00
Abdullah. b06f3c7f8f fix(website): query isVetted so the apps marketplace renders again (#22859)
## What

Production `/apps` renders no apps. The marketplace queries still
request `isFeatured`, but #22674 renamed that flag to `isVetted` across
the server schema (2-20 instance command renames the DB column — same
flag, same data, trust-signal semantics). Production (2.21) rejects the
query:

```
Cannot query field "isFeatured" on type "MarketplaceApp"
```

The transport throws on GraphQL errors, `fetchMarketplaceApps` catches
and falls back to `[]`, so the page renders the empty state.
`/apps/[slug]` detail pages degrade the same way.

## Fix

Rename `isFeatured` -> `isVetted` across the website marketplace module:
both queries, the API response types, the `MarketplaceApp` domain type,
and the vetted-first sort. 4 files, no behavior change beyond restoring
the data (same column, same values).

The catch-all `[]` fallback is intentionally left in place.

## Verification

- Corrected query run against `https://api.twenty.com/metadata`: returns
the 3 live apps (People Data Labs, Last contact, Call Recorder),
`isVetted: true`.
- Rename provenance confirmed: #22674 is a pure rename (paired diff,
symmetric column rename, `previousName: 'isFeatured'` marker on the
entity).
- `nx typecheck twenty-website` + `nx lint twenty-website` green.

Takes effect on the next website deploy (`force-dynamic` route, 300s
revalidate).
2026-07-13 13:56:35 +02:00
github-actions[bot] 9da5383289 i18n - website translations (#22683)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22683?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: github-actions <github-actions@twenty.com>
2026-07-09 09:12:46 +02:00
martmull 16e5d2b9d0 feat(website): add public apps marketplace with one-click install (#22611)
<img width="1335" height="570" alt="image"
src="https://github.com/user-attachments/assets/85bbe656-7525-4800-9ae6-bc59445ccf48"
/>
<img width="1512" height="723" alt="image"
src="https://github.com/user-attachments/assets/6494c539-a3e0-4e95-9df5-ee7559c098ab"
/>

<img width="1512" height="813" alt="image"
src="https://github.com/user-attachments/assets/4cb6adea-d35b-470a-9bfd-d47b2a514e8c"
/>
<img width="1512" height="739" alt="image"
src="https://github.com/user-attachments/assets/757ea3e0-bff9-4482-9a71-3e75949fd7e7"
/>
<img width="1468" height="790" alt="image"
src="https://github.com/user-attachments/assets/37365c56-47ae-4f08-a40f-32e5600a6c0f"
/>


## What

Adds a public `/apps` marketplace on twenty-website listing the vetted,
Twenty-built apps. Each app card and detail page has a one-click Install
button that deep-links to the in-app available-application page
(`app.twenty.com/settings/applications/available/:universalIdentifier`),
where authentication and permission consent are handled before
installing.

- New apps directory (`/apps`) and per-app detail pages
(`/apps/[slug]`), with a category filter
- Routing/sitemap entry for `/apps` and an Apps link in the Resources
menu
- The catalog is fetched from the Twenty GraphQL API (public
`publicMarketplaceApps` / `publicMarketplaceAppDetail` queries),
defaulting to `api.twenty.com`, and degrades to an empty state on
failure. No app info is duplicated in the website; logos and screenshots
come from the catalog's CDN URLs.

## Dependency

This is the website half of the split. It consumes the public queries
added in the server PR #22647, which should merge first.

---------

Signed-off-by: Parship Chowdhury <parshipchowdhury@gmail.com>
Co-authored-by: martmull <martin@twenty.com>
Co-authored-by: Paul Rastoin <45004772+prastoin@users.noreply.github.com>
Co-authored-by: nitin <142569587+ehconitin@users.noreply.github.com>
Co-authored-by: Aressand <97886962+Aressand@users.noreply.github.com>
Co-authored-by: Brahm Lower <bplower@gmail.com>
Co-authored-by: Thomas Trompette <thomas.trompette@sfr.fr>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
Co-authored-by: Parship Chowdhury <parshipchowdhury@gmail.com>
Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Pratik Mahajan <Pratik@mahajan.xyz>
Co-authored-by: neo773 <neo773@protonmail.com>
Co-authored-by: Deepak kumar maharana <100968930+deep231w@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@twenty.com>
Co-authored-by: Marie <51697796+ijreilly@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com>
2026-07-08 19:12:59 +00:00
Marie 8a4bcd1445 (Billing for self hosts) Tie enterprise key to server (#22464)
# Enterprise key: bind to a server, free dev instances, self-serve
transfer, shorter license

## Summary

Enterprise keys were being reused across multiple instances (e.g. one
prod + one dev, or several environments), which broke seat accounting
and made licensing ambiguous. This PR ties each enterprise key to a
**single server**, while giving customers a legitimate, self-serve way
to run a **free development instance** and to **move their key** when
they replace a server.

## Product behavior

### 1. Enterprise key is bound to one server
- The first server to validate an enterprise key **claims** it
(claim-on-first-use). From then on, that key is bound to that one server
(until unbound - see 3.).
- Any other instance that presents the **same key from a different
server is hard-rejected**: it does not receive a license, so enterprise
features stay off there.
- Each instance has a stable server identifier. If one isn't set, the
instance generates and persists one automatically on first validation
(in keyValuePair table), so existing customers generally don't need to
do anything (unless they have disabled config variables in db then they
should add it to .env).

### 2. Free development instance
- Every enterprise subscription gets **one free, non-billable
development instance** in addition to its production instance.
- An instance registers as development by declaring its instance type as
`development` (done by default when validating the enterprise key, then
can be toggled from UI or by updating value in keyValuePair table).
- The free dev slot is only granted while there is an **active
production instance** on the same subscription (so it's a perk for
paying customers, not a way to run for free).
- Only **one** dev instance can be active at a time per subscription,
and it is **not counted as a billable seat**.

### 3. Self-serve unbind / rebind (transfer)
- Admins can **release** the binding from the enterprise settings, which
frees the key so it can be **claimed by a new server**.
- This is the intended path when **sunsetting an instance and standing
up a new one** (migration, re-hosting, disaster recovery): release on
the old/dead box, then the new box claims it on its next validation.
- To prevent abuse, releases are **rate-limited (10 per rolling 30
days)**; hitting the limit shows a clear message.

### 4. Automatic release of dead servers
- If a bound server stops checking in for **14 days**, its binding is
considered stale and is **auto-released**, so a replacement can claim
the key without any manual step. This covers the case where the old
server is already gone and can't release itself.

### 5. Shorter license validity (30 → 7 days)
- The license (validity token) now expires after **7 days** instead of
30. The daily background refresh keeps healthy instances licensed
transparently.
- This limits the value of copying a license from one instance to
another, since a copied license now stops working within a week.

### 6. License issuance is rate-limited
- Issuing a new license is capped at **twice per 24h, independently for
production and for development**. This tolerates the normal daily
refresh (including small drift between runs) while blocking bursts of
license minting for cloned instances.
- Hitting this limit never revokes an existing, still-valid license —
the current one keeps working until it expires; the manual "refresh"
button just reports that the daily limit was reached.

## What changes for existing self-hosted customers

**If you run a single production instance with one enterprise key:**
nothing to do. On the next validation your instance reports its server
identifier, claims the binding, and keeps working.

**If you reuse one key across several instances (e.g. prod + dev, or
multiple environments):** only the **first** instance to validate keeps
its license. The others will **lose enterprise features**. To migrate:
- Keep your production instance as-is (it claims the binding).
- For a secondary/testing box, mark it as a **development instance**
(set the instance type to `development`) to use the free dev slot — no
extra cost.
- If you genuinely need multiple production instances, you'll need
**separate subscriptions/keys** for each.

**If you're replacing a server (decommissioning + rebuilding):**
- **Release** the binding from enterprise settings on the old instance,
then start the new one — it will claim the key automatically.
- If the old server is already gone, just wait for the **14-day
auto-release**, or contact support.

**Legacy instances that can't persist a server identifier
automatically:** set the server identifier explicitly in your
environment configuration (the instance logs a message telling you to do
so).

**Offline instances:** because licenses now last 7 days, an instance
that can't reach our licensing endpoint for more than a week will lose
enterprise features until it can check in again.

> A migration email will be sent to affected customers separately.

## Technical implementation (brief)

- Binding state lives in the **subscription's billing metadata** (bound
server id + last-seen timestamps for prod and dev, release timestamps,
and license-issuance timestamps). No new database is introduced on the
licensing side; the billing provider's subscription metadata is the
source of truth.
<img width="976" height="413" alt="metadata_3"
src="https://github.com/user-attachments/assets/ccc64822-e177-4223-a65a-4a4602aedf0e"
/>

- On each validation, a pure **binding resolver** takes the reported
server id + instance type + current metadata and returns `allowed` (with
the metadata to persist and whether the seat is billable) or `rejected`.
It handles claim-on-first-use, staleness/auto-release, the
dev-requires-active-prod rule, and the single-dev-slot rule.
- **Rate limits** (release + license issuance) use a shared
sliding-window helper stored as pruned timestamp lists in the same
metadata, so the metadata self-cleans and never grows unbounded. License
issuance uses **separate windows per instance type**.
- The self-hosted instance **generates and persists a server
identifier** if none is configured, and sends it (plus instance type) as
instance metadata on validation.
- A rejected binding returns a specific error code; the instance
**revokes its stored license** on that code. A license-issuance
rate-limit instead **throws a typed exception that surfaces to the
manual refresh** while leaving the existing license untouched; the daily
refresh job swallows it.
- License lifetime is a configurable duration (defaulted from 30 to **7
days**), clamped to the subscription's cancellation date when sooner.
2026-07-06 18:07:03 +02:00
Félix Malfait ed2b2f8911 feat: publish MCP & API discovery documents (well-known standards) (#22589)
## What & why

Makes Twenty's **MCP server** and **REST/GraphQL APIs**
auto-discoverable by catalogs (e.g. integrations.sh) and AI agents,
using vendor-neutral open standards rather than a proprietary manifest.

The tricky part is that Twenty is **multi-tenant and the REST OpenAPI is
generated per workspace** (it reflects each workspace's custom objects,
and with no token even the base schema is empty). So there is no single
public URL that describes the full API contract. This PR solves that
with two complementary layers.

## 1. Static standards on `twenty.com` (`twenty-website`)

The brand-level catalog entry, using `{your-workspace-url}` placeholders
since `twenty.com` is not a workspace host:

- `public/.well-known/mcp/server-card.json` — MCP Server Card (SEP-2127)
- `src/app/.well-known/api-catalog/route.ts` — RFC 9727 linkset (route
handler so the `application/linkset+json` content type survives the
global `nosniff` header)
- `public/llms.txt` — LLM-readable overview

## 2. Dynamic per-host serving from `twenty-server`

A new `well-known` core module serves the same documents built from the
**request host**, so every workspace subdomain, custom domain, and
self-hosted instance advertises its own **real, connectable** endpoints
(`https://{that-host}/mcp`, its live `/rest/open-api/core`, etc.) — no
placeholder:

- `GET /.well-known/mcp/server-card.json`
- `GET /.well-known/api-catalog`

Both are public + CORS + cached. The api-catalog's `service-desc` points
at each host's **live** per-workspace OpenAPI — the honest answer to
"it's generated per workspace" (real endpoint, real custom objects,
still token-gated). The `version` comes from `APP_VERSION`.

The two layers are complementary: the static one serves
catalog/marketing discovery at the brand domain; the dynamic one serves
connecting clients the real endpoints — which is where the MCP spec
expects the server card to live (same origin as `/mcp`).

## Refactor

Extracted the request→base-URL logic that `OAuthDiscoveryController` had
as a private method into a shared
`src/utils/get-request-base-url.util.ts`, now used by both it and the
new controller.

## Notes

- Docs URLs are sourced from the shared `DOCUMENTATION_BASE_URL`
(server) and the `SITE_URLS` registry (website) rather than hardcoded.
- MCP endpoint, transport (`streamable-http`), and protocol version
(`2025-06-18`) are read from the existing MCP constants.
- OAuth resource metadata (`/.well-known/oauth-protected-resource`)
already existed and is unchanged.

## Testing

- `twenty-server` unit tests for the builders and controller (host
derivation, version fallback, linkset shape) — passing.
- `nx typecheck twenty-server` — passing.
- `oxlint` + `oxfmt` clean on both packages; website `check-conventions`
OK.

https://claude.ai/code/session_01F6g7kefcfpjXSZjH6cwqhi

---
_Generated by [Claude
Code](https://claude.ai/code/session_01F6g7kefcfpjXSZjH6cwqhi)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22589?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. -->
2026-07-06 17:53:05 +02:00
Abdullah. 049a98a36a fix(website): restore footer CTA buttons on the light card inside the dark stage (#22447)
## What

The footer's **Talk to us** / **Get started** CTAs regressed: filled
rendered white-on-white (only the label showed) and outlined vanished
entirely. Resolves
[this](https://github.com/twentyhq/core-team-issues/issues/2634) issue.

## Why

#22241 marked the footer root as a dark menu-surface
(`data-scheme="dark"`) so the sticky menu adapts over the dark footer
stage. But the footer's content sits on a **white Card inside that
root**, and the button's dark override is a *descendant* selector
(`[data-scheme='dark'] &`) — so it leaked into the card. Filled → white
fill + black label (invisible fill on white); outlined → white stroke +
white label (fully invisible). The card's text was fine because it uses
the light default semantic vars; only the buttons key off the raw
attribute.

## Fix

- Mark the white `Card` as `data-scheme="light"` — it *is* a light
surface. The root keeps `data-menu-surface`/`data-scheme="dark"`, so
**menu adaptation is unchanged**.
- Add a button override scoped to `[data-scheme='dark']
[data-scheme='light'] &` — a light surface *nested inside* a dark one.
It's higher specificity than the dark rule and matches **only** this
footer case, so a dark card nested in a *light* section (e.g.
`HelpedCard`) is never affected. No other button changes.

Result: filled = black fill + white label, outlined = black stroke +
black label — matching the design.

## Testing

- `nx typecheck twenty-website` ✓ · `nx lint twenty-website` ✓
(check-conventions + oxlint + oxfmt)
- Reviewable on the PR preview (footer CTAs, plus menu/FAQ/hero/signoff
buttons unaffected).
2026-07-02 16:31:59 +05:00
Abdullah. 63d092a31b fix(website): center the User Guide nav preview image (#22445)
## What

The **User Guide** item in the Resources dropdown rendered its preview
image off-center (anchored top-left with a gap below the halftone).

## Fix

Set `imagePosition: 'center'` on the User Guide preview so the halftone
book is centered and fills to the bottom of the frame, matching the
others.

## Testing

- `nx lint twenty-website` ✓ (check-conventions + oxlint + oxfmt)
- `nx typecheck twenty-website` ✓
2026-07-02 14:47:48 +05:00
github-actions[bot] 8b6bd34a17 i18n - website translations (#22436)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22436?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: github-actions <github-actions@twenty.com>
2026-07-02 08:53:53 +00:00
Abdullah. 3474d75b56 feat(website): add Product to menu + footer nav, move Why into Resources dropdown (#22429)
## What

Restores the **Product** link to the site nav (removed in #21794),
reversing that commit's nav-structure change:

- **Menu** — Product is the first top-level item (in place of Why);
**Why** moves back into the **Resources** dropdown with its `why.webp`
preview (`IconBulb`, "Why teams choose Twenty").
- **Footer** — Product added to the **Sitemap** group (after Home).

The `/product` and `/why-twenty` routes already exist and are in the
sitemap; only the nav data changed. The rest of #21794 (dropdown frame
height, preview assets, current-page highlight) is untouched.

## Notes

- New `msg` strings (`Product`, and Why's restored strings) are left to
CI / the i18n bot to extract + translate — no catalog changes here.

## Testing

- `nx lint twenty-website` ✓ (check-conventions + oxlint + oxfmt)
- `nx typecheck twenty-website` ✓
2026-07-02 10:46:36 +02:00
Abdullah. 1b06532cb1 fix(website): tighten product-feature spotlight height and align bento spacing (#22428)
## What

Design polish on the product page's `ProductFeature` bento, from
designer review:

- **Spotlight height** — the first (spotlight) card's visual was
`min-height: 420px` on desktop vs the grid cards' `340px` (80px taller),
so it towered over the rest. Now **340px**, matching the grid cards.
- **Spacing consistency** — the spotlight visual used a uniform `margin`
(bottom margin included), unlike the other cards' `CardVisualFrame` (`…
0` bottom). Removed it so the visual→content gap is consistent across
every card.
- **Gap** — bumped the visual→content gap to `spacing(6)` (**24px**) on
desktop for all cards.

## Testing

- `nx lint twenty-website` ✓ (oxlint + oxfmt + check-conventions)
- `nx typecheck twenty-website` ✓
2026-07-02 10:45:45 +02:00
Abdullah. e69c3ae5ce fix(website): bump @opennextjs/cloudflare to 1.20.0 (R2 deploy on Node 24) (#22266)
## 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.
2026-06-28 16:18:31 +00:00
github-actions[bot] 0569c490fd i18n - website translations (#22264)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22264?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: github-actions <github-actions@twenty.com>
2026-06-28 16:06:10 +00:00
github-actions[bot] f8a9a2b249 i18n - website translations (#22263)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22263?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: github-actions <github-actions@twenty.com>
2026-06-28 13:31:10 +00:00
Abdullah. 012af11d77 feat(website): ship all documentation locales (multi-locale site) (#22257)
## 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).
2026-06-28 14:39:59 +05:00
Abdullah. 77c84815ef feat(website): rework the product-stepper Layout visual (#22249)
## 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"
/>
2026-06-27 17:41:34 +02:00
github-actions[bot] 4840233a1c i18n - website translations (#22244)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22244?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: github-actions <github-actions@twenty.com>
2026-06-27 14:06:24 +02:00
Abdullah. 03f3789d13 feat(website): menu adapts to the section beneath it (#22241)
## 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.
2026-06-27 12:58:50 +05:00
github-actions[bot] bf7a766935 i18n - website translations (#22234)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22234?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: github-actions <github-actions@twenty.com>
2026-06-26 22:29:54 +02:00
github-actions[bot] ad1ac4f3c5 i18n - website translations (#22225)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22225?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: github-actions <github-actions@twenty.com>
2026-06-26 22:17:08 +02:00
Abdullah. d58ec64e10 feat(website): swap product Demo for a register Signoff; make signoff headings width-driven (#22232)
## 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.
2026-06-26 18:45:39 +02:00
Abdullah. fea2b8736f feat(website): rebuild dashboard visual faithful to twenty-front (#22218)
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"
/>
2026-06-26 20:57:21 +05:00
Félix Malfait 7caf2a14c5 feat(website): show partner location instead of served regions in marketplace (#22224)
## 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. -->
2026-06-26 16:55:49 +02:00
Abdullah. 1b4bddba6f fix(website): polish product hero AI transition (panel reveal timing + title wrapping) (#22223)
## 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.
2026-06-26 19:43:45 +05:00
github-actions[bot] e60d790990 i18n - website translations (#22203)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22203?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: github-actions <github-actions@twenty.com>
2026-06-26 10:25:37 +02:00
Abdullah. 85f64abb28 Frameless ProductStepper with twenty-front-faithful scenes (#22197)
## 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
2026-06-26 10:17:44 +02:00
github-actions[bot] 1284099940 i18n - website translations (#22162)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22162?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: github-actions <github-actions@twenty.com>
2026-06-25 13:19:57 +02:00
github-actions[bot] a87bc4bed6 i18n - website translations (#22160)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22160?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: github-actions <github-actions@twenty.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-25 12:31:53 +02:00
Abdullah. 5a657129f0 Split website stepper into home-stepper and product-stepper sections (#22149)
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.
2026-06-25 12:24:22 +02:00
github-actions[bot] 904d3d6bff i18n - website translations (#22158)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22158?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: github-actions <github-actions@twenty.com>
2026-06-25 12:20:32 +02:00
Abdullah. 551a792291 Rework website TasksVisual to match twenty-front tasks tab (#22143)
- 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"
/>
2026-06-25 12:20:18 +02:00
Abdullah. d6556221d9 Tidy website ImportVisual markup and CSS (#22148)
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"
/>
2026-06-25 15:12:08 +05:00
Abdullah. 491b9e954a Rework website EmailsVisual to match twenty-front emails tab (#22146)
- 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"
/>
2026-06-25 15:02:44 +05:00
Abdullah. 20f8321dca Rework website FilesVisual to match twenty-front files tab (#22147)
- 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`.
2026-06-25 10:43:51 +02:00
Rashad Karanouh feac2df216 chore(website): remove partners marketplace route lint guard (#22121)
## 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. -->
2026-06-25 05:12:12 +00:00
Rashad Karanouh 500f441807 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. -->
2026-06-24 18:48:03 +02:00
Raphaël Bosi 85406a58fb Use minimal babel presets for wyw to fix the website Cloudflare build (#21994)
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.
2026-06-23 13:25:48 +02:00
github-actions[bot] 16c9782c96 i18n - website translations (#22001)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22001?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: github-actions <github-actions@twenty.com>
2026-06-23 13:17:15 +02:00