skip to Main Content

I was developing a website where all the site sections were rendered from a JSON payload. In other words, the JSON had information such as the name of the section component to which it refers and attributes referring to the properties that must fill in this section (margins, field values, etc.). That’s, from a JSON with a defined structure, I could assemble and reassemble several pages without changing the code, just modifying the JSON by an external tool, for example.

This application was made using NextJS. GetStaticProps method requests the payload for a specific page to an external tool based on the requested route path (for example, localhost:3000/example gets /example payload). Then, it sends this payload to the page component of its props. So far, so good, but I discovered an SEO problem in how this payload became a page that will be explained below.

In the file referring to a page, there is a logic to perform the dynamic import of each section component. First, using the getStaticProps payload, an iteration is made over the array with all the data to assemble a section. Then, for each element of this array, the page took the value referring to the name of the component that imports and performed the lazy import with React suspense to import the component and pass to it the rest of the attributes through the spread operator (Can be seen in the code below).

[...]

const importSection = (sectionName: string) =>
    lazy(() =>
        import(`@templates/sections/${sectionName}`).catch(
            () => import("@components/others/nullView")
        )
    );

[...]

const sectionPromise = sections.map(async (data: any) => {
    const componentName = data.component_name;
    const Section = await importSection(componentName);

    return <Section key={shortid.generate()} {...sectionData} />;
});

[...]

Everything was working perfectly. Each page had its sections rendered correctly. However, this process happened on the client side and, for Lighthouse, for example, or for inspecting it from the browser, the generated HTML was perfect. But for the search engines, this wasn’t very good because they didn’t see the page mounted even though I used getStaticProps and mounted a static page on the server side.

It happens because the trigger for the function to perform this lazy import of components and forward to the page was done using two famous React hooks: useEffect (which runs on the client side) and useState (which is asynchronous). I used useEffect as a trigger, capturing the page’s mount event and triggering the function to perform the lazy import, passing the payload returned by getStaticProps. During this process, React’s Suspense had a loading component. However, useEffect only runs on the client side. In other words, I could not have the page mounted on the server side, which caused my SEO to be lost. So, I refactored useEffect to have this mount functionality on the server side using IIFEs to auto-execute this lazy import function on the server side. The solution worked initially, but I couldn’t assemble the page with the sections, even with the import of components. It happens because I used useState to receive the component listing, and the useState runs asynchronously. Therefore, importing the components does not always end before the page is assembled.

Therefore, during the build to deliver the page, the component remained without the content already rendered because the page finished its assembly process before the state that stores the values ​​returned from the lazy import finished its update (the div __next has no children). I first tried to implement a state-pool allowing me to use state control on the server side. However, the problem of timing continued. I tried countless ways to make useState synchronous, but they didn’t work. Therefore, I need a solution so that I can run the lazy import on the server side and that, during the build, the application waits for the lazy import to finish so that the page comes from the server side with the HTML perfectly assembled.

The complete code with useEffect and useState is below.

[...]

const importSection = (sectionName: string) =>
    lazy(() =>
        import(`@templates/sections/${sectionName}`).catch(
            () => import("@components/others/nullView")
        )
    );

const GenericPage = ({ pageContent }: SectionPayload): ReactElement => {
    const [sections, setSections] = useState<Array<ReactElement>>([]);

    useEffect(() => {
        (async (): Promise<void> => {
            const payloadSections = pageContent?.data.sections || [];

            const sectionPromise = payloadSections.map(async (data: any) => {
                const Section = await importSection(componentName);
                return <Section key={shortid.generate()} {...data} />;
            });

            Promise.all(sectionPromise)
                .then(setSections)
                .catch(() => console.warn("ERROR"));
        })();
    }, [pageContent]);

    return (
        <Suspense fallback={<LoadingPage />}>
            <Main>{sections}</Main>
        </Suspense>
    );
};

export const getStaticProps: GetStaticProps = async (context: GetStaticPropsContext) => {
    if (!context.params) {
        throw new Error("Params not found");
    }

    const pageID = context.params.genericPageID;

    const pageContent = await getPageContent(
        pageID,
        `populate[head][populate]=%2A&populate[dynamic_zone][populate]=%2A&locale=${context.locale}`
    );

    return {
        props: { pageContent },
        revalidate: 60,
    };
};

export const getStaticPaths: GetStaticPaths = async () => {
    [...]
};

export default GenericPage;

2

Answers


  1. You can use Next.js dynamic import:

    // Import all your components/sections/whatever you call it with "dynamic" import
    
    import dynamic from 'next/dynamic'
    
    const Title = dynamic(() => import('./Title'), {
      suspense: true
    });
    const Image = dynamic(() => import('./Image'), {
      suspense: true
    });
    const List = dynamic(() => import('./List'), {
      suspense: true
    });
    

    And then just iterate over your data and return the needed component for each section type:

        <Suspense fallback={'Loading...'}>
          {components.map((item, index) => {
            if (item.type === 'link') {
              return <Link href={item.href}>{item.text}</Link>;
            }
    
            if (item.type === 'title') {
              return <Title key={index} title={item} />;
            }
    
            if (item.type === 'image') {
              return <Image key={index} image={item} />;
            }
    
            if (item.type === 'list') {
              return <List key={index} list={item} />;
            }
    
            return <div key={index}>Unknown component</div>;
          })}
        </Suspense>
    

    Of course instead if simple if blocks you can use whatever you want, for example make some sort of map of components:

    const componentsMap = {
      title: Title,
      list: List,
      // ...
    }
    

    I hope you get the idea! Don’t forget to wrap this whole thing with Suspense or you can wrap individual components to make different loaders if you want to.

    Edit https://stackoverflow.com/questions/73279166

    Login or Signup to reply.
  2. Disclaimer: This is my project https://github.com/hacknlove/orgasmoproject

    The link avovbe is a NextJS plugin that gives you the kind of dynamic rendered content you are trying to write.

    You just:

    1. write some components and declare them as dynamic with a suffix .dynamic.tsx (or `.dynamic.tsx“
    2. write some driver methods to get the data from your data source, or use one of the available drivers (MongoDB, strapi, json files)
    3. define your pages in your database, using the provided playground if you want.
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search