Yomi Eluwande JavaScript Developer. Wannabe Designer and Chief Procrastinator at Selar.co and Worklogs.co

An SEO approach to async components with loadable-components

4 min read 1280

An SEO Approach To Async Components With loadable-components

Have you ever viewed the page source for a web page while loading components asynchronously? If you have, there’s a possibility you may have noticed the actual content is not being rendered.

This is because the components are being loaded asynchronously and on the client side, which can be bad for SEO because crawlers will be unable to get the actual content when indexing the site.

This article will provide a method with which you can get the best of both worlds by using loadable-components to asynchronously load components.

We’ll be working with an existing Gatsby project in this article. The project already uses the loadable component, and we’ll see how to implement it better for SEO purposes.

Problem statement

We have a Gatsby site that uses dynamic data from Contentful, a content management system to render information and modules. The Gatsby site is a blog that shows all the posts from Contentful. The posts are written in a rich text editor, and we use a rich text renderer in the codebase to parse rich text to React components.

However, we’d also like to be able to display other things apart from blog posts on the site. To that end, we created a new content type in Contentful: custom blocks . A custom block, as the name suggests, would allow us to render custom blocks (content that is not necessarily a blog post) on the website.

This is where the challenge lies. As opposed to blog posts, which are always rendered in article format, custom blocks may need to be rendered by different and multiple React components depending on design. For example, there’s a React component for a pricing information custom block on Contentful, a React component for an FAQ custom block on Contentful, etc.

So, to implement that, there’s a file below that uses the custom block’s name to render its own component — i.e., if the custom block’s name matches any of the keys in CUSTOM_BLOCKS, then the corresponding component will be rendered.

// blocks.js
import TestPage from './TestPage'
import PricingInfo from './PricingInfo'
import FAQInfo from './FAQInfo'

const CUSTOM_BLOCKS = {
  TestPage: TestPage,
  PricingInfo: PricingInfo,
  FAQInfo: FAQInfo,
}
export default CUSTOM_BLOCKS

The custom blocks can then be used in a code snippet like the one below, where the CustomBlockComponent is only returned if there’s a corresponding match with customBlock.name.

// CustomBlock.js
import CUSTOM_BLOCKS from './blocks'

const CustomBlock = ({ customBlock }) => {
  const CustomBlockComponent = CUSTOM_BLOCKS[customBlock.name]
  if (!CustomBlockComponent) {
    return null
  }
  return <CustomBlockComponent customBlock={customBlock} />
}

export default CustomBlock

With this current implementation, we’re loading all the custom blocks and their components all at once, even though we don’t need them. Right now, it’s just two custom blocks, but imagine if it were a whole lot more than that.

We made a custom demo for .
No really. Click here to check it out.

Using loadable-components

A case like this is where loadable/component comes in. It allows us to only load the components when they are needed, i.e., asynchronously. Let’s add loadable/component to the first code snippet shared above.

// blocks.js
import loadable from '@loadable/component'

const CUSTOM_BLOCKS = {
  TestPage: loadable(() => import('./TestPage')),
  PricingInfo: loadable(() => import('./PricingInfo')),
  FAQInfo: loadable(() => import('./FAQInfo')),
}
export default CUSTOM_BLOCKS

All the custom blocks are being loaded asynchronously, so they’ll only be loaded when needed, which in turn results in the code being optimized for performance.

This is the reason why we have chosen to use loadable-components in our project, and it seems to solve the problem we initially had. However, importing the components with loadable means the content of the custom block will not be pre-rendered into the static HTML.

As an example, in the page source below, I’m expecting the Date One text to be in the source, but it’s not. The Date One text is inside one of the custom block files above, and it needs some JavaScript to be evaluated, hence, it’s not showing up.

Page Source Missing Async Components

This is what we’ll try to solve in this article: how to load the components asynchronously and also make sure that content gets rendered in the static HTML.

Configuring loadable-components

We can solve this by making some additional configurations to how loadable/component is set up. We already have loadable/component installed in the codebase, but we need to make some configurations. First, install the dependencies below.

yarn add -D @loadable/babel-plugin @loadable/webpack-plugin babel-preset-gatsby

The next thing is to add a custom Babel plugin to the project. To do that, we’ll need to modify the .babelrc.js file. In the plugins array, add the line below:

// .babelrc.js
{
  "plugins": [
    ...
    "@loadable/babel-plugin",
    ...
  ]
}

Next, we’ll add a custom webpack plugin to the gatsby-node.js file.

// gatsby-node.js
const LoadablePlugin = require('@loadable/webpack-plugin')

exports.onCreateWebpackConfig = ({ stage, actions }) => {
  actions.setWebpackConfig({
    plugins: [new LoadablePlugin()],
  })
}
exports.onCreateBabelConfig = ({ actions }) => {
  actions.setBabelPlugin({
    name: `@loadable/babel-plugin`,
  })
}

The final step in all of this is making sure that the content of the custom block is pre-rendered with the static HTML. One way to do that is by using the fallback prop of loadable/components.

Pre-rendering custom block elements in static HTML

The fallback prop determines what to show while the component is being loaded asynchronously. This is what will be used to make sure asynchronous components get rendered to the static HTML. How?

So, for asynchronous components, the following happens:

  1. Static HTML is rendered
  2. React components are hydrated into the static HTML
  3. Because of the asynchronous components taking time to resolve, the current DOM is destroyed and only created again when it’s done loading

We can then take advantage of step two to get and save the current static HTML and then use that as a fallback. That’s exactly what’s being done in the code snippet below. If you recall above, the CustomBlock.js file simply checks whether a custom block component exists and then returns it.

Now it’s doing a whole more than that:

  • Setting an id to CustomBlock__, plus whatever the current custom block name is
  • Adding a fallback prop, which is set to be HTML gotten from the getRenderedContent() function
  • Lastly, the getRenderedContent function checks whether an element with an ID exists in the HTML and, if yes, returns it
    // CustomBlock.js
    import * as React from 'react'
    
    import CUSTOM_BLOCKS from './blocks'</p>
    
    <p>const getRenderedContent = customBlockName => {
      if (typeof window === 'undefined') return ''
      const element = window.document.querySelector(
        <code>#CustomBlock__${customBlockName}</code>
      )
      return element ? element.innerHTML : ''
    }
    const CustomBlock = ({ customBlock }) => {
      const CustomBlockComponent = CUSTOM_BLOCKS[customBlock.name]
      if (!CustomBlockComponent) {
        return null
      }
      return (
        <section id={<code>CustomBlock__${customBlock.name}</code>}>
          <CustomBlockComponent
            customBlock={customBlock}
            fallback={
              <div
                dangerouslySetInnerHTML={{
                  __html: getRenderedContent(customBlock.name),
                }}
              />
            }
          />
        </section>
      )
    }
    export default CustomBlock

It’s a bit of a hack, but then we get to see the content of the asynchronous components in the page source, and that’s good for SEO.

Now we can build the site and run it in production with the commands below:

yarn build && serve public

The dates are now coming up in the page source, which means the custom block elements are now being pre-rendered which in turn means crawlers can successfully crawl this page.

Page Source With Async Components

Conclusion

To simulate what I’ve explained in this article, there’s a GitHub repository that contains the codebase for the Gatsby project above. It also contains an exported Contentful space so you can set that up (by importing into a new space) and connect to the Gatsby project.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Yomi Eluwande JavaScript Developer. Wannabe Designer and Chief Procrastinator at Selar.co and Worklogs.co

Leave a Reply