Editing of section model fields not visible in live editor - NextJS 14 app router

Builder content link

Builder public api key
87f7e6ddda884039ad862d083035a471

What are you trying to accomplish
I want to create a blog post template, so I created a blog post model. I want the title, main image and date to always be on the page in a certain format, so I coded those in my /blog/[…postUrl]/page.tsx. I also want my clients to be able to write the blog post however they want, so below the title, image, etc. I added the RenderBuilderContent component so that they can do that.

The content is rendered correctly and I can add content to the blog, but when I change the fields of the blogpost model (e.g. the title), I don’t see the changes live in the editor or in the draft. I have to publish the content, wait a few seconds, then refresh the page. Also, when I change the Url field, I instantly get a 404 page. Then again, I have to publish the changes and wait some time before I can see the content on the new Url.

I tried converting my page.tsx to a client component and using UseEffect to fetch te content on the page, but then I still had to refresh te page to see the changes. That is a bit better because I didn’t have to publish te content to see the changes, but it’s still not live editing and I want my pages to be rendered on the server for SEO.

Screenshots or video link
This is what my editor looks like. This is how it should be, but I want to see the changes that I make in the input fields on the right side (title, date, shortText) live in the editor.

Code stack you are integrating Builder with
@builder.io/react 3.2.6
@builder.io/sdk 2.2.2
next 14.2.1
react 18.2.0

Code

import { Builder, builder } from "@builder.io/sdk";
import { RenderBuilderContent } from "@/components/builder";
import MainWrapper from "@/components/layouts/MainWrapper";
import { BlogPost } from "../page";
import Image from "next/image";
import { BuilderContent } from "@builder.io/react";
import Title from "@/components/Standard/Title";
import Text from "@/components/Standard/Text";
import NotFound from "@/app/[...page]/not-found";

builder.init(process.env.NEXT_PUBLIC_BUILDER_API_KEY!);

interface PageProps {
  params: {
    postUrl: string;
  };
}

export async function generateStaticParams() {
  const articles = await builder.getAll("blogpost", {});

  return articles.map((article) => ({
    params: {
      postUrl: article?.data?.url,
    },
  }));
}

type BlogPostFull = BlogPost & {
  data: {
    date: string;
    content: BuilderContent;
    tags: string[];
  };
};

export default async function BlogArticle(props: PageProps) {
  const content: BlogPostFull = await builder
    .get("blogpost", {
      prerender: false,
      // Include references, like the `author` ref
      options: { includeRefs: true },
      query: {
        // Get the specific article by handle
        "data.url": props?.params?.postUrl?.toString(),
      },
    })
    .toPromise();

  if (!content) {
    return NotFound();
  }

  const formattedDate = new Date(content.data.date).toLocaleDateString(
    "Nl-nl",
    {
      year: "numeric",
      month: "long",
      day: "numeric",
    }
  );
  return (
    <>
      <div className=" prose-headings:font-rodetta prose-headings:font-bold text-neutral-content">
        <MainWrapper className="max-w-[850px] gap-5">
          <div className="w-full relative h-96">
            <Image
              src={content.data.mainImage}
              alt="alt"
              layout="fill"
              objectFit="cover"
              priority={true}
            />
          </div>
          <div className="">
            <Title order={1} text={content.data.title} />
            <Text text={formattedDate} className="italic" />
          </div>
          <Text
            text={content.data.shortText}
            className="font-semibold text-lg"
          />

          <RenderBuilderContent content={content} model="blogpost" />
        </MainWrapper>
      </div>
    </>
  );
}

"use client";
import { ComponentProps } from "react";
import { BuilderComponent, useIsPreviewing } from "@builder.io/react";
import { BuilderContent, builder } from "@builder.io/sdk";
import DefaultErrorPage from "next/error";
import "../builder-registry";

type BuilderPageProps = ComponentProps<typeof BuilderComponent>;

// Builder Public API Key set in .env file
builder.init("87f7e6ddda884039ad862d083035a471"!);

export function RenderBuilderContent({ content, model }: BuilderPageProps) {
  // Call the useIsPreviewing hook to determine if
  // the page is being previewed in Builder
  const isPreviewing = useIsPreviewing();
  // If "content" has a value or the page is being previewed in Builder,
  // render the BuilderComponent with the specified content and model props.
  if (content || isPreviewing) {
    return <BuilderComponent content={content} model={model} />;
  }
  // If the "content" is falsy and the page is
  // not being previewed in Builder, render the
  // DefaultErrorPage with a 404.
  return <DefaultErrorPage statusCode={404} />;
}

Hello @tobias,

You may need to use BuilderContent for live editing to work,

Example:

          <BuilderContent
            content={article}
            options={{ includeRefs: true }}
            model="blogpost"
          >
            {(data, loading, fullContent) => (
              <>
                <Head>
                  <title>{data?.title}</title>
                  <meta name="description" content={data?.blurb} />
                  <meta name="og:image" content={data?.image} />
                </Head>
                <div>
                  <BuilderComponent
                    model="blogpost"
                    content={fullContent}
                    options={{ includeRefs: true }}
                    data={{ article }}
                  />
                </div>
              </>
            )}
          </BuilderContent>

Additionally, you can refer to the below blog post for reference

Hope this helps!

Thanks,

Hello @manish-sharma, thank you for your reply.

When I try the method you suggested, I get this error:
“TypeError: (0 , react__WEBPACK_IMPORTED_MODULE_1__.createContext) is not a function”.

How can I resolve this? I saw something about using another experimental sdk, is that the only way to make it work? Does this method even make it work?

Hello @tobias,

