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.