Nextjs - Defining and Rendering Blocks

default discord avatar
Taun
8 months ago
41

I'm still trying to wrap my mind around how a headless CMS like Payload works. I'd like to render a Hero block's content.



Is this the general process:



1. Define collections (sets of data), such as Pages, Media and Portfolio.


2. Within Nextjs, the [...slug].tsx renders the pages data and RenderBlocks component renders the blocks. This way pages can be created using the CMS admin panel and they'll automatically be rendered.


3. Blocks folders contain a fields config file, as well as a component file that has the front-end logic.



This is where I'm lost... how does Payload CMS import the block files (the fields and the component) to be used within the admin panel?



When I select a block to be added to a page (using the Payload dashboard) and upload the data, will it read my component file and render that block to that page automatically? So I can then navigate to that page slug and the block's content will be rendered according to the block component logic and styling? I'm wondering how that system works.



I see the Pages collection file contains an object that defines a layout and imports the blocks.


{
      name: 'layout',
      label: 'Page Layout',
      type: 'blocks',
      minRows: 1,
      blocks: [
        CallToAction,
        Content,
        Image,
        CoverBlock,
      ],
    },

How are the components being rendered? Through the RenderBlocks component? How is that logic finding the blocks components and rendering?



import React from 'react';
import { Layout } from '../../collections/Page';
import { components } from '../../blocks';
import classes from './index.module.css';

type Props = {
  layout: Layout[]
  className?: string
}

const RenderBlocks: React.FC<Props> = ({ layout, className }) => (
  <div className={[
    classes.renderBlocks,
    className,
  ].filter(Boolean).join(' ')}
  >
    {layout.map((block, i) => {
      const Block: React.FC<any> = components[block.blockType];

      if (Block) {
        return (
          <section
            key={i}
            className={classes.block}
          >
            <Block {...block} />
          </section>
        );
      }

      return null;
    })}
  </div>
);

export default RenderBlocks;


