96ea1e1ffc85d7b16cea989a15d4a2ad13f3175a
120 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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.
|
||
|
|
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. --> |
||
|
|
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> |
||
|
|
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. --> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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. |
||
|
|
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. -->
|
||
|
|
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).
|
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. --> |
||
|
|
c98620e14c |
Rework website PipelineVisual to match twenty-front kanban (#21989)
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" /> |
||
|
|
5ce91e711c |
fix(website): render partner marketplace dynamically to stop profile 404s (#21963)
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. --> |
||
|
|
569d887d1e |
[Website] Cut over to the rebuilt site (#21825)
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. |
||
|
|
9de1b6330c |
feat(website - self hosts billing): add internal endpoint to reissue enterprise keys (#21660)
## 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> |
||
|
|
7afc991bd6 |
Partner application form: required fields, skills rework, fail-fast validation (#21710)
## 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. --> |
||
|
|
c05c34aaec |
[Website] Convert remaining images to WebP and compress some current ones. (#21404)
As title. Also deleted some unused assets that are no longer required. |
||
|
|
4db049db27 |
[Website] Product hero rework into a single, scroll-driven story. (#21105)
This reworks the /product hero into a single scroll-driven story. It
opens on a collaborative CRM intro — an autoplay cursor ("Alice") tours
real app-preview surfaces (Companies → an Anthropic record, clicking
through its tabs → People → Notes) — then, as you scroll, the light
intro morphs into a dark AI section via one continuous color wipe that
flows up through the (transparent) navigation menu. The AI section
presents four "build" pillars (pipeline board, follow-up tasks, sales
dashboard, email-sequence workflow), each playing an agentic preamble
(thinking + tool steps) and skeleton-loading state before revealing the
finished artifact as a finale. All preview surfaces — table, kanban,
record, dashboard charts, and workflow — are rendered for real and
matched to twenty-front's design (step-8 chart palette, default
opportunity stages, etc.) rather than static images.
Beyond the core morph, this PR layers in the coherence and polish work:
- the intro halftone reuses the AI section's pattern tinted to the brand
blue and scoped to the visual;
- the workflow finale draws its edges/labels in as a staged build;
- AI response copy is kept consistent with the visuals (matching stage
names and task counts);
- the ARR line chart fades in cleanly instead of flashing a partial
draw;
- the experience resets sensibly on scroll — the intro cursor tour
restarts when you scroll back to it, and the AI section returns to its
first tab once you're back at the top.
Here is the video showcasing the experience:
https://github.com/user-attachments/assets/c20708ed-4435-4f1a-a1c6-308b99305a97
Awaiting confirmation about the experience from @Bonapara before
merging.
|
||
|
|
b60a91a075 |
fix(website): render releases from local notes, drop GitHub gate (#21238)
The releases page gated visible notes on fetchLatestGithubReleaseTag(); when that unauthenticated GitHub call was rate-limited at build (common on shared Cloudflare build IPs, no GITHUB_TOKEN), it returned null and getVisibleReleaseNotes returned [], rendering 'No releases are visible yet for the current published version' — the exact prod symptom. force-static made each deploy a coin flip. Committed MDX (+images) is the single source of truth, so gate visibility on nothing: render all local notes deterministically with no network call. Removes the dev/prod divergence and deletes the now-dead fetch-latest-release-tag.ts and get-visible-releases.ts. |
||
|
|
5a55021e26 |
feat(website): add /partners/apply standalone page (#21219)
## Summary - Adds a standalone `/partners/apply` page — a shareable URL that opens the partner application wizard full-page (no modal, no nav, no footer), on a plain black background - Adds a `slots` prop to `PartnerApplicationWizard` so it can render outside a `Dialog.Root` context (Base UI), keeping the existing modal on `/partners` completely untouched - Surfaces logic function errors to the user: the API route now checks the webhook response body for `ok: true`, so a silent backend failure no longer shows a false success state ## Test plan - [ ] `yarn jest --no-coverage` — 365/365 passing - [ ] `npx oxlint -c .oxlintrc.json .` — 0 errors (1 pre-existing warning unrelated to this PR) - [ ] `npx oxfmt --check .` — clean - [ ] `npx tsc --noEmit` — clean - [ ] Visit `/partners/apply` — wizard loads full-page on black background, no menu, no footer - [ ] Complete the wizard and submit — redirects to `/partners/list` - [ ] Visit `/partners` — "Become a partner" modal still opens normally |
||
|
|
6b5a956c85 |
feat(website): partner marketplace UI fixes & CTA improvements (#21163)
## Summary Partner marketplace UI fixes and CTA improvements on the website (`twenty-website`). ## Changes - **Find a partner buttons** → link to `/partners/list` (hero + signoff sections) instead of opening the contact modal. - **Chip row layout fix** → align partner chip rows to the first chip's baseline. In the narrow 3-column grid, category chips wrap to multiple lines; centering floated the label to the middle and visually lifted the first chip into the row above (e.g. "Custom Development" appeared under Languages). Baseline keeps the label pinned beside the first chip. - **Card CTA** → replace the conditional "Book a call" calendar CTA with a "View profile" link to the partner profile page, shown on every card regardless of whether a calendar link exists. Keeps card heights consistent across the grid. - **Card avatar** → show the partner's real profile picture when a safe `http(s)` URL is present, keeping the initials block as the fallback. - **Profile "Contact <partner>" CTA** → when a partner has no booking (calendar) link, show a "Contact <partner>" button (alongside LinkedIn if present) that opens a `mailto:rashad@twenty.com` with a partner-named subject and a pre-filled body prompt. ## Tests - New: `PartnerAvatar`, `PartnerProfileCtas` unit tests. - Updated: `PartnerCard` tests for the View-profile CTA. - All `Partner*` website tests pass. ## Screenshots <img width="1440" height="816" alt="Screenshot 2026-06-02 at 23 40 55" src="https://github.com/user-attachments/assets/bd88a9be-3297-4d7f-891c-c9d403d2b4d9" /> <img width="1441" height="818" alt="Screenshot 2026-06-02 at 23 40 47" src="https://github.com/user-attachments/assets/b315424f-48d1-4d29-9e97-1fcf4d8c47f2" /> <img width="458" height="505" alt="Screenshot 2026-06-02 at 23 40 18" src="https://github.com/user-attachments/assets/2a4e0ec2-8e0a-4220-84ec-177c788aa580" /> |
||
|
|
7e034f711f |
feat(website): surface partner Categories (partnerScope) in marketplace, drop deploymentExpertise facet (#21127)
## What Rebinds the marketplace's expertise facet from `deploymentExpertise` (Cloud / Self-host) to **`partnerScope`** — the five partner Categories: Advisory & Discovery · Solutioning · Custom Development · Hosting & Infrastructure · Training & Adoption. Moves the card chip, the profile facts row, the dropdown filter, the `?categories=` URL param, and the API-boundary normalization onto `partnerScope`. The standalone Cloud/Self-host facet is **dropped** (hosting is now the `HOSTING` category), per the harmonization decision. ## Depends on - The app exposing `partnerScope` — companion app PR #21126. - The new `partnerScope` options + data migration — signup app PR #21040. ## Tests TDD red→green on: `filter-partners`, both API normalizers, `filter-url-helpers`, `PartnerCard`, `use-filter-state`. 53/53 pass; typecheck + lint + format clean. ## Merge order (we'll decide) Independent diff. Suggested last of the four, after the signup PRs (#21039 / #21040) and the app PR (#21126). Run `lingui:extract` once after #21039 merges so the `.po` files don't conflict twice. Deploy the app + migrate before the website ships. |
||
|
|
d0e0e27035 |
[Website] Partner application wizard + logic-function handover (#21039)
## Summary Replaces the single-screen partner-application modal with a **4-step wizard** on the public form, and points the route's upstream at the new `submit-partner-application` HTTP logic function in the twenty-partners SDK app. After design review, the Expertise step landed on the validated **Category + Skills** model: a small set of *stable* macro categories the partner operates in, plus a *free, semi-structured* Skills field for the concrete things that differentiate them (React, SAP, Shopify, …). Companion PR (partners-app side): #21040 ## ⚠️ Deployment notes Before this can ship to prod, the website worker needs a new env var: - **Add `PARTNER_APPLICATION_SECRET`** to the deploy config at https://github.com/twentyhq/twenty-infra/tree/main/cloudflare/website. Without it the route returns `503` ("Partner application endpoint is not configured."). - The value must **match** the `PARTNER_APPLICATION_SECRET` workspace variable set in the partners workspace UI (Settings → Apps → Twenty Partners → Variables) — that's how the handler authenticates the incoming `X-Application-Secret` header. - `PARTNER_APPLICATION_WEBHOOK_URL` also needs repointing from the TFT webhook to the logic-function URL (`https://partner.twenty.com/s/partner-applications` or equivalent) at the same time. ## Wizard - 4 steps inside `Modal.Root`: **Identity → Profile → Expertise → Commercials**. Step-dot indicator, per-step required-field gating, reset on close. The big serif hero shows **only on step 1**; later steps use the compact `STEP n OF 4 · NAME` strip to reclaim vertical space. - **Profile** captures Type of team (Solo/Agency), LinkedIn, City, Country, Languages. Country uses the searchable Select (placeholder-only label). - **Expertise = Category + Skills + Notes:** - **Category** — multi-select cards over 5 macro categories (`ADVISORY`, `SOLUTIONING`, `DEVELOPMENT`, `HOSTING`, `SUPPORT`), each with a one-line description + examples. (Replaces the old draft `partnerScope` enum; the backend keeps the field name — see #21040.) - **Skills** — free tag input with a clickable suggestion row + keyboard autocomplete (↑/↓/Enter/Esc) and "add your own". Empty by default. - **Notes** — one free textarea (merges the former `workspaceUrl` + `customerReferences`), reviewed manually. - `deploymentExpertise` removed from the form (covered by the Hosting category). - **In-modal success view** on submit ("Thanks, / we'll be in touch!") with a Close button — replaces the old silent close. - Removes the partners-page "Which partner program is right for you?" three-cards section. ## Design-system primitives - **`Form.Select`** — searchable popup whose dropdown is **portaled to `<body>`** (fixed, anchored to the trigger, flips up, height-capped) so the modal's `overflow`/`transform` can't clip it; pointer events are stopped so clicking inside it doesn't dismiss the dialog. - **`Form.TagInput`** — optional `suggestions` prop adds the suggestion row + autocomplete menu (used by Skills); behaviour unchanged when no suggestions are passed. - **`CategoryCardSelect`** — compact multi-select cards. - `Form.MultiSelect`, `Form.Currency`. ## Validation & payload - **Single validation source:** client and server share Zod field schemas (`partner-application-field-schemas.ts`). The reducer validates via those instead of hand-rolled regexes, so client and server agree by construction (e.g. both reject non-TLD URLs). - **Typed request body:** `buildPartnerApplicationRequestBody(state)` returns a typed `PartnerApplicationRequest` (unit-tested); `handleSubmit` just serializes it. - Payload is camelCase matching the logic-function input; `applicationNotes` replaces `workspaceUrl`/`customerReferences`. - Auth: the upstream call carries an `X-Application-Secret` header backed by `PARTNER_APPLICATION_SECRET` (handler-enforced — the SDK's `isAuthRequired` only accepts user-session JWTs, not workspace API keys). The webhook-URL env uses `z.url()` (not `z.httpUrl()`) so `http://localhost:2020/...` dev destinations parse. ## Demo 📹 _Screen recording of the wizard end-to-end (open → walk steps → submit → Partner record lands):_ https://github.com/user-attachments/assets/7458dd86-e3ff-47b5-9878-0eb134ff38e3 ## Tests - **62 passing** across reducer, Zod schema, route, the new payload-builder suite, and Form helper suites. `npx tsc` clean, `nx lint:diff-with-main` clean, Lingui catalogs regenerated (French slots are a follow-up). ## Test plan - [ ] `/partners` → "Become a partner" → wizard opens on Step 1 (full hero) - [ ] Identity: name / work email / company → Next - [ ] Profile: pick **Type of team**; search country ("fra" → France); pick languages → Next (compact header from here on) - [ ] Expertise: select 1+ **Category** cards; add **Skills** (click a suggestion, type one + Enter, drive the ↑/↓ autocomplete); optionally fill **Notes** - [ ] Country dropdown opens without being clipped by the modal, and clicking inside it does **not** close the wizard - [ ] Commercials → Submit → **in-modal "Thanks, we'll be in touch!"**; Network shows POST `/api/partner-application` `200` - [ ] Partner record lands with the chosen categories in `partnerScope`, plus `skills`, `applicationNotes`, `slug` from company, `reviewed: false`, `partnerTier: 'NEW'` - [ ] Re-submit same email + different city → Partner updates; `validationStage`/`reviewed`/`partnerTier` preserved - [ ] Back/Next preserves entered values; Reset on close; mobile single-column / chips wrap --------- Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> |
||
|
|
b027e4bdb1 |
[Website] i18n module, page-local sections, translatable copy (#21082)
**i18n** — collapsed the ~22 scattered i18n files into a single module and turned on Spanish alongside French. **Sections** — dropped the old compound pattern (`Section.Root`, `Section.Heading`, …). Reusable layout shells moved to `src/templates/`, atomic bits stay in `design-system/`, and each page now owns its copy in local `_components` blocks instead of pulling it out of shared sections. Data files hold arrays only, no prose. **Copy → `<Trans>`** — A lot of headings were split across several `<HeadingPart>`s just for font styling, which meant each piece was a separate translation string. A translator got "Build your Enterprise CRM" and "at AI Speed" as two unrelated strings and had no way to reorder them for their language. Those are now single `<Trans>` units with placeholders. Same idea for the old `\n` + `white-space: pre-line` line-break trick: replaced with a small `ResponsiveLineBreak` element so the break is doesn't quietly rot, and did a dead-code pass. The de-fragmentation changes the message IDs, so around 60 strings will fall back to English in fr/es until Crowdin re-syncs. |
||
|
|
13f09d8946 |
[Dashboards] Remove gauge chart types and code (#20410)
Follow-up cleanup to #20172. |
||
|
|
4e0b69eb8d |
[Website] force-static releases page & move workspace bullet to pro self-host (#20834)
Add `export const dynamic = 'force-static'` to the releases page to prevent runtime re-renders on Cloudflare Workers where fs is unavailable, which caused stale "Releases were not found" errors after ISR cache eviction. Also moves the "Up to 5 workspaces" bullet from the Organization plan to the Pro plan on the self-hosting pricing view. |
||
|
|
07a20cba5e |
[Website] Partners directory (#20632)
## What this PR does
Adds the **Partners Marketplace** page to the Twenty marketing website
(`/partners-marketplace`), built with Next.js App Router. The page
fetches live partner data from the Twenty API and presents it in a
responsive grid with an interactive filter bar.
## Partners grid
- Fetches partners from the `/s/partners` endpoint via a typed
`getPartners()` server-side fetcher
- Responsive 1 → 2 → 3 column grid (mobile / tablet / desktop)
- Each card shows name, region eyebrow, intro text, chip rows (Regions /
Languages / Deploys), and a Calendly CTA
- Stagger entrance animation (700ms cascade, respects
`prefers-reduced-motion`)
## Filter bar
- Three facets: **Region**, **Language**, **Deployment** — multi-select
chips
- **Selection model:** OR within a facet, AND across facets (e.g.
`Europe OR US` AND `French`)
- Filter state lives in URL search params
(`?regions=EUROPE,US&languages=FRENCH`) — filtered views are shareable
and browser-back works correctly
- Client-side filtering — no server round-trip per interaction
- Result count ("Showing 3 of 8 partners") updates live with
`aria-live="polite"`
- "Clear filters" button resets all facets in one URL update, only shown
when filters are active
- Empty state ("No partners match your filters") replaces the grid when
nothing matches
- 200ms opacity fade-out on card removal; initial stagger animation
preserved on first load
- `prefers-reduced-motion: reduce` disables all transitions
## Architecture
- `page.tsx` stays a **Server Component** — fetches partners
server-side, all partner HTML is in the initial response for SEO
- `<MarketplaceClient>` is the client boundary — owns filter state via
`useFilterState()` (backed by `useSearchParams`)
- Canonical URL set in page metadata so `?regions=...` deep-links don't
get indexed as duplicates
- `<Suspense>` wrapper around `MarketplaceClient` for Next.js 15
`useSearchParams` compliance
- No new npm dependencies
## Test coverage
31 tests across three suites:
- `filter-partners.test.ts` — pure filter logic (OR / AND semantics,
empty results)
- `filter-url-helpers.test.ts` — URL param encode / decode / toggle /
round-trip
- `use-filter-state.test.tsx` — hook behaviour with mocked
`next/navigation`
## Screenshot
<img width="1783" height="1196" alt="Screenshot 2026-05-17 at 15 01 54"
src="https://github.com/user-attachments/assets/9dddf827-f440-4cad-8ec3-81ede6d46434"
/>
## Test plan
- [ ] Navigate to `/partners-marketplace` — all live partners render
- [ ] Click a Region chip — URL updates with `?regions=...`, cards
filter, count updates
- [ ] Click the same chip again — selection removed, all cards return
- [ ] Select chips from two different facets — AND behaviour narrows
results correctly
- [ ] Trigger empty state (e.g. filter to a region with no partners) —
empty state shown with "Clear filters" button
- [ ] Click "Clear filters" — all cards return, URL cleared
- [ ] Deep-link to `?regions=EUROPE&languages=FRENCH` — page loads with
filters applied
- [ ] Browser back button restores previous filter state
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||
|
|
8826d12a18 |
[Website] Host customer story hero images locally and fix multi-segment redirects (#20790)
Customer story page shows the following error, which I believe leads to an internal server on the individual customer story pages. <img width="636" height="75" alt="image" src="https://github.com/user-attachments/assets/fc9ede75-fd3b-4538-8211-8182f4a99b9b" /> This PR replaces remote URLs of those images with local copies to avoid a 404 issue. Will test once deployed on dev to confirm if the error is resolved, but locally, I do not see console errors any longer after this change. There is some duplicated copy that I found upon audit which can be made DRY, but I will resolve it in a separate PR to keep this PR single-responsibility. |
||
|
|
658bdf3e57 |
chore(website): rename twenty-website-new → twenty-website (#20745)
## Summary Follow-up to the Cloudflare/OpenNext migration (#20741). Now that the legacy `twenty-website` package was already removed in #20270, the `-new` suffix on the marketing site package is no longer meaningful. ## What changes - **Directory rename**: `git mv packages/twenty-website-new packages/twenty-website` (1213 files moved, no content change) - **Package + nx config**: `package.json` and `project.json` name fields updated, `sourceRoot` repointed - **Source refs**: `load-local-articles.ts` and `load-local-release-notes.ts` had a hardcoded `'twenty-website-new'` segment in their monorepo-root fallback path; `app/[locale]/releases/page.tsx` had display strings showing where to add content - **External refs**: root `package.json` workspaces, root `CLAUDE.md` / `README.md`, `twenty-sdk` + `create-twenty-app` READMEs, `.vscode/twenty.code-workspace`, `.cursor/rules/changelog-process.mdc`, Crowdin config + the three `website-i18n-*` CI workflows + `ci-website.yaml` - **Docker cleanup**: `packages/twenty-docker/twenty-website-new/Dockerfile` deleted; the two Makefile targets (`prod-website-new-build` / `prod-website-new-run`) that referenced it removed — EKS deploy was retired in the Cloudflare migration - **`yarn.lock`** regenerated against the new workspace path ## What's deliberately not in this PR The dev hostname `website-new.twenty-main.com` in `wrangler.jsonc` stays for now. Migrating it to `website.twenty-main.com` needs coordinated DNS deletion (current CNAME points at the legacy Docusaurus NLB and serves 503s) and removal of the matching legacy `website` Helm chart in `twenty-infra`. Flagged as a separate cleanup. Companion infra PR: https://github.com/twentyhq/twenty-infra/pull/682 (workflow paths + Terraform ECR + docs) ## Test plan - [x] `yarn install --immutable` resolves clean against the new path - [x] `npx nx typecheck twenty-website` passes - [x] `npx nx lint twenty-website` passes - [ ] CI on this PR confirms the same on a fresh checkout - [ ] After merge: trigger `Deploy Website` workflow against `environment=dev` to confirm the renamed working-directory deploys correctly |
||
|
|
59107b5b23 | Remove twenty-website package. (#20270) | ||
|
|
c1da7be6d7 |
Billing for self-hosts (#18075)
## Summary Implements enterprise licensing and per-seat billing for self-hosted environments, with Stripe as the single source of truth for subscription data. ### Components - **twenty-website** hosts the private key to sign `ENTERPRISE_KEY` and `ENTERPRISE_VALIDITY_TOKEN`. It communicates with Stripe to emit the daily `ENTERPRISE_VALIDITY_TOKEN` if the subscription is active, based on the user's Stripe subscription ID stored in `ENTERPRISE_KEY`. - **Stripe** is the single source of truth for subscription data (status, seats, billing). - **The client** (twenty-server + DB + workers) saves `ENTERPRISE_KEY` in the `keyValuePair` table (or `.env` if `IS_CONFIG_VARIABLES_IN_DB_ENABLED` is false) and the daily-renewed `ENTERPRISE_VALIDITY_TOKEN` in the `appToken` table. `ENTERPRISE_VALIDITY_TOKEN` is verified client-side using a public key to grant access to enterprise features (RLS, SSO, audit logs, etc.). ### Flow 1. When requesting an upgrade to an enterprise plan (from **Enterprise** in settings), the user is shown a modal to choose monthly/yearly billing, then redirected to Stripe to enter payment details. After checkout, they land on twenty-website where they are exposed to their `ENTERPRISE_KEY`, which they paste in the UI. It is saved in the `keyValuePair` table. On activation, a first `ENTERPRISE_VALIDITY_TOKEN` with 30-day validity is stored in the `appToken` table. 2. **Every day**, a cron job runs and does two things: - **Refreshes the validity token**: communicates with twenty-website to get a new `ENTERPRISE_VALIDITY_TOKEN` with 30-day validity if the Stripe subscription is still active. If the subscription is in cancellation, the emitted token has a validity equal to the cancellation date. If it's no longer valid, the token is not replaced. The cron only needs to run every 30 days in practice, but runs daily so it's resilient to occasional failures. - **Reports seat count**: counts active (non-soft-deleted) `UserWorkspace` entries and sends the count to twenty-website, which updates the Stripe subscription quantity with proration. Seats are also reported on first activation. If the subscription is canceled or scheduled for cancellation, the seat update is skipped. 3. `ENTERPRISE_VALIDITY_TOKEN` is verified server-side via a public key to grant access to enterprise features. ### Key concepts Three distinct checks are exposed as GraphQL fields on `Workspace`: | Field | Meaning | |---|---| | `hasValidEnterpriseKey` | Has any valid enterprise key (signed JWT **or** legacy plain string) | | `hasValidSignedEnterpriseKey` | `ENTERPRISE_KEY` is a properly signed JWT (billing portal makes sense) | | `hasValidEnterpriseValidityToken` | `ENTERPRISE_VALIDITY_TOKEN` is present and not expired (expiration depends on signed token payload, not on "expiresAt" on appToken table which is only indicative) | Feature access is gated by `isValid()` = `hasValidEnterpriseValidityToken || hasValidEnterpriseKey` (to support both new and legacy keys during transition). After transition isValid() = hasValidEnterpriseValidityToken ### Frontend states The Enterprise settings page handles multiple states: - **No key**: show "Get Enterprise" with checkout modal - **Orphaned validity token** (token valid but no signed key): prompt user to set a valid enterprise key - **Active/trialing but no validity token**: show subscription status with a "Reload validity token" action - **Active/trialing**: show full subscription info, billing portal access, cancel option - **Cancellation scheduled**: show cancellation date, billing portal - **Canceled**: show billing history link and option to start a new subscription - **Past due / Incomplete**: prompt to update payment or restart ### Temporary retro-compatibility: legacy plain-text keys Previously, enterprise features were gated by a simple check: any non-empty string in `ENTERPRISE_KEY` granted access. With this PR, we transition to a controlled system relying on signed JWTs. To avoid breaking existing self-hosted users: - **Legacy plain-text keys still grant access** to enterprise features. `hasValidEnterpriseKey` returns `true` for both signed JWTs and plain strings, and `isValid()` checks `hasValidEnterpriseKey` as a fallback when no validity token is present. - **A deprecation banner** is shown at the top of the app when `hasValidEnterpriseKey` is `true` but `hasValidSignedEnterpriseKey` is `false`, informing the user that their key format is deprecated and they should activate a new signed key. - **No billing portal or subscription management** is available for legacy keys since there is no Stripe subscription to manage. This retro-compatibility will be removed in a future version. At that point, `isValid()` will only check `hasValidEnterpriseValidityToken`. ### Edge cases - **Air-gapped / production environments**: for self-hosted clients that block external traffic (or for our own production), provide a long-lived `ENTERPRISE_VALIDITY_TOKEN` (e.g. 99 years) directly in the `appToken` table, with no `ENTERPRISE_KEY`. The daily cron will skip the refresh (no enterprise key to authenticate with), but the pre-seeded validity token will be used to grant feature access. No billing or seat reporting occurs in this mode. - **`IS_CONFIG_VARIABLES_IN_DB_ENABLED` is false**: if the user tries to activate an enterprise key but DB config writes are disabled, the backend returns a clear error asking them to add `ENTERPRISE_KEY` to their `.env` file manually. - **Canceled subscriptions**: the `/seats` endpoint skips Stripe updates for canceled or cancellation-scheduled subscriptions to avoid Stripe API errors. ### How to test - launch twenty-website on a different url (eg localhost:1002) - add ENTERPRISE_API_URL=http://localhost:3002/api/enterprise (or else) in your server .env - ask me for twenty-website's .env file content (STRIPE_SECRET_KEY; STRIPE_ENTERPRISE_MONTHLY_PRICE_ID;STRIPE_ENTERPRISE_YEARLY_PRICE_ID; ENTERPRISE_JWT_PRIVATE_KEY; ENTERPRISE_JWT_PUBLIC_KEY; NEXT_PUBLIC_WEBSITE_URL) - visit Admin panel / enterprise |
||
|
|
7a2e397ad1 |
Complete linaria migration (#18361)
## Summary
Completes the migration of the frontend styling system from **Emotion**
(`@emotion/styled`, `@emotion/react`) to **Linaria** (`@linaria/react`,
`@linaria/core`), a zero-runtime CSS-in-JS library where styles are
extracted at build time.
This is the final step of the migration — all ~494 files across
`twenty-front`, `twenty-ui`, `twenty-website`, and `twenty-sdk` are now
fully converted.
## Changes
### Styling Migration (across ~480 component files)
- Replaced all `@emotion/styled` imports with `@linaria/react`
- Converted runtime theme access patterns (`({ theme }) => theme.x.y`)
to build-time `themeCssVariables` CSS custom properties
- Replaced `useTheme()` hook (from Emotion) with
`useContext(ThemeContext)` where runtime theme values are still needed
(e.g., passing colors to non-CSS props like icon components)
- Removed `@emotion/react` `css` helper usages in favor of Linaria
template literals
### Dependency & Configuration Changes
- **Removed**: `@emotion/react`, `@emotion/styled` from root
`package.json`
- **Added**: `@wyw-in-js/babel-preset`, `next-with-linaria` (for
twenty-website SSR support)
- Updated Nx generator defaults from `@emotion/styled` to
`@linaria/react` in `nx.json`
- Simplified `vite.config.ts` (removed Emotion-specific configuration)
- Updated `twenty-website/next.config.js` to use `next-with-linaria` for
SSR Linaria support
### Storybook & Testing
- Removed `ThemeProvider` from Emotion in Storybook previews
(`twenty-front`, `twenty-sdk`)
- Now relies solely on `ThemeContextProvider` for theme injection
### Documentation
- Removed the temporary `docs/emotion-to-linaria-migration-plan.md`
(migration complete)
- Updated `CLAUDE.md` and `README.md` to reflect Linaria as the styling
stack
- Updated frontend style guide docs across all locales
## How it works
Linaria extracts styles at build time via the `@wyw-in-js/vite` plugin.
All expressions in `styled` template literals must be **statically
evaluable** — no runtime theme objects or closures over component state.
- **Static styles** use `themeCssVariables` which map to CSS custom
properties (`var(--theme-color-x)`)
- **Runtime theme access** (for non-CSS use cases like icon `color`
props) uses `useContext(ThemeContext)` instead of Emotion's `useTheme()`
|
||
|
|
01d2269bd0 |
Fix website build (#18061)
As per title |
||
|
|
cda70a70ca |
fix: replace react-tooltip with AppTooltip and refactor MenuItemAvatar (#17846)
This PR addresses TODO comments and improves code quality: ### 1. PullRequestItem.tsx - Replaced `react-tooltip` with `twenty-ui` `AppTooltip` component - Removed TODO comment - Uses internal component library for consistency ### 2. MenuItemAvatar.tsx - Refactored to use `MenuItem` internally, eliminating code duplication - Removed about 63 lines of duplicate code - Removed TODO comment as the merge is now complete --------- Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> |
||
|
|
9f97be67b1 |
Migrate documentation to Mintlify and configure 301 redirects (#15502)
## Summary Completes the migration of all documentation from twenty-website to a new Mintlify-powered documentation site at docs.twenty.com. ## Changes Made ### New Package: `twenty-docs` - ✅ Created new Mintlify documentation package - ✅ Migrated 95 content pages (user-guide, developers, twenty-ui) - ✅ Migrated 81 images - ✅ Converted all custom components to Mintlify native components - ✅ Configured navigation with 2 tabs and 94 pages - ✅ Added Helper AI Agent with searchArticles tool for docs search ### Updated: `twenty-website` - ✅ Added 11 redirect rules (301 permanent) in next.config.js - ✅ Removed all documentation content (111 files) - ✅ Removed documentation routes (user-guide, developers, twenty-ui) - ✅ Removed documentation components (9 files) - ✅ Updated keystatic.config.ts - ✅ Preserved all marketing/release pages ### Updated: Core Files - ✅ Updated README.md - docs links point to docs.twenty.com - ✅ Updated CONTRIBUTING.md - code quality link updated - ✅ Updated SupportDropdown.tsx - user guide link updated - ✅ Updated Footer.tsx - user guide link updated |
||
|
|
434df8a94c |
Release - revert/downgrade twenty website next version (#15181)
[PR #14917](https://github.com/twentyhq/twenty/pull/14917/files#diff-e37dead9533eef25d3a1ac323bb68e93ad2edbb932e972e48f4c756e3c2d5c0f) upgraded twenty-website to Next.js v15, which requires React 19. However, the twenty-ui package (imported by twenty-website) uses React 18.2, causing a React version mismatch error. Solution : Downgrade Next.js from ^15.5.4 to ^14.2.0 to maintain compatibility with React 18.2 used across the monorepo. |
||
|
|
a5790e3967 |
Release - switch 1.10 to 1.8 (#15124)
Before <img width="500" height="500" alt="Screenshot 2025-10-16 at 11 38 07" src="https://github.com/user-attachments/assets/7fb99487-8e18-41e4-b7e1-0951782480c2" /> After <img width="500" height="500" alt="Screenshot 2025-10-16 at 11 37 58" src="https://github.com/user-attachments/assets/602c188b-0208-4e74-a3b6-5631df1bb968" /> |
||
|
|
01d40c4f86 |
website(releases): fix MDX content list to match visible releases (fix offset) (#14904)
# Current behavior 1.7.0 is displayed as 1.6.0 <img width="1303" height="1082" alt="CleanShot 2025-10-06 at 11 23 38" src="https://github.com/user-attachments/assets/a502aeec-eafc-4db9-bd04-0969a00ca474" /> # Fix Fixes an off-by-one mismatch between rendered releases and compiled MDX content by generating MDX from the filtered visible releases list. This ensures versions like 0.2.3, 0.3.0, ... display the correct content. |
||
|
|
843689ee05 |
chore(twenty-website): upgrade next (#14917)
Standardized `PageProps` usage across multiple pages. Upgraded `next`, `eslint-config-next`, and related dependencies to `^15.5.4` for compatibility and performance improvements. |
||
|
|
62f817b207 |
Added articles (pricing, onboarding, OS licence) and updated icons (#14334)
- added FAQs for several folders - created Pricing folder, including article explaining AGPL v3 licence - detailed settings section - added page to give link to calendar again for onboarding call - changed the icons - they are not necessarily relevant but at least they are different so this remains colorful --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
32cdb66802 |
Type fixes and website fix (#13825)
Various fixes |
||
|
|
033bedde0a |
Upgrade NX (#13758)
Upgrade from NX18 to NX21 |
||
|
|
00f0c68478 |
fix: Display year for releases from previous years on website (#13276) (#13291)
### 🐛 Bug Fix **Fixes #13276 - Website does not show year of releases** ### 📝 Description Fixed the Twenty website to properly display release years for previous year releases and standardized date formatting. **Key Changes:** - Show year for releases from previous years - Abbreviate month names for consistent layout (e.g., "April" → "Apr") - Added fallback to prevent runtime errors when no releases exist ### 🔧 Files Changed - **`formatDisplayDate.ts` (new)** - Utility function for consistent date formatting - **`Release.tsx`** - Use utility function and improve CSS layout - **`page.tsx`** - Add fallback for undefined `latestGithubRelease` ### 🧪 Example Output | Before | After | |--------|-------| | `April 25th 2024` (missing year) | `Apr 25th 2024` | | `April 25th 2025` | `Apr 25th` | | Runtime error | Graceful fallback | ### � Screenshots **Before & After:** <img width="1055" height="778" alt="image" src="https://github.com/user-attachments/assets/68f227ec-2b1f-486b-89a6-8c3d8058ba36" /> <img width="1055" height="778" alt="image" src="https://github.com/user-attachments/assets/74ed84b3-3a65-4090-b371-a48c25740bbd" /> --- **Related Issue:** Closes #13276 --------- Co-authored-by: Félix Malfait <felix@twenty.com> |
||
|
|
02bd15d61f |
Website changes docs playground (#10413)
From #10376 (extracting website related changes to deploy them separately, later) --------- Co-authored-by: oliver <8559757+oliverqx@users.noreply.github.com> |
||
|
|
da00dee8a1 |
Fix settings navigation active state for sub-pages (#12318)
Changes the default behavior for settings navigation items to stay active when navigating to sub-pages. **Problem:** - Navigation items like "Data Model" and "Webhooks" were not staying highlighted when navigating to detail pages - This was because `matchSubPages` defaulted to requiring exact path matches **Solution:** - Updated logic to make sub-page matching the default behavior (`end: item.matchSubPages === false`) - Only "Accounts" explicitly sets `matchSubPages: false` for its custom sub-item navigation - Removed redundant `matchSubPages: true` declarations throughout the codebase **URL Changes:** -- checked with @Bonapara - `/settings/workspace` → `/settings/general` - `/settings/workspace-members` → `/settings/members` - `/settings/api-keys` → `/settings/apis` - `/settings/developers/webhooks` → `/settings/webhooks` before: https://github.com/user-attachments/assets/56b94a49-9c31-4bb5-9875-ec24f4bc4d1e after: https://github.com/user-attachments/assets/38742599-c045-44d1-8020-56f3eacca779 --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
f1d658bcb6 |
Fix: Twenty-website docs same TOC ids #11865 (#11872)
This PR fixes issue https://github.com/twentyhq/twenty/issues/11865. The highlight heading logic in TOC was checking the heading text which could be the same for multiple headings. The ids for these headings were also just the heading texts, leading to conflict in ids too. Fix: - Appended index of the heading item from the list of headings to the id of the heading. This fixed conflicting ids. - Used these unique ids to toggle the highlight style. Behaviour after the fix: https://github.com/user-attachments/assets/ab3bc205-0b0e-451d-b9cb-4fa852263efc Edit: close #11865 --------- Co-authored-by: prastoin <paul@twenty.com> |
||
|
|
ce42928143 |
Fix keystatic github redirect_uri by relying on x-forwarded-* headers (#10862)
Fix suggested by https://github.com/Thinkmill/keystatic/issues/1022#issuecomment-2009029315. |
||
|
|
2c465bd42e |
Integrate Keystatic to edit twenty.com content (#10709)
This PR introduces Keystatic to let us edit twenty.com's content with a CMS. For now, we'll focus on creating release notes through Keystatic as it uses quite simple Markdown. Other types of content will need some refactoring to work with Keystatic. https://github.com/user-attachments/assets/e9f85bbf-daff-4b41-bc97-d1baf63758b2 --------- Co-authored-by: Félix Malfait <felix@twenty.com> |