If you’re utilizing the Next.js 14 app router with server-side components, we recommend using our Next.js experimental SDK. However, if you’re not utilizing any server-side components, you can opt for our SDK v1 or SDK v2.

You can find the Next.js experimental SDK example at the following link:

For further comparison between our SDK versions, you can refer to our documentation here:

Thanks,

Hello @manish-sharma. I configured the gen 2 SDK and it works, but I cannot find in your documentation how I can make a component accept children. Can you explain that to me or provide a link to where that is explained in your documentation? Thank you :smiley:

Hello @tobias,

Builder’s Gen 2 SDKs don’t require withChildren() . For more information on the SDKs, visit SDK Comparision .

For more details on child-related options when working with child components, visit the canHaveChildren , childRequirements, and defaultChildren sections of the registerComponent() documentation.

Yes I read that Gen 2 SDK’s don’r require withChildren(), so does that mean they accept children by default? Right now I have a component and I added these inputs trying to make it accept children:

 inputs: [
      {
        name: "children",
        type: "list",
        subFields: [
          {
            name: "blocks",
            type: "uiBlocks",
          },
        ],
      },
    ],

Hello @tobias,

That is right, GEN 2 accepts children by default and your implementation is correct.

It is not working in the visual editor. In my NextJS code I can add children to the component without problems or errors, but in the VE I cannot drag items in the component. When I try to do that, I can only place it above or below my component.

Hello @tobias,

Can you share your code example?

Yes I can, here’s some of my code:

Component code:

import { twMerge } from "tailwind-merge";

type MainWrapperProps = {
  children: React.ReactNode;
  className?: string;
};
export default function MainWrapper(props: MainWrapperProps) {
  return (
    <>
      <div className="w-screen flex justify-center pt-20">
        <main
          className={twMerge(
            "w-full max-w-[1100px] flex flex-col gap-16 px-4",
            props.className
          )}
        >
          {props.children}
        </main>
      </div>
    </>
  );
}

const componentsConfig = [
 {
    name: "MainWrapper",
    path: "layouts/MainWrapper",
    isRSC: true,
  },
]

export default componentsConfig;

// This is just a function to render the components easier, when I remove it to try the default method it still doesn't work.
export async function loadComponents() {
  const imports = componentsConfig.map((conf) =>
    import(`@/components/${conf.path}`).then((module) => ({
      name: conf.name,
      component: module.default,
      inputs: conf.inputs,
      isRSC: conf.isRSC,
    }))
  );
  return Promise.all(imports);
}

Page code:

import {
  Content,
  fetchOneEntry,
  getBuilderSearchParams,
} from "@builder.io/sdk-react-nextjs";
import { loadComponents } from "./componentsConfig";
import Image from "next/legacy/image";

interface MyPageProps {
  params: {
    page: string[];
  };
  searchParams: Record<string, string>;
}

const apiKey = "87f7e6ddda884039ad862d083035a471";

export default async function Page(props: MyPageProps) {
  const customComponents = await loadComponents();

  const urlPath = "/" + (props.params?.page?.join("/") || "");

  const content = await fetchOneEntry({
    model: "page",
    apiKey,
    options: getBuilderSearchParams(props.searchParams),
    userAttributes: { urlPath },
  });

  return (
    <>
      <Content
        content={content}
        model="page"
        apiKey={apiKey}
        customComponents={customComponents}
      />
    </>
  );
}
export const revalidate = 1;

Hello @tobias,

Could you please share the code snippet for registering the custom component with Builder.io using uiBlocks? That would help us better understand the implementation and provide you with the appropriate guidance.

Hello @manish-sharma.
Yes, this is the full code of the component registration. I believe I already provided you with the rest of the code.

const componentsConfig = [
{
    name: "MainWrapper",
    path: "layouts/MainWrapper",
    inputs: [
      {
        name: "children",
        type: "list",
        subFields: [
          {
            name: "blocks",
            type: "uiBlocks",
          },
        ],
      },
    ],
    isRSC: true,
  },
]

export default componentsConfig;

// This is just a function to render the components easier, when I remove it to try the default method it still doesn't work.
export async function loadComponents() {
  const imports = componentsConfig.map((conf) =>
    import(`@/components/${conf.path}`).then((module) => ({
      name: conf.name,
      component: module.default,
      inputs: conf.inputs,
      isRSC: conf.isRSC,
    }))
  );
  return Promise.all(imports);
}

With this code, I cannot add child components to the component in the VE, I can only add a empty child as a list item.

Hello @tobias,

I hope the issue with dragging and dropping the child component in the main wrapper parent component is fixed now.

The below example is working for us

const customComponents = [
  {
    component: MainWrapper,
    name: "MainWrapper",
    path: "layouts/MainWrapper",
    canHaveChildren: true,
    inputs: [
      {
        name: "section",
        type: "list",
        subFields: [
          {
            name: "children",
            type: "uiBlocks",
            defaultValue: [],
          },
        ],
      },
    ],
    isRSC: true,
  },
];

export default customComponents;

import { Blocks } from "@builder.io/sdk-react";
type MainWrapperProps = {
  children: React.ReactNode;
  className?: string;
};

export default function MainWrapper(props: MainWrapperProps) {

  return (
    <>
      <div
        style={{
          width: "100%",
          display: "flex",
          justifyContent: "center",
          paddingTop: "5rem",
        }}
      >
        <main
          style={{
            width: "100%",
            maxWidth: "1100px",
            display: "flex",
            flexDirection: "column",
            gap: "1rem",
            padding: "1rem",
            ...props.style, // Spread any additional styles passed via props
          }}
          className={props.className}
        >
          {props.children}
        </main>
      </div>
    </>
  );
}