This guide explains how to integrate your project with the SourceFlow Page Builder, including setup, usage, and best practices.
- SourceFlow Page Builder README
The SourceFlow Page Builder lets you define reusable components and manage pages visually. Here’s how it works:
-
Component Definitions:
For each component you want to use in the Page Builder, create adefinitions.sourceflow.mjs
file. -
Build Step:
Use thesfprepare
CLI (installed via NPM) to combine all definitions into a singlesourceflow-components.json
file. -
Accessing Definitions:
The combined JSON file is available at<<domain>>/sourceflow-components.json
. -
Components Page:
Create acomponents.jsx
page that outputs a staticcomponents.html
file (client-side only) at<<domain>>/components
. -
Dynamic Routing:
Update your[url_slug].js
catch-all route to extract data from Page Builder and render pages accordingly. -
CMS Integration:
SourceFlow CMS will embed yourcomponents.html
page in an iframe and communicate viapostMessage
. -
Data Structure:
The CMS sends an array of objects, each with anid
,props
, andcomponent
name, for rendering:[ { id: '0d3b1b32-bae9-4dc4-948b-9bb28542e62d', props: { title: 'Enter the main title here...', content: '<p>Enter formatted content here...</p>', className: '', }, component: 'Content', }, ];
-
Rendering:
Thecomponents.html
page renders components based on the received data. -
Workflow Summary:
- Write definitions files
- Combine them with
sfprepare
- CMS picks up the definitions
- CMS loads your components page
- CMS sends content data
- Components page renders a preview
- User saves
- Site renders saved data server-side
Install the CLI tool in your project:
npm i @sourceflow-uk/page-builder-cli
# or
yarn add @sourceflow-uk/page-builder-cli
# or
pnpm add @sourceflow-uk/page-builder-cli
All projects that use SourceFlow's Page Builder must install this package.
Add this to your ESLint config (e.g., .eslintrc
, .eslintrc.json
) to avoid warnings for anonymous default exports in definitions files:
"overrides": [
{
"files": ["**/definitions.sourceflow.mjs"],
"rules": {
"import/no-anonymous-default-export": "off"
}
}
]
Update your package.json
:
"scripts": {
"postbuild": "sfprepare -d /components -o /out"
}
Or, if you want a separate script:
"scripts": {
"sfprepare": "sfprepare -d /components -o /out",
"postbuild": "npm run sfprepare"
}
Notes:
-
Do not use
prepare
as the script name (it's a reserved NPM lifecycle script). -
If you already have a
postbuild
script (e.g., fornext-sitemap
), addsfprepare
before other commands:"postbuild": "sfprepare -d /components -o /out && next-sitemap"
Flag | Description | Default | Example |
---|---|---|---|
-V, --version |
Show version number | - | sfprepare -V |
-d, --directory |
Components folder to scan for definitions | /src/components |
sfprepare -d /components |
-p, --preview |
Path to the components preview HTML | /out/components/index.html |
sfprepare -p /out/components.html |
-o, --output |
Output directory for sourceflow-components.json |
/out |
sfprepare -o /out |
-h, --help |
Show help | - | sfprepare -h |
Example:
sfprepare -d /components -o /dist
If your Next.js project outputs to /out
, you can omit the -o
flag.
- Only components with a
definitions.sourceflow.mjs
file will appear in the Page Builder. - The file must be named exactly
definitions.sourceflow.mjs
.
Field | Description | Required | Example |
---|---|---|---|
component |
Name matching your component | Yes | BlogSection |
label |
User-friendly name | Yes | Blog Section |
component_category |
Category in snake_case | Yes | default |
propSchema |
Array of prop definitions | Yes (if your component has at least 1 prop, if there are no props, you can omit this field) | See below |
export default {
component: 'BlogSection',
label: 'Blog Section',
component_category: 'default',
propSchema: [
{
name: 'title',
label: 'Title',
type: 'string',
defaultValue: 'Enter the main title here...',
},
{
name: 'className',
label: 'Class Name',
type: 'string',
defaultValue: '',
},
{
name: 'description',
label: 'Description',
type: 'formatted_text',
},
{
name: 'icon',
label: 'Icon',
type: 'file',
},
// ...more props
],
};
Each prop in propSchema
can have the following fields:
Field | Required? | Description | Example / Values |
---|---|---|---|
name |
Yes | Prop name (must match your component) | "title" |
label |
Yes | User-friendly label | "Section with Image" |
type |
Yes | Data type | See Supported Types below |
template_schema |
Yes (if type: template ) |
Defines fields for nested templates | See example below |
object_schema |
Yes (if type: object ) |
Defines fields for nested objects | See example below |
defaultValue |
No | Default value for the field | See Default Values Example below |
category_internal_build_name |
No | Internal build name for the category(snake_case). Use either this or category_id not both. |
"testimonials" |
category_id |
No | ID of the category that you want to link this prop to (UUID string). Use either this or category_internal_build_name not both. |
"cb62ab14-dfac-43f6-89ab-a283445584a4" |
allowMultipleOptions |
No | Allow multiple options (for array or category_id ) |
true / false |
options |
No | Options for select fields, in the format { label: '', value: ''} |
[ { label: "A", value: "A" } ] |
isRequired (Not supported yet) |
No | Mark as required | true / false |
grouped_under |
No | Makes this prop be nested under another prop so that it's nicer in the CMS UI, for example, the prop buttonTextClassName might use this to be grouped_under: "buttonText" so that we can compartmentalise the props |
"title" |
- String:
"MyDefaultStringValue"
In a string format. - Number:
123
In a number format. - Boolean:
true
/false
In a boolean format. - Array:
["A", "B", "C"]
or[{any: "thing"}]
In an array format. - File:
{id: "<<ID_OF_IMAGE_FROM_CMS>>"}
or{url: "https://example.com/image.jpg"}
In an object either withid
orurl
. - Template:
[{any: "thing"}]
. In an array format. - Formatted Text:
"<p>My default formatted text</p>"
In a string format. - Forms: Not supported.
- Object:
{any: "thing"}
In an object format. - Modules: Not supported.
- Repeater:
[{any: "thing"}]
In an array format.
string
number
boolean
array
file
template
formatted_text
forms
object
modules
repeater
Notes:
- Use
type: 'repeater'
for simple repeatable fields (e.g., multiple strings). - Use
type: 'template'
andtemplate_schema
for more complex/nested repeatable set of fields. - Use
type: 'object'
andobject_schema
for nested objects (single instance).
export default {
component: 'CTA',
label: 'Call to Action',
component_category: 'default',
propSchema: [
{
name: 'myobject',
label: 'My Object',
type: 'object',
object_schema: [
{ name: 'field1', label: 'Field 1', type: 'string' },
{
name: 'field2',
label: 'Field 2',
type: 'template', // We can even put templates inside objects
template_schema: [
{ name: 'something', label: 'Something', type: 'string' },
{ name: 'somethingElse', label: 'Something Else', type: 'number' },
],
},
],
},
{
name: 'mytemplate',
label: 'My Template',
type: 'template',
template_schema: [
// ...other fields
{
label: 'Field1 - String linked with Category Example',
name: 'field1',
category_id: 'cb62ab14-dfac-43f6-89ab-a283445584a4',
allowMultipleOptions: true,
type: 'string',
defaultValue: 'Hello world!',
},
{
name: 'Field8 - Object Example',
label: 'Field8',
type: 'object', // We can even put objects inside templates
object_schema: [
{
name: 'Field8.1',
label: 'Field8.1',
type: 'string',
defaultValue: 'Hello world!',
},
// ... more nested fields
],
},
],
isRequired: false,
},
],
};
export default {
component: 'ExampleComponent',
label: 'Example Component',
component_category: 'default',
propSchema: [
{ name: 'title', label: 'title', type: 'string', defaultValue: 'Hello world!' },
{
name: 'description',
label: 'description',
type: 'formatted_text',
},
{ name: 'buttonText', bs_col_width: 4, type: 'string', defaultValue: 'Click me!', isRequired: true },
{
name: 'buttonClass',
bs_col_width: 4,
type: 'string',
defaultValue: '',
options: [
{
label: 'Primary',
value: 'btn-primary',
},
{
label: 'Secondary',
value: 'btn-secondary',
},
{
label: 'Danger',
value: 'btn-danger',
},
],
allowMultipleOptions: false,
isRequired: true,
},
{
name: 'sponsors',
type: 'repeater',
},
{
name: 'icon',
label: 'icon',
type: 'file',
},
{ name: 'myRandomNameForModuleID', type: 'modules' },
{
name: 'template',
type: 'template',
template_item_label: 'Module Items',
template_schema: [
{
label: 'Field1',
name: 'field1',
category_id: 'cb62ab14-dfac-43f6-89ab-a283445584a4',
allowMultipleOptions: true, // Allow multiple category values to be selected
},
{
label: 'Field2Module',
name: 'field2Module',
type: 'modules',
},
],
},
{
name: 'massive_object_one',
label: 'massive_object_one',
type: 'object',
object_schema: [
{
label: 'Field 1',
name: 'image1',
type: 'file',
defaultValue: {
id: '32248875-332a-4111-8a1c-f5e1a3752936',
},
},
],
},
{
name: 'massive_object_two',
label: 'massive_object_two',
type: 'object',
object_schema: [
{
label: 'Field 1',
name: 'image1',
type: 'file',
defaultValue: {
id: '32248875-332a-4111-8a1c-f5e1a3752936',
},
},
],
},
{
name: 'massive_object_three',
label: 'massive_object_three',
type: 'object',
object_schema: [
{
label: 'Field 1',
name: 'image1',
type: 'file',
defaultValue: { url: 'https://placehold.co/300' },
},
],
},
{
name: 'blogs',
label: 'blogs',
type: 'template',
template_item_label: 'Blog',
template_schema: [
{
label: 'Field0 - Repeater',
name: 'Field0-Repeater',
type: 'repeater',
},
{
label: 'Field1',
name: 'field1',
category_id: 'cb62ab14-dfac-43f6-89ab-a283445584a4',
allowMultipleOptions: true,
type: 'string',
defaultValue: 'Hello world!',
},
{
label: 'Field2',
name: 'field2',
type: 'number',
defaultValue: 0,
},
{
label: 'Field3',
name: 'field3',
type: 'boolean',
defaultValue: false,
},
{
label: 'Field4',
name: 'field4',
type: 'array',
options: [],
defaultValue: [],
},
{
label: 'Field5',
name: 'field5',
type: 'file',
},
{
label: 'Field6',
name: 'field6',
type: 'formatted_text',
defaultValue: '',
},
{
label: 'Field 7',
name: 'field7',
type: 'object',
object_schema: [{ label: 'Field 7.1', name: 'field7.1', type: 'string', defaultValue: 'Hello world!' }],
},
{
label: 'Field 8',
name: 'field8',
type: 'template',
template_schema: [{ label: 'Field 8. name', name: 'name', type: 'string', defaultValue: 'Hello world!' }],
},
],
},
],
};
Generate a boilerplate definitions file:
sfgenerate -d /components -n MyComponent
sfgenerate --directory /path/to/output field1:string field2:number field3:array field4:boolean field5:file field6:template field7:formatted_text field8:forms field9:object
Flags:
Flag | Description | Default | Example |
---|---|---|---|
-V, --version |
Show version | - | sfgenerate -V |
-d, --directory |
Output directory | /src/components |
sfgenerate -d /components |
-n, --name |
Component name | - | sfgenerate -n MyComponent |
-do, --definitions-only |
Only output definitions file | false | sfgenerate -do -n MyComponent |
-c, --with-docs |
Include documentation in output | false | sfgenerate -c -n MyComponent |
-h, --help |
Show help | - | sfgenerate -h |
Reminder: Review and clean up the generated boilerplate as needed.
- Output to
/out/components/index.html
or/out/components.html
. - Place the page in your
pages
orapp
directory (e.g.,pages/components.jsx
).
-
Content State
const [content, setContent] = useState([]);
-
Receiving Messages
Use
useEffect
to listen for messages from the parent CMS:useEffect(() => { window.addEventListener('message', (event) => { if (!event.data) return; const parsed = JSON.parse(event.data.message); switch (event.data.type) { case 'SET_CONTENT': setContent(parsed); break; // Handle FOCUS_CONTENT and CLEAR_FOCUS_CONTENT as needed default: console.log('Unknown event type.'); } }); }, []);
Event Types:
SET_CONTENT
: Sets the content array for rendering.FOCUS_CONTENT
: Focuses/highlights a specific element by ID.CLEAR_FOCUS_CONTENT
: Removes highlight from a specific element.
-
Sending Messages (Optional but recommended for user experience)
You can send messages back to the CMS for actions like focusing on a specific prop field. This is optional but improves user experience.
type
Description Payload Example type: 'EDITABLE_ELEMENT_CLICKED'
Sends the ID and prop name of the clicked element, this will trigger the CMS to focus on that prop field { id: contentId, propName: prop }
See below document.body.addEventListener('click', (event) => { const prop = event.target.getAttribute('data-sourceflow-prop'); if (prop) { const element = event.target.closest('[data-sourceflow-content-id]'); if (element) { const contentId = element.getAttribute('data-sourceflow-content-id'); window.parent.postMessage( { type: 'EDITABLE_ELEMENT_CLICKED', message: { id: contentId, propName: prop }, }, '*' ); } } });
const style = document.createElement('style'); style.innerHTML = ` [data-sourceflow-prop]:hover { border: 2px dashed gray; cursor: pointer; } `; document.head.appendChild(style);
-
Rendering Components
Example:
return ( <div> <Content items={content} /> </div> );
Where
<Content />
maps over the items and renders the correct component by name:export default function Content({ items, global }) { const allowedComponents = { Accordion, ArticleFeed, BlockQuote, Divider, Form, TeamBio, Title, }; return ( <section> {items.filter(Boolean).map(({ id, component, props }) => ( <div key={id}> <a id={id} /> {(() => { const Component = allowedComponents[component]; return Component ? <Component key={id} global={global} {...props} /> : null; })()} </div> ))} </section> ); }
Update your [url_slug].js
file to read from dynamic_pages.json
:
import Content from '../components/Content';
import dynamic_pages from '../.sourceflow/dynamic_pages.json';
export default function Page({ content }) {
return <Content items={content} />;
}
export async function getStaticProps({ params }) {
const { url_slug } = params;
const slugPath = url_slug.join('/');
const page = dynamic_pages.find((page) => page.url_slug === slugPath);
return {
props: {
meta: { title: page.title },
content: page.content,
},
};
}
export async function getStaticPaths() {
const paths = dynamic_pages.map((page) => ({
params: { url_slug: page.url_slug.split('/') },
}));
return {
paths,
fallback: false,
};
}
Tips:
- Ensure dynamic pages overwrite existing routes as needed.
- Only components with a
definitions.sourceflow.mjs
file are available in Page Builder. - Always check and clean up generated files.
- Ensure your build scripts and output paths are correct.