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()
?.assets?.block?.forEach(entry => {
links.set(entry.sys.id, entry)
assetMap
})
// create a map of all block and inline entries
const entryMap = new Map()
?.entries?.block?.forEack(entry => {
links.set(entry.sys.id, entry)
entryMap
})?.entries?.inline?.forEach(entry => {
links.set(entry.sys.id, entry)
entryMap
})
return {
renderNode: {
.HEADING_2]: (node, children) => {
[BLOCKSreturn <h2>{children}</h2>
,
}
.EMBEDDED_ASSET]: (node, children) => {
[BLOCKS// find the asset in our map
const asset = assetMap.get(node.data.target.sys.id)
return (
<MyImage
={asset.url}
src={asset.width}
width={asset.height}
height={asset.description}
alt/>
),
}
.EMBEDDED_ENTRY]: (node, children) => {
[INLINESconst 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.