Skip to content

Commit 828ddcc

Browse files
committed
wip:1 (0r0uw)
1 parent 86849a0 commit 828ddcc

120 files changed

Lines changed: 5430 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from "react";
2+
import { Admin, Api } from "webiny/extensions";
3+
4+
export const FunnelBuilder = () => {
5+
return (
6+
<>
7+
<Api.Extension src={import.meta.dirname + "/api/TenantModelExtension.ts"} />
8+
<Admin.Extension src={import.meta.dirname + "/pageType/index.tsx"} />
9+
<Admin.Extension src={import.meta.dirname + "/pageEditor/index.tsx"} />
10+
</>
11+
);
12+
};
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# Funnel Builder Extension
2+
3+
The Funnel Builder extension adds multi-step funnel capabilities to Webiny's Website Builder.
4+
5+
> **Architecture Change from v5**: Unlike Webiny v5 where everything was on the Webiny side, v6 separates concerns: **public website rendering happens in Next.js**, while **Webiny Admin is purely an editor UI** for building funnels, managing steps, defining condition rules, and dropping fields. All visual components and React code live in your Next.js app. See [How It All Fits Together](https://www.webiny.com/learn/course/website-builder/setting-up-website-builder) for details.
6+
7+
> **Note**: This README covers the Webiny backend extension setup. For Next.js frontend implementation details (multi-tenancy, theming, component customization), see the [Next.js Starter Kit README](https://github.com/webiny/website-builder-nextjs/blob/funnel-builder/README.md).
8+
9+
## Setup Guide
10+
11+
### Step 1: Install the Extension
12+
13+
This extension is already installed in your Webiny project. If you need to verify, check that `extensions/funnelBuilder/` exists in your project root.
14+
15+
### Step 2: Clone the Next.js Starter Kit
16+
17+
The Funnel Builder requires a Next.js frontend app to render your funnels. Clone the official starter kit with funnel support:
18+
19+
```bash
20+
git clone -b funnel-builder https://github.com/webiny/website-builder-nextjs.git my-funnel-app
21+
cd my-funnel-app
22+
npm install
23+
```
24+
25+
**Important**: Make sure to clone the `funnel-builder` branch, which includes all the necessary funnel components.
26+
27+
**Note**: Once cloned, you can move this code to your own private repository for version control and customization. The starter kit is meant to be a starting point for your own project.
28+
29+
Before running `npm install`, verify that the `@webiny/website-builder-nextjs` and `@webiny/sdk` versions in `package.json` match your Webiny version. You can check your version with:
30+
31+
```bash
32+
webiny --version
33+
```
34+
35+
If your Webiny version is `6.2.1`, your `package.json` should have:
36+
37+
```json
38+
{
39+
"dependencies": {
40+
"@webiny/website-builder-nextjs": "~6.2.1",
41+
"@webiny/sdk": "~6.2.1"
42+
}
43+
}
44+
```
45+
46+
### Step 3: Start Your Webiny Admin App
47+
48+
You'll need the Admin app running to configure the Next.js connection:
49+
50+
- **Locally**: Run `yarn webiny watch admin` in your Webiny project directory
51+
- **Deployed**: Use your Admin CloudFront URL directly
52+
53+
### Step 4: Get Your API Credentials
54+
55+
In Webiny Admin:
56+
57+
1. Click **Support** in the bottom-left sidebar
58+
2. Select **Configure Next.js**
59+
3. Copy the environment variables shown in the dialog
60+
61+
The dialog will show:
62+
63+
```bash
64+
NEXT_PUBLIC_WEBSITE_BUILDER_API_KEY=your-api-key-here
65+
NEXT_PUBLIC_WEBSITE_BUILDER_API_HOST=https://your-api-host.com
66+
NEXT_PUBLIC_WEBSITE_BUILDER_API_TENANT=root
67+
```
68+
69+
**Note**: The `NEXT_PUBLIC_WEBSITE_BUILDER_API_TENANT` variable is only needed when working with a single tenant. The Next.js starter kit resolves tenant at runtime from the hostname subdomain (for public traffic) or the `wb.tenant` query param (from the editor iframe). See the Next.js repo README for details on multi-tenant setup.
70+
71+
If your Admin is running on a non-localhost domain (deployed CloudFront URL), the dialog will also include `NEXT_PUBLIC_WEBSITE_BUILDER_ADMIN_HOST` — make sure to copy that too.
72+
73+
### Step 5: Configure Your Next.js App
74+
75+
Create a `.env` file in your Next.js project root:
76+
77+
```bash
78+
NEXT_PUBLIC_WEBSITE_BUILDER_API_KEY=your-api-key-here
79+
NEXT_PUBLIC_WEBSITE_BUILDER_API_HOST=https://your-api-host.com
80+
# NEXT_PUBLIC_WEBSITE_BUILDER_API_TENANT=root # Only needed for single-tenant setups
81+
```
82+
83+
### Step 6: Start the Next.js Dev Server
84+
85+
```bash
86+
npm run dev
87+
```
88+
89+
Your Next.js app will be available at [http://localhost:3000](http://localhost:3000).
90+
91+
## Creating Your First Funnel
92+
93+
1. In Webiny Admin, go to **Website Builder → Pages**
94+
2. Click **New Page**
95+
3. Set the **Page Type** to **Funnel**
96+
4. Set a **Title** (e.g., "Lead Generation Funnel") and **Path** (e.g., `/get-started`)
97+
5. Click **Create**
98+
6. Build your funnel using the available components in the editor
99+
7. Click **Publish**
100+
8. Visit your Next.js app at the configured path (e.g., `http://localhost:3000/get-started`)
101+
102+
The funnel editor and components work the same as in Webiny v5. If you're migrating from v5, you'll find all the familiar mechanics and features.
103+
104+
## Tenant Theming
105+
106+
The Funnel Builder extension extends the Webiny Tenant model with theme settings that can be accessed by your Next.js app for dynamic styling.
107+
108+
### Theme Fields Added to Tenant Settings
109+
110+
The extension adds a "Theme" section to **Tenant Settings** with the following fields:
111+
112+
- **Primary Color**: Primary brand color (e.g., `#ff0000`)
113+
- **Secondary Color**: Secondary brand color (e.g., `#0000ff`)
114+
- **Logo URL**: URL of the tenant logo
115+
116+
These values are stored under `tenant.extensions.theme` and can be fetched via the Webiny SDK.
117+
118+
### Accessing Theme Values in Next.js
119+
120+
Your Next.js app can fetch these tenant-specific theme values using the SDK:
121+
122+
```typescript
123+
import { createSdk } from "@/src/lib/webiny";
124+
125+
const result = await createSdk(tenantId).tenantManager.getCurrentTenant();
126+
const theme = result.value.values.extensions?.theme ?? {};
127+
128+
// theme.primaryColor
129+
// theme.secondaryColor
130+
// theme.logo
131+
```
132+
133+
The Next.js starter kit includes `getTenantThemeCss()` which automatically fetches these values and generates CSS custom properties that can be used throughout your app. See the Next.js repo README for implementation details.
134+
135+
### Configuring Theme Values
136+
137+
**For Non-Root Tenants:**
138+
139+
1. In Webiny Admin, go to **Settings → Tenant Settings**
140+
2. Find the **Theme** section
141+
3. Set your Primary Color, Secondary Color, and Logo URL
142+
4. Save the settings
143+
5. Your Next.js app will automatically fetch and apply these values on the next request
144+
145+
**For Root Tenant:**
146+
147+
The Theme settings UI is not available for the root tenant. If you need to configure theme values for the root tenant, you must edit them via the API using the Tenant Manager SDK.
148+
149+
```typescript
150+
// Example: Update root tenant theme via API
151+
const sdk = createSdk("root");
152+
await sdk.tenantManager.updateTenant({
153+
id: "root",
154+
values: {
155+
extensions: {
156+
theme: {
157+
primaryColor: "#ff0000",
158+
secondaryColor: "#0000ff",
159+
logo: "https://example.com/logo.png"
160+
}
161+
}
162+
}
163+
});
164+
```
165+
166+
## Learn More
167+
168+
- **Website Builder Docs**: [webiny.com/learn/course/website-builder](https://www.webiny.com/learn/course/website-builder/setting-up-website-builder)
169+
- **Next.js Starter Kit**: [github.com/webiny/website-builder-nextjs](https://github.com/webiny/website-builder-nextjs) (funnel-builder branch)
170+
- **Webiny Community**: [webiny.com/slack](https://www.webiny.com/slack)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { TenantModelExtension as ModelExtension } from "webiny/api/tenant-manager";
2+
3+
/* Extends the Tenant model with theme settings used by the funnel builder.
4+
* Fields are stored under tenant.extensions.theme. */
5+
class TenantModelExtension implements ModelExtension.Interface {
6+
execute(extension: ModelExtension.Extension) {
7+
extension
8+
.fields(fields => ({
9+
theme: fields
10+
.object()
11+
.renderer("objectAccordionSingle")
12+
.label("Theme")
13+
.fields(themeFields => ({
14+
primaryColor: themeFields
15+
.text()
16+
.label("Primary Color")
17+
.description("Primary brand color (e.g. #ff0000).")
18+
.renderer("textInput")
19+
.defaultValue(""),
20+
secondaryColor: themeFields
21+
.text()
22+
.label("Secondary Color")
23+
.description("Secondary brand color (e.g. #0000ff).")
24+
.renderer("textInput")
25+
.defaultValue(""),
26+
logo: themeFields
27+
.text()
28+
.label("Logo URL")
29+
.description("URL of the tenant logo.")
30+
.renderer("textInput")
31+
.defaultValue("")
32+
}))
33+
.layout([["primaryColor"], ["secondaryColor"], ["logo"]])
34+
}))
35+
.layout([["theme"]]);
36+
}
37+
}
38+
39+
export default ModelExtension.createImplementation({
40+
implementation: TenantModelExtension,
41+
dependencies: []
42+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { createObjectHash } from "../utils/createObjectHash";
2+
3+
export abstract class AbstractModel<TDto> {
4+
abstract toDto(): TDto;
5+
6+
populate(dto: Partial<TDto>) {
7+
for (const dtoKey in dto) {
8+
if (dtoKey in this) {
9+
const value = dto[dtoKey];
10+
if (value !== undefined) {
11+
// @ts-ignore We can ignore this TS error.
12+
this[dtoKey] = value;
13+
}
14+
}
15+
}
16+
}
17+
18+
getChecksum(): string {
19+
return createObjectHash(this.toDto());
20+
}
21+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { AbstractModel } from "./AbstractModel";
2+
import { getRandomId } from "../utils/getRandomId";
3+
import { FunnelConditionRuleModel } from "./FunnelConditionRuleModel";
4+
import { FunnelStepModel } from "./FunnelStepModel";
5+
6+
export type ConditionActionParams<TExtra = Record<string, any>> = {
7+
extra: TExtra;
8+
};
9+
10+
export type ConditionActionParamsDto<TExtra = Record<string, any>> = ConditionActionParams<TExtra>;
11+
12+
export interface FunnelConditionActionModelDto<TExtra = Record<string, any>> {
13+
id: string;
14+
type: string;
15+
params: ConditionActionParamsDto<TExtra>; // Additional parameters for the validator.
16+
}
17+
18+
export class FunnelConditionActionModel<TExtra = Record<string, any>> extends AbstractModel<
19+
FunnelConditionActionModelDto<TExtra>
20+
> {
21+
conditionRule: FunnelConditionRuleModel;
22+
id: string;
23+
type: string;
24+
params: ConditionActionParams<TExtra>;
25+
26+
static type = "";
27+
28+
// String shown in the conditional rules dialog (in the actions dropdown menu).
29+
static optionLabel = "";
30+
31+
constructor(
32+
conditionRule: FunnelConditionRuleModel,
33+
dto?: Partial<FunnelConditionActionModelDto<TExtra>>
34+
) {
35+
super();
36+
this.conditionRule = conditionRule;
37+
this.id = dto?.id || getRandomId();
38+
this.type = dto?.type || "";
39+
this.params = {
40+
extra: (dto?.params?.extra || {}) as TExtra
41+
};
42+
}
43+
44+
toDto(): FunnelConditionActionModelDto<TExtra> {
45+
return { id: this.id, type: this.type, params: this.params };
46+
}
47+
48+
isApplicable(): FunnelStepModel | undefined {
49+
return undefined;
50+
}
51+
52+
static fromDto<TExtra = Record<string, any>>(
53+
conditionRule: FunnelConditionRuleModel,
54+
dto: FunnelConditionActionModelDto<TExtra>
55+
): FunnelConditionActionModel<TExtra> {
56+
// Could not import the module directly because of circular dependency.
57+
return require("./conditionActions/conditionActionFactory").conditionActionFromDto(
58+
conditionRule,
59+
dto
60+
);
61+
}
62+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { FunnelConditionModel, FunnelConditionModelDto } from "./FunnelConditionModel";
2+
import { getRandomId } from "../utils/getRandomId";
3+
4+
export type LogicalOperator = "and" | "or";
5+
export type ConditionGroupItem = FunnelConditionModel | FunnelConditionGroupModel;
6+
7+
export interface FunnelConditionGroupModelDto {
8+
id: string;
9+
operator: LogicalOperator;
10+
items: Array<FunnelConditionModelDto | FunnelConditionGroupModelDto>;
11+
}
12+
13+
export class FunnelConditionGroupModel {
14+
id: string;
15+
operator: LogicalOperator;
16+
items: Array<ConditionGroupItem>;
17+
18+
constructor(dto?: FunnelConditionGroupModelDto) {
19+
this.id = dto?.id || getRandomId();
20+
this.operator = dto?.operator ?? "and";
21+
this.items = (dto?.items || []).map(item => {
22+
if ("sourceFieldId" in item) {
23+
return FunnelConditionModel.fromDto(item);
24+
}
25+
return FunnelConditionGroupModel.fromDto(item);
26+
});
27+
}
28+
29+
toDto(): FunnelConditionGroupModelDto {
30+
return {
31+
id: this.id,
32+
operator: this.operator,
33+
items: this.items.map(item => {
34+
return item.toDto();
35+
})
36+
};
37+
}
38+
39+
static fromDto(dto: FunnelConditionGroupModelDto): FunnelConditionGroupModel {
40+
return new FunnelConditionGroupModel(dto);
41+
}
42+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import {
2+
FunnelConditionOperatorModel,
3+
FunnelConditionOperatorModelDto
4+
} from "./FunnelConditionOperatorModel";
5+
import { AbstractModel } from "./AbstractModel";
6+
import { getRandomId } from "../utils/getRandomId";
7+
8+
export interface FunnelConditionModelDto {
9+
id: string;
10+
sourceFieldId: string; // the field whose value we're checking
11+
operator: FunnelConditionOperatorModelDto; // the operator to use for comparison
12+
}
13+
14+
export class FunnelConditionModel extends AbstractModel<FunnelConditionModelDto> {
15+
id: string;
16+
sourceFieldId: string;
17+
operator: FunnelConditionOperatorModel;
18+
19+
constructor(dto?: Partial<FunnelConditionModelDto>) {
20+
super();
21+
this.id = dto?.id || getRandomId();
22+
this.sourceFieldId = dto?.sourceFieldId || "";
23+
this.operator = dto?.operator
24+
? FunnelConditionOperatorModel.fromDto(dto.operator)
25+
: new FunnelConditionOperatorModel();
26+
}
27+
28+
toDto(): FunnelConditionModelDto {
29+
return {
30+
id: this.id,
31+
sourceFieldId: this.sourceFieldId,
32+
operator: this.operator.toDto()
33+
};
34+
}
35+
36+
static fromDto(dto: FunnelConditionModelDto): FunnelConditionModel {
37+
return new FunnelConditionModel(dto);
38+
}
39+
}

0 commit comments

Comments
 (0)