đŸ‘©â€đŸ’» chrismanbrown.gitlab.io

making a static website with nextjs and contentful

my experience using nextjs as a ssg

2021-10-10

I spent most of the afternoon trying to use next.js with contentful.com as a CMS to create a static website.

It was not super fun to get started.

Here’s the thing. Next.js was designed to incorporate server-side rendering in a hybrid client-side / server-side render model. Static site generation is a new feature of the framework that isn’t fully baked yet.

1. How to build

This isn’t obvious from skimming the docs. If you want to export a static site from your next.js project, you first have to next build to build your site in the /.next directory, and then next export to create a static export from /.next in /out.

My recommendation is to add the following to the scripts section of your package.json:

scripts {
  build: "next build && next export",
  serve: "npx serve out",
}

2. next/image

The next thing that happens is that if you have an image in your site, the export will fail.

next/image is what looks like a great image optimizer. But if you use it at all during export, everything breaks.

Hilariously, if you don’t use next/image and decide to just use an img element, next.js will warn you to use next/image instead.

3. Get static paths and props

Okay one really great feature of next (and nuxt, and sveltekit – really, who doesn’t do this at this point?) is its file-system based router.

But when you’re making a static site you have to do a few extra steps to get it to work.

Each page that takes a prop now requires a getStaticProps function to resolve and return the props passed to your component. It’s where you put your API calls or whatnot.

Any dynamic route probably requires a getStaticPaths function as well to resolve and return all possible paths that the dynamic path could resolve to. So that next can iterate over all of them and build them. If you have a dynamic route for blog posts, for example, this is where you’ll do a getAllPosts call:

export async function getStaticPaths() {
  const allPosts = (await getAllPosts()) ?? []
  return {
    paths: allPosts?.map(({ slug }) => `/post/${slug}`),
    fallback: false,
  }
}

Note: the static build here continued to fail for me until I set fallback to false so that it treated the list of allPosts as fully exhaustive.

These functions take over for the routing and stuff that next.js offloads to the server, and without them the ssg freaks out.

4. Rendering rich text

If you’re using next to make a static blog, you’re going to need to render rich text, because that’s what blog posts are made out of.

Actually, you’re probably going to want to render rich text at some point now matter what you’re making. Because, you know, rendering text on a page is kind of what the web is all about.

Well you can’t do that right out the gate. But contentful ships a @contentful/rich-text-react-renderer package that does a very basic job of rendering rich text.

And then there’s a funky kind of API provided by @contentful/rich-text-types to return markup or components for each block type, which is important for the next part.

5. Rendering inline assets and entries

So by default you can’t render any embedded assets or entries in your rich text. Think, an embedded image, an embedded link to another blog post, etc, etc.

So the convention is apparently to fetch the document, collect all the linked assets and entries, and then use the above mentioned funky rich-text-types API to render them.

It looks a little something like this:

// renderOptions.js
import { BLOCKS, INLINES } from '@contentful/rich-text-types';

export default function renderOptions(links) {

  // create a map of all block assets
  const assetMap = new Map()
  links?.assets?.block?.forEach(entry => {
    assetMap.set(entry.sys.id, entry)
  })

  // create a map of all block and inline entries
  const entryMap = new Map()
  links?.entries?.block?.forEack(entry => {
    entryMap.set(entry.sys.id, entry)
  })
  links?.entries?.inline?.forEach(entry => {
    entryMap.set(entry.sys.id, entry)
  })

  return {
    renderNode: {
      [BLOCKS.HEADING_2]: (node, children) => {
        return <h2>{children}</h2>
      },

      [BLOCKS.EMBEDDED_ASSET]: (node, children) => {
        // find the asset in our map
        const asset = assetMap.get(node.data.target.sys.id)
        return (
          <MyImage
            src={asset.url}
            width={asset.width}
            height={asset.height}
            alt={asset.description}
          />
        )
      },

      [INLINES.EMBEDDED_ENTRY]: (node, children) => {
        const entry = entryMap.get(node.data.target.sys.id)

        if (entry.__typename === 'ContentType') {
          return <ContentTypeComponent {...entry} />
        } else if (entry.__typename === 'AnotherContentType') {
          return <AnotherContentTypeComponent {...entry} />
        } else if (entry.__typename === 'YetAnother') {
          return <YetAnotherComponent {...entry} />
        }
      },
    }
  }
}

And then you use this renderOptions like this:

// pages/post/[slug].js
import { documentToReactComponents } from '@contentful/rich-text-react-renderer';
import renderOptions from '../../lib/renderOptions'

export default function Post({ post: { content } }) {

  return (
    <article>
      <section>
        <p>{documentToReactComponents(content.json, renderOptions(content.links))}</p>
      </section>
    </article>
  )
}

This is a fair bit of tedius work, but it is actually a really cool feature because it not only allows you to write custom components and styles for rich text elements, but it also allows you to do the same for any custom content types you create in contentful.

Conclusion

That’s it. With all those things accounted for and taken care of, you can start making a static website with next.js and contentful.

next.js started with server-side rendering, and then moved on to a hybrid client/server model. Full client side rendering (static websites) is the last paradigm to be supported by next.js, and it shows: it’s not fully baked or supported yet.