prevwong / craft.js

🚀 A React Framework for building extensible drag and drop page editors
https://craft.js.org
MIT License
7.31k stars 704 forks source link

Server side rendering possibility #200

Open timc1 opened 3 years ago

timc1 commented 3 years ago

To support rendering Craft nodes into static markup on the server, we've had to hack around the way blocks are built – instead, it would be nice to just build various blocks the way they're supposed to be as per the docs and have SSR built-in.

What would enabling rendering Elements on the server entail? Either way seems pretty critical to get this type of support for a landing page!

visualcookie commented 2 years ago

To support rendering Craft nodes into static markup on the server, we've had to hack around the way blocks are built – instead, it would be nice to just build various blocks the way they're supposed to be as per the docs and have SSR built-in.

What would enabling rendering Elements on the server entail? Either way seems pretty critical to get this type of support for a landing page!

Hej, I'm currently looking into SSR with craft.js and saw that you figured out a hacky way. Mind presenting how you solved it until this Feature gets really implemented into craft.js? Would be greatly appreciated.

hugominas commented 2 years ago

I have also tried implementing this feature a couple of times but got stuck, the components were not rendered properly.

It would be outstanding to have option to pre-rendering the components on the server and let next.js handle tha static caching after. It would a great upgrade for SEO . I hope @prevwong has some time to help us with this.

desis123 commented 2 years ago

yes that will be great addition

mattvb91 commented 1 year ago

@timc1 could you share some code for your solution?

hunter2009 commented 1 year ago

Is there any progress being made?

hennessyevan commented 10 months ago

Has anyone in user-land accomplished this?

pepas24 commented 10 months ago

I've made a functional app with server side rendering using Craft and Next JS, based on a comment I read time ago (I don't remeber where but here in the issues or discussions).

Its a private repo, but the main idea is to have a wrapper component with all Craft related behavior and then replace them with naked components when are loaded by Next. I think is a good approach almost for my use case.

This is the important part

const UserNode = ({ node, data }: any): any => {
  let typeName = ""

  if (typeof node.type === "object") {
    typeName = node.type.resolvedName
  } else {
    typeName = node.type
  }

  const Children = node.nodes.map((x: any) => {
    return <UserNode key={x} node={data[x]} data={data} />
  })

  switch (typeName) {
    case "ButtonCraft":  // use the sufix Craft convention to keep both components separated
      return <Button {...node.props} /> // then I switch to naked component with their props
    case "ContainerCraft":
      return <Container {...node.props}>{Children}</Container>
    default:
      return <></>
  }
}

export default function RenderLayout({ layout }: any) {
  // pass Craft layput to the recursive component
  return <UserNode node={layout.ROOT} data={layout} />
}

And this is how page code looks

export async function getServerSideProps() {
  const pageData = await getPage("my-page-slug")
  const page = JSON.parse(lz.decompress(lz.decodeBase64(pageData[0].content)))

  return {
    props: { page },
  }
}

export default function CustomPage({ page }: { page: any }) {
  return <RenderLayout layout={page} />
}

A Button component looks like this

// Button.craft.tsx
export function ButtonCraft(props: ButtonProps) {
  const {
    connectors: { connect, drag },
  } = useNode()

  return (
    <Button
      ref={(ref) => connect(drag(ref as HTMLButtonElement))}
      {...props}
    ></Button>
  )
}

ButtonCraft.craft = {
  displayName: "Button",
  props: {
    bg: "red",
    color: "black",
    size: "default",
    text: "Button",
  },
  custom: {
    userDeletable: true,
  },
  related: {
    settings: ButtonSettings,
  },
}
// Button.tsx
export interface ButtonProps {
  bg?: string
  color?: string
  size?: string
  text: string
}

const Button = forwardRef((props: ButtonProps, ref: Ref<HTMLButtonElement>) => {
  return <button ref={ref}>{props.text}</button>
})

Button.displayName = "Button"

export default Button

This approach also allows to keep things separated, where .craft files add editor behaivor, .settings add the component customization to the app and the component just handle their own end user behaivor.

imagen

I hope this can help, the project is old and maybe some things don't work now.

ErgeneGuo commented 8 months ago

I've made a functional app with server side rendering using Craft and Next JS, based on a comment I read time ago (I don't remeber where but here in the issues or discussions).我已经使用Craft和Next JS制作了一个服务器端渲染的功能性应用程序,基于我以前读到的评论(我不记得在哪里,但在这里的问题或讨论)。

Its a private repo, but the main idea is to have a wrapper component with all Craft related behavior and then replace them with naked components when are loaded by Next. I think is a good approach almost for my use case.这是一个私有的repo,但主要的想法是有一个包装组件与所有工艺相关的行为,然后替换它们与裸组件时,由下一步加载。我认为这是一个很好的方法,几乎为我的用例。

This is the important part这是最重要的部分

const UserNode = ({ node, data }: any): any => {
  let typeName = ""

  if (typeof node.type === "object") {
    typeName = node.type.resolvedName
  } else {
    typeName = node.type
  }

  const Children = node.nodes.map((x: any) => {
    return <UserNode key={x} node={data[x]} data={data} />
  })

  switch (typeName) {
    case "ButtonCraft":  // use the sufix Craft convention to keep both components separated
      return <Button {...node.props} /> // then I switch to naked component with their props
    case "ContainerCraft":
      return <Container {...node.props}>{Children}</Container>
    default:
      return <></>
  }
}

export default function RenderLayout({ layout }: any) {
  // pass Craft layput to the recursive component
  return <UserNode node={layout.ROOT} data={layout} />
}

And this is how page code looks这就是页面代码的样子

export async function getServerSideProps() {
  const pageData = await getPage("my-page-slug")
  const page = JSON.parse(lz.decompress(lz.decodeBase64(pageData[0].content)))

  return {
    props: { page },
  }
}

export default function CustomPage({ page }: { page: any }) {
  return <RenderLayout layout={page} />
}

A Button component looks like thisButton组件如下所示

// Button.craft.tsx
export function ButtonCraft(props: ButtonProps) {
  const {
    connectors: { connect, drag },
  } = useNode()

  return (
    <Button
      ref={(ref) => connect(drag(ref as HTMLButtonElement))}
      {...props}
    ></Button>
  )
}

ButtonCraft.craft = {
  displayName: "Button",
  props: {
    bg: "red",
    color: "black",
    size: "default",
    text: "Button",
  },
  custom: {
    userDeletable: true,
  },
  related: {
    settings: ButtonSettings,
  },
}
// Button.tsx
export interface ButtonProps {
  bg?: string
  color?: string
  size?: string
  text: string
}

const Button = forwardRef((props: ButtonProps, ref: Ref<HTMLButtonElement>) => {
  return <button ref={ref}>{props.text}</button>
})

Button.displayName = "Button"

export default Button

This approach also allows to keep things separated, where .craft files add editor behaivor, .settings add the component customization to the app and the component just handle their own end user behaivor.这种方法还允许保持事物分离,其中.craft文件添加编辑器编辑器,.settings将组件自定义添加到应用程序,组件只处理自己的最终用户编辑器。

imagen

I hope this can help, the project is old and maybe some things don't work now.我希望这可以帮助,该项目是旧的,也许有些东西现在不工作。

This should package all the components to the client-side? increase bundle size?