Nextjs - Defining and Rendering Blocks

  • default discord avatar
    itsjxck
    8 months ago

    As I understand it, a

    Block

    in Payload context is just encapsulated configuration. How it renders in the Payload admin panel is entirely dependent on the

    fields

    configured within the

    Block


    When I select a block to be added to a page (using the Payload dashboard) and upload the data, will it read my component file and render that block to that page automatically? So I can then navigate to that page slug and the block's content will be rendered according to the block component logic and styling? I'm wondering how that system works.

    By this, do you mean "render that block to that page automatically"

    within

    Payload or in your app?

  • default discord avatar
    Taun
    8 months ago

    Hi @itsjxck . Yea I'm running a Next.js Payload boilerplate so the CMS and front-end logic is in one repo. I think it's called a mono-repo.



    So I have a block folder called CoverBlock, containign the field config CoverBlockFields.ts and a component 'CoverBlockComponent.tsx'.


    I've configured the block fields and added it to the page config so I can add the block within a page. I'm wondering how I can connec the component to the block to fetch and render that data accordingly?



    Here's the code:



    // CoverBlockComponent.ts
    
    import Image from 'next/image';
    import { CoverBlockType } from './CoverBlockFields';
    import RichText from '../../components/RichText';
    import React from 'react';
    
    const CoverBlockComponent: React.FC<CoverBlockType> = (props) => {
      const { content, image } = props;
    
      return (
        <div>
          <Image
            src={`${process.env.NEXT_PUBLIC_SERVER_URL}/media/${image.filename}`}
            alt={image.alt}
            width={image.width}
            height={image.height}
          />
          <RichText content={content} />
        </div>
      );
    };
    
    export default CoverBlockComponent;


    // blocks/CoverBlock/fields.ts
    import { Block } from 'payload/types';
    import { MediaType } from '../../collections/Media';
    
    export type CoverBlockType = {
      blockType: 'content',
      blockName?: string,
      content: unknown,
      image: MediaType,
    };
    
    const CoverBlock: Block = {
      slug: 'cover',
      labels: {
        singular: 'Cover',
        plural: 'Covers',
      },
      fields: [
        {
          name: 'content',
          label: 'Heading',
          type: 'richText',
          required: true,
        },
        {
          name: 'feature',
          label: 'Cover Image',
          type: 'relationship',
          relationTo: 'media',
          required: true,
        },
      ],
    };
    
    export default CoverBlock;
  • default discord avatar
    itsjxck
    8 months ago

    Ahhh, I assume you're using the Payload Nextjs boilerplate then?

  • default discord avatar
    Taun
    8 months ago

    yes



    On the coverblocktest page that I'm rendering the block to, the image loads, but not the rich text? That's probably some basic front-end code I'm getting wrong.

  • default discord avatar
    itsjxck
    8 months ago

    Interesting. Using your examples, and the NextJs Custom Server boilerplate (

    https://github.com/payloadcms/nextjs-custom-server/tree/master/blocks/Content

    ) as reference, I think maybe the config for coverblock is slightly incorrect. Your

    CoverBlockType

    has

    blockType: 'content'

    which could be causing an issue as there is already a block defined with that type



    I'd expect your config to be something like:


    // blocks/CoverBlock/Config.ts
    import { Block } from 'payload/types';
    import { MediaType } from '../../collections/Media';
    
    const CoverBlock: Block = {
      slug: 'cover',
      labels: {
        singular: 'Cover',
        plural: 'Covers',
      },
      fields: [
        {
          name: 'content',
          label: 'Heading',
          type: 'richText',
          required: true,
        },
        {
          name: 'feature',
          label: 'Cover Image',
          type: 'relationship',
          relationTo: 'media',
          required: true,
        },
      ],
    };
    
    export default CoverBlock;

    // blocks/CoverBlock/Component.tsx
    
    import Image from 'next/image';
    import { CoverBlockType } from './CoverBlockFields';
    import RichText from '../../components/RichText';
    import React from 'react';
    
    export type Type = {
      blockType: 'cover',
      blockName?: string,
      content: unknown,
      image: MediaType,
    };
    
    const CoverBlockComponent: React.FC<Type> = (props) => {
      const { content, image } = props;
    
      return (
        <div>
          <Image
            src={`${process.env.NEXT_PUBLIC_SERVER_URL}/media/${image.filename}`}
            alt={image.alt}
            width={image.width}
            height={image.height}
          />
          <RichText content={content} />
        </div>
      );
    };
    
    export default CoverBlockComponent;


    You will need to update this file similarly

    https://github.com/payloadcms/nextjs-custom-server/blob/master/blocks/index.ts


    To include your new block type



    The

    RenderBlocks

    component imports the config of your blocks here

    https://github.com/payloadcms/nextjs-custom-server/blob/master/components/RenderBlocks/index.tsx#L3

    Then uses the

    blockType

    field to pick the correct component to render here

    https://github.com/payloadcms/nextjs-custom-server/blob/master/components/RenderBlocks/index.tsx#L18


    So your

    blocks/index.ts

    will looks something like this:


    import { CallToAction } from './CallToAction/Config';
    import { Component as cta } from './CallToAction/Component';
    import { Content } from './Content/Config';
    import { Component as content } from './Content/Component';
    import { Image } from './Image/Config';
    import { Component as image } from './Image/Component';
    
    // Your component
    import CoverBlock from './CoverBlock/Config';
    import CoverBlockComponent from './CoverBlock/Component' ;
    
    export const collections = {
      CallToAction,
      Content,
      Image,
      CoverBlock,
    };
    
    export const components = {
      cta,
      content,
      image,
      // This field name needs to match the `blockType` of your Component `Type` config
      cover: CoverBlockComponent,
    };
  • default discord avatar
    Taun
    8 months ago

    Thanks a lot @itsjxck . For some reason I included

    export type CoverBlockType = {
      blockType: 'content',
      blockName?: string,
      content: unknown,
      image: MediaType,
    };

    I can remove that right? The rest of my code matches yours -



    // blocks/CoverBlock/fields.ts
    import { Block } from 'payload/types';
    import { MediaType } from '../../collections/Media';
    
    export type CoverBlockType = {
      blockType: 'content',
      blockName?: string,
      content: unknown,
      image: MediaType,
    };
    
    const CoverBlock: Block = {
      slug: 'cover',
      labels: {
        singular: 'Cover',
        plural: 'Covers',
      },
      fields: [
        {
          name: 'content',
          label: 'Heading',
          type: 'richText',
          required: true,
        },
        {
          name: 'feature',
          label: 'Cover Image',
          type: 'relationship',
          relationTo: 'media',
          required: true,
        },
      ],
    };
    
    export default CoverBlock;


    Oh I see you have that in the component file. I'll move it there.

  • default discord avatar
    itsjxck
    8 months ago

    You need to change the

    blockType

    to match the

    slug


    If you have this in a public repo I'd be happy to have a look and do a PR for you

  • default discord avatar
    Taun
    8 months ago

    So the export type

     export type CoverBlockType = {
      blockType: 'content',
      blockName?: string,
      content: unknown,
      image: MediaType,
    };

    of the CoverBlockComponent file needs to match the slug 'cover' of the fields?

  • default discord avatar
    itsjxck
    8 months ago

    Yeah, the field

    CoverBlockType.blockType

    needs to be

    cover

    instead of

    content

    to match your

    CoverBlock.slug
  • default discord avatar
    Taun
    8 months ago

    Ooh, thanks. Here's the repo:

    https://github.com/taunhealy/2160-Lodge-Bukela-2


    I'll appreciate you taking a look and making changes

  • default discord avatar
    itsjxck
    8 months ago

    My GH username is

    itsjxck

    (

    https://github.com/itsjxck

    ) if you wouldn't mind giving me access so i don't need to fork it

  • default discord avatar
    Taun
    8 months ago

    Sure, sent.



    So the process is:



    1. Define collections fields


    2. Define block fields, such as the slug.


    3. Create the block component and export the type, such as CoverBlockType and define the blockType as 'cover.'


    4. Import and call the CoverBlock fields and component to the blocks/index.ts and define the slug 'cover' that is the block type of the component. That way Payload will know to render the component that contains the 'cover' slug



    ?

  • default discord avatar
    itsjxck
    8 months ago
    https://github.com/taunhealy/2160-Lodge-Bukela-2/pull/1


    @Taun I updated your files to make them consistent with the other Blocks which might help you to see the differences between the provided Blocks and your custom one

  • default discord avatar
    Taun
    8 months ago

    Great, thanks a lot. Is the naming convention of Types etc industry standard?



    I don't understand how RenderBlocks is finding [block.blockType]? How does block.blockType correlate with



     export const components = {
      'call-to-action': CallToAction,
      'cta-grid': CTAGrid,
      content: Content,
      spacer: Spacer,
      statistics: Statistics,
      media: Media,
      'media-collage': MediaCollage,
      'media-content-collage': MediaContentCollage,
      'media-grid': MediaGrid,
      'media-stat-collage': MediaStatCollage,
      'study-slider': StudySlider,
      'sticky-content': StickyContent,
      slider: Slider,

    ?

  • default discord avatar
    Marťafiixek
    8 months ago

    Checked you repository. You mean this:


     `components[block.blockType];`

    Every block you get from Payload CMS (from API call) is an object. Every block therefore has a property called

    blockType

    .



    Property

    blockType

    is created from slug property you define in Payload.



    When you do the below, you are simply searching for a key in a constant

    components

    that equals to

    block.blockType

    .


     `components[block.blockType];`


    --- Another look at the problem



    When you create a block in Payload CMS with slug

    call-to-action

    , you place it in a page, and then you fetch the page, you will get a huge page object, which can contain several blocks.



    When you want to render blocks, then you will use that

    RenderBlocks

    component.

    RenderBlocks

    goes through the array of blocks you pass to the component.



    If a

    call-to-action

    block is currently being in the loop, it will do


    components[block.blockType]

    eg.


    components["call-to-action"]


    which is going to find the

    call-to-action

    key inside

    components

    which will output the

    CallToAction

    component

  • default discord avatar
    Taun
    8 months ago

    Thanks a lot @Marťafiixek for the great explanation. I get it now.



    I'm unable to render my component - the page (coverblocktest) is displaying the default boilerplate text and image and isn't rendering the rich text and styling for the image to be full-screen. Do you know why?



    Here's the repo:


    https://github.com/taunhealy/2160-Lodge-Bukela-2
    image.png
  • default discord avatar
    Marťafiixek
    8 months ago

    I cloned your repository and installed all packages needed, however there is a lot of errors in your source code



    And I don't have much time to fix them



    I would maybe clone the template again and start again



    Because for example, you have these imports





    But CoverH2/Component doesn't contain a named export



    It only contains a default export



    Code_Ijt860Lw9Q.png
    Code_YlOueoeo7a.png
  • default discord avatar
    Taun
    8 months ago

    Thanks @Marťafiixek , I'll do that.

Open the post
Continue the discussion in Discord
Like what we're doing?
Star us on GitHub!

Star

Connect with the Payload Community on Discord

Discord

online

Can't find what you're looking for?

Get help straight from the Payload team with an Enterprise License.