⚔️ Code Conqueror

📜 Building a blog with Next.js

Mar 11, 2020

Next.js is a great (I would argue the best) platform for building a simple (but custom) blog in 2020. New features like static site generation and preview mode make building a blazing fast blog super simple.

It really boils down to a choice — do you write all your blog in markdown files, or do you use a content management system (CMS) to manage the posts. If you don't mind writing markdown then that is certainly the most straightforward route, but there are numerous advantages to using a CMS (better authoring environment, mobile editing support, easy multi-author support etc).

Markdown vs Content management systems (CMS)

Let's review the two main choices when it comes to getting started with your Next.js powered blog — if you should write your own markdown files for each post (like kentcdodds, or Next.js own blog), or if you should use a content management system or CMS like Prismic, TinaCMS, DatoCMS etc. Let's review the pros and cons of each:

Markdown (example code):

Pros:

  • dead-simple to get started, just add a file to
    pages/
    and adjust your config.
  • super easy to theme using a tool like Theme-ui
  • blazing fast static site is automatic since the content lives in the repository
  • using
    .mdx
    you can add rich content easily via React components

Cons:

  • less technical users might find it intimidating to edit (eg via github)
  • authoring environment is not to everyone liking
  • no easy way to edit on mobile

Content Management System (CMS, example code):

Pros:

  • Actually usable by non-technical users (if your blog is collaborative)
  • Modern CMS have lots of great features for controlling drafts / comments etc.
  • Some CMS (like Notion) have a first-rate editing environment that is much easier than writing markdown

Cons:

  • You have to write a lot more custom code in your blog to interact with the CMS
  • Depending on the CMS adding custom content is probably not as easy as with markdown (eg think if you want to add a new component that displays a tweet quote).

This very website is a Next.js blog that uses Notion as a CMS to back it. For us the trade-off was the authoring environment inside of Notion being a lot easier to write distraction free compared to markdown. At the same time, it makes maintenance on this site a lot higher because of all the custom things that need to be sorted out to parse the Notion blocks.

Setup Next.js to create a markdown blog

The steps here are really super simple. You get your Next.js project setup using the mdx example. by running:

yarn create next-app --example with-mdx with-mdx-app

Which will quickly scaffold out a Next.js application that serves a markdown file as it's homepage. From there you simply have to add your posts as files at the location you want the route to appear. So if my post about chickens was titled "everything chickens" then I would add a file at

pages/blog/everything-chickens.mdx
and add markdown to that file:

