diff --git a/packages/twenty-website/src/app/[locale]/(focused)/partners/brief/ClientBriefPageContent.tsx b/packages/twenty-website/src/app/[locale]/(focused)/partners/brief/ClientBriefPageContent.tsx
index 738ea3e644..cf5c6a6cd1 100644
--- a/packages/twenty-website/src/app/[locale]/(focused)/partners/brief/ClientBriefPageContent.tsx
+++ b/packages/twenty-website/src/app/[locale]/(focused)/partners/brief/ClientBriefPageContent.tsx
@@ -25,11 +25,15 @@ const BriefContainer = styled.div`
}
`;
-export function ClientBriefPageContent() {
+export function ClientBriefPageContent({
+ partnerSlug,
+}: {
+ partnerSlug?: string;
+}) {
return (
-
+
);
diff --git a/packages/twenty-website/src/app/[locale]/(focused)/partners/brief/page.tsx b/packages/twenty-website/src/app/[locale]/(focused)/partners/brief/page.tsx
index 3b3c15f78d..28c101c2f5 100644
--- a/packages/twenty-website/src/app/[locale]/(focused)/partners/brief/page.tsx
+++ b/packages/twenty-website/src/app/[locale]/(focused)/partners/brief/page.tsx
@@ -1,3 +1,4 @@
+import { normalizePartnerSlug } from '@/client-brief/normalize-partner-slug';
import {
getRouteI18n,
type LocaleRouteParams,
@@ -10,10 +11,13 @@ export const generateMetadata = buildRouteMetadata('partnersBrief');
export default async function ClientBriefPage({
params,
+ searchParams,
}: {
params: Promise;
+ searchParams: Promise<{ partner?: string | string[] }>;
}) {
await getRouteI18n(params);
+ const { partner } = await searchParams;
- return ;
+ return ;
}
diff --git a/packages/twenty-website/src/client-brief/build-client-brief-request-body.test.ts b/packages/twenty-website/src/client-brief/build-client-brief-request-body.test.ts
new file mode 100644
index 0000000000..95a60f764a
--- /dev/null
+++ b/packages/twenty-website/src/client-brief/build-client-brief-request-body.test.ts
@@ -0,0 +1,23 @@
+import { buildClientBriefRequestBody } from './build-client-brief-request-body';
+import { INITIAL_CLIENT_BRIEF_STATE } from './client-brief-state';
+
+const state = {
+ ...INITIAL_CLIENT_BRIEF_STATE,
+ firstName: 'Jane',
+ lastName: 'Smith',
+ email: 'jane@acme.com',
+ companyName: 'Acme Real Estate',
+ need: 'Migrate from HubSpot',
+};
+
+describe('buildClientBriefRequestBody', () => {
+ it('puts the referring partner slug on the wire', () => {
+ expect(
+ buildClientBriefRequestBody(state, 'acme-consulting').partnerSlug,
+ ).toBe('acme-consulting');
+ });
+
+ it('omits the key entirely when no slug is given', () => {
+ expect('partnerSlug' in buildClientBriefRequestBody(state)).toBe(false);
+ });
+});
diff --git a/packages/twenty-website/src/client-brief/build-client-brief-request-body.ts b/packages/twenty-website/src/client-brief/build-client-brief-request-body.ts
index e2058e6f19..c4dccfd6cd 100644
--- a/packages/twenty-website/src/client-brief/build-client-brief-request-body.ts
+++ b/packages/twenty-website/src/client-brief/build-client-brief-request-body.ts
@@ -10,6 +10,7 @@ function splitLanguages(value: string): string[] {
export function buildClientBriefRequestBody(
state: ClientBriefState,
+ partnerSlug?: string,
): ClientBriefRequest {
const body: ClientBriefRequest = {
firstName: state.firstName.trim(),
@@ -27,6 +28,7 @@ export function buildClientBriefRequestBody(
if (state.seatCount.trim()) body.seatCount = state.seatCount.trim();
if (state.timeline.trim()) body.timeline = state.timeline.trim();
if (state.budgetRange.trim()) body.budgetRange = state.budgetRange.trim();
+ if (partnerSlug) body.partnerSlug = partnerSlug;
return body;
}
diff --git a/packages/twenty-website/src/client-brief/client-brief-request-schema.test.ts b/packages/twenty-website/src/client-brief/client-brief-request-schema.test.ts
index 2f5ca6e0e4..53dd429305 100644
--- a/packages/twenty-website/src/client-brief/client-brief-request-schema.test.ts
+++ b/packages/twenty-website/src/client-brief/client-brief-request-schema.test.ts
@@ -68,4 +68,22 @@ describe('clientBriefRequestSchema', () => {
.success,
).toBe(false);
});
+
+ it('accepts a payload carrying a referring partner slug', () => {
+ expect(
+ clientBriefRequestSchema.safeParse({
+ ...minimalValid,
+ partnerSlug: 'acme-consulting',
+ }).success,
+ ).toBe(true);
+ });
+
+ it('rejects a malformed partner slug', () => {
+ expect(
+ clientBriefRequestSchema.safeParse({
+ ...minimalValid,
+ partnerSlug: 'Acme Consulting!',
+ }).success,
+ ).toBe(false);
+ });
});
diff --git a/packages/twenty-website/src/client-brief/client-brief-request-schema.ts b/packages/twenty-website/src/client-brief/client-brief-request-schema.ts
index 844850c199..be7ab9fca8 100644
--- a/packages/twenty-website/src/client-brief/client-brief-request-schema.ts
+++ b/packages/twenty-website/src/client-brief/client-brief-request-schema.ts
@@ -3,6 +3,7 @@ import { z } from 'zod';
import { emailFieldSchema } from '@/partner-application/email-field-schema';
import { CLIENT_BRIEF_HOSTING_TYPES } from './data/hosting-type-values';
+import { partnerSlugSchema } from './partner-slug-schema';
const optionalNonEmptyString = z.string().trim().min(1).optional();
@@ -19,6 +20,7 @@ export const clientBriefRequestSchema = z.strictObject({
seatCount: optionalNonEmptyString,
timeline: optionalNonEmptyString,
budgetRange: optionalNonEmptyString,
+ partnerSlug: partnerSlugSchema.optional(),
});
export type ClientBriefRequest = z.infer;
diff --git a/packages/twenty-website/src/client-brief/normalize-partner-slug.test.ts b/packages/twenty-website/src/client-brief/normalize-partner-slug.test.ts
new file mode 100644
index 0000000000..a85bf6abc6
--- /dev/null
+++ b/packages/twenty-website/src/client-brief/normalize-partner-slug.test.ts
@@ -0,0 +1,29 @@
+import { normalizePartnerSlug } from './normalize-partner-slug';
+
+describe('normalizePartnerSlug', () => {
+ it('returns a well-formed slug unchanged', () => {
+ expect(normalizePartnerSlug('acme-consulting')).toBe('acme-consulting');
+ });
+
+ it('returns undefined when the param is absent', () => {
+ expect(normalizePartnerSlug(undefined)).toBeUndefined();
+ });
+
+ it('returns undefined for an empty string', () => {
+ expect(normalizePartnerSlug('')).toBeUndefined();
+ });
+
+ it('drops a slug with unsupported characters instead of forwarding it', () => {
+ expect(normalizePartnerSlug('Acme Consulting!')).toBeUndefined();
+ });
+
+ it('drops an over-long slug', () => {
+ expect(normalizePartnerSlug('a'.repeat(101))).toBeUndefined();
+ });
+
+ it('takes the first value when the param is repeated', () => {
+ expect(normalizePartnerSlug(['acme-consulting', 'other'])).toBe(
+ 'acme-consulting',
+ );
+ });
+});
diff --git a/packages/twenty-website/src/client-brief/normalize-partner-slug.ts b/packages/twenty-website/src/client-brief/normalize-partner-slug.ts
new file mode 100644
index 0000000000..0156aa7f2d
--- /dev/null
+++ b/packages/twenty-website/src/client-brief/normalize-partner-slug.ts
@@ -0,0 +1,9 @@
+import { partnerSlugSchema } from './partner-slug-schema';
+
+// The API schema is strict: a malformed value must be dropped here, not
+// forwarded, or it fails the whole brief submission.
+export function normalizePartnerSlug(
+ raw: string | string[] | undefined,
+): string | undefined {
+ return partnerSlugSchema.safeParse(Array.isArray(raw) ? raw[0] : raw).data;
+}
diff --git a/packages/twenty-website/src/client-brief/partner-slug-schema.ts b/packages/twenty-website/src/client-brief/partner-slug-schema.ts
new file mode 100644
index 0000000000..47f4bf2344
--- /dev/null
+++ b/packages/twenty-website/src/client-brief/partner-slug-schema.ts
@@ -0,0 +1,9 @@
+import { z } from 'zod';
+
+// Mirrors the backend `slugify` output. Shared by the request schema and
+// normalizePartnerSlug so the rule has one owner.
+export const partnerSlugSchema = z
+ .string()
+ .trim()
+ .regex(/^[a-z0-9-]+$/)
+ .max(100);
diff --git a/packages/twenty-website/src/client-brief/wizard/ClientBriefWizard.tsx b/packages/twenty-website/src/client-brief/wizard/ClientBriefWizard.tsx
index d947b0cbfc..004955c669 100644
--- a/packages/twenty-website/src/client-brief/wizard/ClientBriefWizard.tsx
+++ b/packages/twenty-website/src/client-brief/wizard/ClientBriefWizard.tsx
@@ -135,7 +135,7 @@ function StepRenderer({ controller }: { controller: ClientBriefController }) {
}
}
-export function ClientBriefWizard() {
+export function ClientBriefWizard({ partnerSlug }: { partnerSlug?: string }) {
const { i18n } = useLingui();
const controller = useClientBriefState();
const {
@@ -176,7 +176,7 @@ export function ClientBriefWizard() {
return;
}
- const payload = buildClientBriefRequestBody(state);
+ const payload = buildClientBriefRequestBody(state, partnerSlug);
setSubmitting(true);
try {
const response = await fetch('/api/client-brief', {
@@ -199,6 +199,7 @@ export function ClientBriefWizard() {
goNext,
i18n,
isLastStep,
+ partnerSlug,
setFieldErrors,
setSubmitError,
setSubmitted,
diff --git a/packages/twenty-website/src/partners-marketplace/PartnerProfile.tsx b/packages/twenty-website/src/partners-marketplace/PartnerProfile.tsx
index 34bca7b141..0b5b7e40a3 100644
--- a/packages/twenty-website/src/partners-marketplace/PartnerProfile.tsx
+++ b/packages/twenty-website/src/partners-marketplace/PartnerProfile.tsx
@@ -144,6 +144,7 @@ export function PartnerProfile({ partner }: { partner: MarketplacePartner }) {
calendarLink={partner.calendarLink}
links={partner.links}
linkUrls={partner.linkUrls}
+ slug={partner.slug}
/>