Sorry for the late reply.
Here is my CustomColumns implementation:
'use client';
import { Blocks, BuilderBlock } from '@builder.io/sdk-react-nextjs';
import { Box, Card, Grid } from '@mantine/core';
import Image from 'next/image';
import { PropsWithBuilderContext } from '@/types';
interface Column {
blocks: BuilderBlock[];
image?: string;
}
interface CustomColumnsProps {
builderBlock?: BuilderBlock;
columns?: Column[];
}
export const CustomColumns = ({ builderBlock, columns = [] }: PropsWithBuilderContext<CustomColumnsProps>) => {
return (
<Box p={30}>
<Grid gutter='lg'>
{columns.slice(0, 4).map((col, index) => (
<Grid.Col
key={index}
miw={300}
span={{ base: 12, md: 6 }}
>
<Card>
<Card.Section>
<div className='relative aspect-video'>
<Image
alt=''
className='object-cover'
fill
src='/default-image.svg'
/>
</div>
</Card.Section>
<Blocks
key={index}
blocks={col.blocks}
parent={builderBlock?.id}
path={`component.options.columns.${index}.blocks`}
/>
</Card>
</Grid.Col>
))}
</Grid>
</Box>
);
};
this is the registry:
import { RegisteredComponent } from '@builder.io/sdk-react-nextjs';
import { CustomColumns } from '.';
export const CustomColumnsEntry: RegisteredComponent = {
component: CustomColumns,
name: 'CustomColumns',
inputs: [
{
name: 'columns',
type: 'array',
defaultValue: [
{
image:
'https://cdn.builder.io/api/v1/image/assets%2Fpwgjf0RoYWbdnJSbpBAjXNRMe9F2%2Ffb27a7c790324294af8be1c35fe30f4d',
blocks: [
{
'@type': '@builder.io/sdk:Element',
component: {
name: 'Text',
options: {
text: 'Enter some text...',
},
},
responsiveStyles: {
large: {
display: 'flex',
flexDirection: 'column',
position: 'relative',
flexShrink: '0',
boxSizing: 'border-box',
marginTop: '20px',
lineHeight: 'normal',
height: 'auto',
textAlign: 'center',
},
},
},
],
},
{
image:
'https://cdn.builder.io/api/v1/image/assets%2Fpwgjf0RoYWbdnJSbpBAjXNRMe9F2%2Ffb27a7c790324294af8be1c35fe30f4d',
blocks: [
{
'@type': '@builder.io/sdk:Element',
component: {
name: 'Text',
options: {
text: 'Enter some text...',
},
},
responsiveStyles: {
large: {
display: 'flex',
flexDirection: 'column',
position: 'relative',
flexShrink: '0',
boxSizing: 'border-box',
marginTop: '20px',
lineHeight: 'normal',
height: 'auto',
textAlign: 'center',
},
},
},
],
},
],
subFields: [
{
name: 'image',
type: 'file',
allowedFileTypes: ['jpeg', 'jpg', 'png', 'svg'],
required: true,
defaultValue:
'https://cdn.builder.io/api/v1/image/assets%2Fpwgjf0RoYWbdnJSbpBAjXNRMe9F2%2Ffb27a7c790324294af8be1c35fe30f4d',
},
{
name: 'blocks',
type: 'blocks',
hideFromUI: true,
helperText: 'This is an editable region where you can drag and drop blocks.',
defaultValue: [
{
'@type': '@builder.io/sdk:Element',
component: {
name: 'Text',
options: {
text: 'Enter some text...',
},
},
responsiveStyles: {
large: {
display: 'flex',
flexDirection: 'column',
position: 'relative',
flexShrink: '0',
boxSizing: 'border-box',
marginTop: '20px',
lineHeight: 'normal',
height: 'auto',
textAlign: 'center',
},
},
},
],
},
],
},
],
};
props.builderBlock
:
{
"@type": "@builder.io/sdk:Element",
"@version": 2,
"id": "builder-55dd460b4268467a8fbdbc6dd3d324ae",
"component": {
"name": "CustomColumns",
"options": {
"columns": [
{
"image": "https://cdn.builder.io/api/v1/image/assets%2Fpwgjf0RoYWbdnJSbpBAjXNRMe9F2%2Ffb27a7c790324294af8be1c35fe30f4d",
"blocks": [
{
"@type": "@builder.io/sdk:Element",
"@version": 2,
"id": "builder-489f9f510068476396d1d60c273eeb16",
"component": {
"name": "Text",
"options": {
"text": "Enter some text..."
}
},
"responsiveStyles": {
"large": {
"display": "flex",
"flexDirection": "column",
"position": "relative",
"flexShrink": "0",
"boxSizing": "border-box",
"marginTop": "20px",
"lineHeight": "normal",
"height": "auto",
"textAlign": "center"
}
}
}
]
},
{
"image": "https://cdn.builder.io/api/v1/image/assets%2Fpwgjf0RoYWbdnJSbpBAjXNRMe9F2%2Ffb27a7c790324294af8be1c35fe30f4d",
"blocks": [
{
"@type": "@builder.io/sdk:Element",
"@version": 2,
"id": "builder-b875cfec35534ba99f844c782678fb91",
"component": {
"name": "Text",
"options": {
"text": "Enter some text..."
}
},
"responsiveStyles": {
"large": {
"display": "flex",
"flexDirection": "column",
"position": "relative",
"flexShrink": "0",
"boxSizing": "border-box",
"marginTop": "20px",
"lineHeight": "normal",
"height": "auto",
"textAlign": "center"
}
}
}
]
}
]
}
},
"responsiveStyles": {
"large": {
"display": "flex",
"flexDirection": "column",
"position": "relative",
"flexShrink": "0",
"boxSizing": "border-box"
}
}
}
I’m getting these errors:
Warning: React.jsx: type is invalid – expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it’s defined in, or you might have mixed up default and named imports.
at Ee (webpack-internal:///(ssr)/./node_modules/.pnpm/@builder.io+sdk-react-nextjs@0.14.21_next@14.2.3_@babel+core@7.24.4_react-dom@18.3.1_react@18_phiha7xbsb6cfmiixd7orjunka/node_modules/@builder.io/sdk-react-nextjs/lib/node/USE_CLIENT_BUNDLE-14685e18.js:136:86)
…
Internal error: Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it’s defined in, or you might have mixed up default and named imports.
at aw (/home/nghiavi/prism-builder/node_modules/.pnpm/next@14.2.3_@babel+core@7.24.4_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:35:46775)
if I remove the <Blocks />
the component display correctly.
I tried with RSC but the errors look similar, in additional of this error but I don’t know if it’s related:
⨯ TypeError: Cannot read properties of undefined (reading ‘localState’)
at stringify ()
at stringify ()
at stringify ()
at stringify ()
at stringify ()
digest: “3223408045”
RSC version:
import { Blocks, BuilderBlock } from '@builder.io/sdk-react-nextjs';
import { Box, Card, CardSection, Grid, GridCol } from '@mantine/core';
import Image from 'next/image';
import { PropsWithBuilderContext } from '@/types';
interface Column {
blocks: BuilderBlock[];
image?: string;
}
interface CustomColumnsProps {
builderBlock?: BuilderBlock;
columns?: Column[];
}
export const CustomColumns = ({ builderBlock, columns = [] }: PropsWithBuilderContext<CustomColumnsProps>) => {
return (
<Box p={30}>
<Grid gutter='lg'>
{columns.slice(0, 4).map((col, index) => (
<GridCol
key={index}
miw={300}
span={{ base: 12, md: 6 }}
>
<Card>
<CardSection>
<div className='relative aspect-video'>
{col.image ? (
<Image
alt=''
className='object-cover'
fill
src={col.image ?? '/default-image.svg'}
/>
) : null}
</div>
</CardSection>
<Blocks
key={index}
blocks={col.blocks}
parent={builderBlock?.id}
path={`component.options.columns.${index}.blocks`}
/>
</Card>
</GridCol>
))}
</Grid>
</Box>
);
};
import { RegisteredComponent } from '@builder.io/sdk-react-nextjs';
import { CustomColumns } from '.';
export const CustomColumnsEntry: RegisteredComponent = {
component: CustomColumns,
name: 'CustomColumns',
isRSC: true,
inputs: [
{
name: 'columns',
type: 'array',
defaultValue: [
{
image:
'https://cdn.builder.io/api/v1/image/assets%2Fpwgjf0RoYWbdnJSbpBAjXNRMe9F2%2Ffb27a7c790324294af8be1c35fe30f4d',
blocks: [
{
'@type': '@builder.io/sdk:Element',
component: {
name: 'Text',
options: {
text: 'Enter some text...',
},
},
responsiveStyles: {
large: {
display: 'flex',
flexDirection: 'column',
position: 'relative',
flexShrink: '0',
boxSizing: 'border-box',
marginTop: '20px',
lineHeight: 'normal',
height: 'auto',
textAlign: 'center',
},
},
},
],
},
{
image:
'https://cdn.builder.io/api/v1/image/assets%2Fpwgjf0RoYWbdnJSbpBAjXNRMe9F2%2Ffb27a7c790324294af8be1c35fe30f4d',
blocks: [
{
'@type': '@builder.io/sdk:Element',
component: {
name: 'Text',
options: {
text: 'Enter some text...',
},
},
responsiveStyles: {
large: {
display: 'flex',
flexDirection: 'column',
position: 'relative',
flexShrink: '0',
boxSizing: 'border-box',
marginTop: '20px',
lineHeight: 'normal',
height: 'auto',
textAlign: 'center',
},
},
},
],
},
],
subFields: [
{
name: 'image',
type: 'file',
allowedFileTypes: ['jpeg', 'jpg', 'png', 'svg'],
required: true,
defaultValue:
'https://cdn.builder.io/api/v1/image/assets%2Fpwgjf0RoYWbdnJSbpBAjXNRMe9F2%2Ffb27a7c790324294af8be1c35fe30f4d',
},
{
name: 'blocks',
type: 'blocks',
hideFromUI: true,
helperText: 'This is an editable region where you can drag and drop blocks.',
defaultValue: [
{
'@type': '@builder.io/sdk:Element',
component: {
name: 'Text',
options: {
text: 'Enter some text...',
},
},
responsiveStyles: {
large: {
display: 'flex',
flexDirection: 'column',
position: 'relative',
flexShrink: '0',
boxSizing: 'border-box',
marginTop: '20px',
lineHeight: 'normal',
height: 'auto',
textAlign: 'center',
},
},
},
],
},
],
},
],
};
here is the page.tsx
rendering the custom component:
export default async function Page({ params, searchParams }: PageProps) {
const urlPath = (params.template ? `/${params.template}` : '') + '/' + (params.page?.join('/') ?? '');
const configuration = await getWebsiteConfiguration({ urlPath });
if (!isWebsiteConfigurationValid(configuration)) {
return <MissingBuilderConfigAlert configuration={configuration} />;
}
const cmsEndpoint = configuration.cmsEndpoint;
const stableId = configuration.stableId;
const pageContent = await fetchOneEntry({
options: getBuilderOptions('page', searchParams),
apiKey: PUBLIC_API_KEY,
model: 'page',
userAttributes: { urlPath },
cacheSeconds: 0,
staleCacheSeconds: 0,
});
const canShowContent = !!pageContent || isPreviewing(searchParams) || isEditing(searchParams);
if (!canShowContent) {
return <NotFound />;
}
const [{ responseData: webConfiguration }, { responseData: stableCustomFields }] = cmsEndpoint
? await Promise.all([
fetch(`${cmsEndpoint}/web/configuration`).then(parseResponseBody<ResponseData<WebConfiguration>>),
fetch(`${cmsEndpoint}/web/stable_custom_field`).then(parseResponseBody<ResponseData<StableCustomField[]>>),
])
: [{ responseData: null }, { responseData: null }];
const customFields = stableCustomFields
? stableCustomFields.reduce<Record<string, Omit<StableCustomField, 'media'>>>(
(fields, field) => ({
...fields,
[field.keyCode]: field,
}),
{},
)
: {};
return (
<main>
<Content
apiKey={PUBLIC_API_KEY}
content={pageContent}
customComponents={customComponents}
linkComponent={Link}
model='page'
data={{
searchParams,
cmsEndpoint,
configuration: webConfiguration,
customFields,
stableId,
}}
/>
</main>
);
}
I’ve built the site with CustomColumns code so you can take a look.