# Everything you need to know about chickens
I don't actually know anything, so will defer to
a [better resource](https://en.wikipedia.org/wiki/Chicken)

Adding metadata to the markdown files

You can export metadata for your post from the markdown file directly by defining a constant and exporting it. This will become very handy when we go to create the index of posts.

So in our case we can define a title, description and a little icon to show next to the title:

import withPost from "components/withPost";
export const meta = {
title: "Everything you need to know about chickens",
description: "Some info on chickens...",
icon: "🐔",
slug: "/blog/chickens"
};
# Everything you need to know about chickens
I don't actually know anything, so will defer to
a [better resource](https://en.wikipedia.org/wiki/Chicken)

Creating an index using webpack require magic

The next trick (as used on the Next.js blog) is to create an index of all the posts using webpack to find all the mdx files. We add these two lines to require all mdx files in our blog index:

// pages/index.js
function importAll(r) {
return r.keys().map(r);
}
const blogItems = importAll(require.context("./blog", false, /.mdx$/));

what we are doing here is getting all the mdx files including our custom

meta
export and then storing them in an array of objects. We then can iterate over that array to make an index of all the posts:

Using some webpack require magic we get all the posts in the index at build time

You could add some fancy things like pagination and search, but for now this is a basic starting point.

Adding custom components to your post

You might want to add a custom component to one of your posts in order to add some nice interactive context. For example say we want to add a tweet. We take the embed link from twitter and then can just paste directly in the

mdx

You can also define custom React components and import them, but for our purposes all we have to do is use the twitter embed code:

<blockquote class="twitter-tweet">
<p lang="en" dir="ltr">The coronavirus panic is dumb</p>
&mdash; Elon Musk (@elonmusk)
<a
href="https://twitter.com/elonmusk/status/1236029449042198528?ref_src=twsrc%5Etfw"
>March 6, 2020</a
>
</blockquote>
<script
async
src="https://platform.twitter.com/widgets.js"
charset="utf-8"
></script>

We have a repository that shows how to set all of this up. (Including theming / styling as discussed below).

Setup Next.js to create a CMS backed-blog

If you are creating a CMS backed blog you need to make an additional choice. Will your blog be statically generated, or server side rendered. If you come from using Gatsby.js you might find that the static generation is familiar, but server rendering also has it's place (as those server rendered pages can be cached). Some considerations:

Server side rendered:

Pros

  • Visitors will always get very fresh content, and using the
    stale-while-revalidate
    caching policy it's possible to give them a fresh page even after they visit.
  • You don't have to worry about the build step

Cons

  • Page load times can suffer if you don't hit the cache often
  • Can be more expensive (since the same server data would be computed multiple times) depending on how effective the caching is

Static Site generation:

Pros

  • Pages are lightning fast since they are just html, css and javascript that can be cached at the edge close to the user
  • The pages are more secure since all of the access to the data happens in development land and is decoupled from user land

Cons

  • Build times can take a while depending on various factors (like number of pages)
  • You have to write some extra connector code to have an effective preview mode (which you will want to do so that you can preview your content on-site live)

For a while server side rendering with some static optimization was really all that was possible in Next.js, but with the recent introduction of

getStaticProps
Next has much better support for static site generation (per page level), and a preview mode which takes some setup but can lead to an effective environment for both viewers and authors alike.

For our purposes we'll explore generating the site statically, since even though it is a bit more work it leads to an arguably better user experience (as long as your CDN cache is effective).

Managing Posts

For our CMS backend we will choose Notion.so not so much because it is intended to be used as a CMS, but more because it is a great platform for authoring content (and after all that's mostly the point of creating a blog). Notion has a public API that's undocumented so it takes some finesse to get things to work properly, but essentially you can follow this code from Zeit.

The key points are once you have the code to parse the API responses setting up your Next.js routes and static data fetching methods. Say we have an index page at

/pages/index.js
and a blog page at
/pages/[slug].js
then in the index we have to fetch the list of posts (in notion just a page that has links out to other pages containing the posts) like:

const HomePage = ({ data }) => {
return <div>** some logic for rendernig the parsed post index**</div>;
};
export async function getStaticProps() {
// fetch and parse the data from Notion:
const data = await getBlogIndex();
return { props: { data } };
}

And for the posts we also need to fetch the list of all the slugs at build time so Next.js knows which routes to configure:

const PostPage = ({ data }) => {
return <div>** some logic for rendering the post**</div>;
};
export async function getStaticProps({ params }) {
if (params) {
const { slug } = params;
if (typeof slug !== "string") {
throw new Error("slug can only be a string");
}
const data = await getBlogPost(slug);
return { props: { data } };
} else {
throw new Error("slug does not exist");
}
}
export async function getStaticPaths() {
const data = await getBlogIndex();
const paths = Object.keys(data).map((slug) => ({
params: { slug },
}));
return {
paths,
fallback: false,
};
}

Here we generate both the

getStaticPaths
and the
getStaticPosts
which are used to render the page. We have omitted the actual post rendering logic, which would depend both on your CMS, but also on the way that you structured the pages (eg which metadata). But you can see a simple example in the demo repository. We will discuss below a few more specifics if you want to do this with Notion.so, but the above code will work roughly the same way with any CMS as long as you correctly define the
getBlogPost
and
getBlogIndex
functions.

Preview mode

Preview mode is an exciting extension of static site generation in Next.js which sets up authenticated cookies to let Next know that instead of rendering the pages statically it should instead server side render the page.

This has the advantage that you can view the post as you write it on the page as it will be rendered, while end-users just see the published static page.

To enable preview mode you generate a preview route like

/pages/api/preview.js
which is responsible for setting the appropriate cookies and redirecting to the post page. The page that uses
getStaticProps
is aware of those cookies set by the preview route and adjusts to server side rendering accordingly. You can also pass through a boolean that says if you are in preview mode, eg. to display somewhat different UX (a link to the CMS or a badge that warns of preview mode). The preview route should look something like:

export default async (req: NextApiRequest, res: NextApiResponse) => {
if (typeof req.query.slug !== "string") {
return res.status(401).json({ message: "invalid slug" });
}
const post = await getBlogPost(req.query.slug);
if (!post) {
return res.status(401).json({ message: "Invalid slug" });
}
res.setPreviewData({});
res.writeHead(307, { Location: `/blog/${post.slug}` });
res.end();
};

You can see here that we also fetch the post like we will do after redirect, not necessarily because we need the data here, but because we want to eg. validate credentials for fetching the post data / that this post route actually works. In our setup you would access

/api/preview?slug=post-name
to get redirected to the preview version of your post.

Your implementation of Preview mode will likely vary widely between CMS providers, but we have provided a basic example using Notion.so forked from the Zeit example.

Theming your blog / making it look good

Without the advantage of a library of themes like Wordpress has you'll have to make some decisions about how to style the Blog UI. Of course part of the reason for writing your own blog from scratch is to have more control over the look and feel, but it can make it take a while to get started. To rapidly get your blog off the ground, so that you can focus on content, we recommend theming your blog with Theme UI. Theme UI is a library that allows you do define a theme spec which will set some basic style for your application. You then write either semantic HTML or use the built in theme-aware components in order to build your page. It even has built-in support for automatically theming mdx based markdown files (very handy for a blog). Let's see how this works in practice. You define a theme file that specifies some basic colors / fonts / spacing values:

// lib/theme.js
export default {
colors: {
text: "#000",
background: "#fff",
primary: "#07c",
},
fonts: {
body:
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif',
heading: "Georgia, serif",
},
fontWeights: {
body: 400,
heading: 700,
},
styles: {
h1: {
fontSize: 32,
fontFamily: "heading",
fontWeight: "heading",
color: "primary",
mt: 4,
mb: 2,
},
},
};

The dead easiest way to configure this is with one of the presets. But it allows you to have quite a bit of flexibility when it comes to styling, but you don't pollute your application with css-in-js unless it is explicitly an override.

On our markdown example we've added theme-ui to show how to set up up in a Next.js project. A similar scheme would work for the CMS backed projects using the built in theme-ui components to render the objects from the CMS (that's how this very site works!).