Building my blog

June 10, 2020

I wanted to take up the task of setting up a blog for the umpteenth time. I always tell myself that this is the time it'll get published. Well, here we are again. In this post I'm going to explore the process of setting up this blog, the adventures of trying to control the urge to over engineer, and hopefully some fun lessons I learned along the way.

Hello World

Given that this is my first post, I'll start off with an introduction. Hello, world! I'm Justin. I work as an engineer at Artsy where I try to lend my technical expertise to hopefully help expand the art market to support more art and artists in the world. I'm going to be using this blog primarily as a way to improve on expressing myself and to try to be better about learning in the open. Hopefully some of the content will be useful to someone out there!

A Notion of What to Build

When starting this project the most important aspect to me was having a good editing experience. I've primarily used git/markdown based editing systems in the past and definitely found myself wanting to try something new.

At Artsy, we use Notion.so to record a lot of information and I've been really appreciative of its editing experience. Though Notion doesn't yet have a public API @ijjk from Vercel has an interesting example using Next.js to serve up a blog with Notion as the backend. I ran across that example a few months ago and it's been on my back burner to try out every since. It wasn't until I ran across react-notion from splitbee that I decided to really try to spin up a blog with Notion + Next.js.

If you're unfamiliar with Next.js, it's a react framework for generating sites. It handles dynamic, static, and server-side generation and somehow manages to keep it extremely simple. Huge props to the Vercel team for making such great tool. I'm not really going to be going into the workings of Next in this post so I highly encourage you to check out their documentation before reading further.

react-notion is a react library for rendering data from Notion's private API in a way that makes it look nearly identical to what you'd actually be seeing in Notion. This was tremendously compelling for me because it largely removes the need to preview a post before it can be posted. Also, I end up endlessly yak shaving on text style when trying to build blog sites, leaning into Notion's styling lets me avoid all that.

Setting up the project

First off, I want my blog to be simple and fast. I'm using two primary mechanisms to achieve that:

  1. Utilizing Next's static site generation to prerender posts
  1. Using Preact instead of React

Statically generating posts

Static site generation with Next is fairly simple. In your dynamic route file you simply need to add a getStaticPaths function which returns a list of all the pages to be generated and a getStaticProps function which generates the props for any given page. My dynamic route file is [post].tsx which is responsible for rendering posts at https://just-be.dev/[post].

getStaticPaths

Here's what my getStaticPaths file looks like at the time of this writing:

export const getStaticPaths: GetStaticPaths = async () => {
  const posts = ((await getTableContents(
    process.env.BLOG_ID,
    process.env.NOTION_TOKEN
  )) as unknown) as Post[];

  return {
    paths: posts
      .filter((post) => post.Published)
      .map((post) => ({
        params: {
          ...post,
          post: post.Slug.split(",")[0].trim(),
        },
      })) as any,
  };
};

The first part calls getTableContents from a custom notion api I wrote that's based heavily off of Split Bee's notion-api-worker. Essentially it queries my blog overview page (shown below) and gets a JSON representation of all the posts in the table. That array of posts is then filtered to only include published posts and transformed to be the format that Next expects to be returned from getStaticPaths. As a refresher, getStaticPaths is supposed to return an object similar to { paths: [{ params: { ... } }] }.

An overview table of my blog posts that controls their published state
An overview table of my blog posts that controls their published state

In my case I'm returning post in params (which corresponds to [post] in the dynamic route), but also all the other metadata of the post. I include the other metadata to avoid having to do unnecessary api calls when rendering each page (like trying to look up the page id when only given the slug).

getStaticProps

The getStaticProps method is pretty simple too.

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const postMeta = (!params.id || !params.Name
    ? await fetchPostMetaFromSlug(params.post as string)
    : params) as Post;

  const page = await fetchPageById(postMeta.id, process.env.NOTION_TOKEN);
  return { props: { title: postMeta.Name, page: page.recordMap.block } };
};

We start with fetching metadata for the post unless it's provided in params. As mentioned above, it'll already be provided when getStaticProps is called from getStaticPaths. That's the default path when the site is being built, but getStaticProps can be called standalone in development so it's important that it have the ability to get whatever information it needs.

After retrieving (or using the already provided) post metadata the content of the page is fetched. This will directly call Notion's private API and get back a raw response that will later be passed to react-notion to actually render out the page.

That can be seen in the component implementation:

const BlogPost = ({ page, title }) => {
  return (
    <>
      <header className="notion mb-1">
        <h1 className="notion-h1">{title}</h1>
      </header>
      <NotionRenderer blockMap={page} />
    </>
  );
}; 

That relatively simple component is taking care of all the formatting that you're seeing right now. I happily don't have to fiddle with any of it. It's worth noting that I pass down the title explicitly via getStaticProps.

Using Preact

Preact is a 3kb mostly drop-in replacement for react. It's a good way to get a react-like api with a minimal payload. It's not necessarily incredibly straight forward to integrate currently and some things also don't work as well out of the box. Still, it's a good choice for boosting performance.

Jason Miller (the creator of Preact) created a preact + Next.js example repo that I used to kick start my efforts. If you're starting fresh, just clone the repo and everything will work like a normal Next app. If you're converting an existing Next app, just do the following...

Install the development dependencies

yarn add --dev @prefresh/next react-refresh

These dependencies are required to enable react fast refresh.

Install the app's dependencies

yarn add preact preact-render-to-string

preact-render-to-string is needed because preact doesn't ship with the ability to render to string (for server side rendering) out of the box.

The next part is a little weird, but necessary. Next is incredibly helpful, sometimes a bit overly so. It automatically checks to ensure react is installed. If it's not, the build will fail. That unfortunately means you're required to have react and react-dom installed... except, maybe not. Add the following lines to the dependencies section in your package.json.

"react": "github:preact-compat/react#1.0.0",
"react-dom": "github:preact-compat/react-dom#1.0.0",
"react-ssr-prepass": "npm:preact-ssr-prepass@^1.0.1"

The first two entries are an alias to a set of github repos that essentially form a bare wrapper to preact/compat. With this syntax you can avoid having to pull in the entirety of react and react-dom. The last library is one that Next requires as well, but here we're specifying an npm alias to the preact package equivalent.

Setup Next's configuration

Next supports a configuration file (next.config.js) that can be used to control how it configures webpack and to pass custom configuration to the framework itself. You'll want to create that file in the root of your project and copy over the following snippet.

const withPrefresh = require('@prefresh/next');

const config = {
  experimental: {
    modern: true,
    polyfillsOptimization: true
  },

  webpack(config, { dev, isServer }) {
    const splitChunks = config.optimization && config.optimization.splitChunks
    if (splitChunks) {
      const cacheGroups = splitChunks.cacheGroups;
      const preactModules = /[\\/]node_modules[\\/](preact|preact-render-to-string|preact-context-provider)[\\/]/;
      if (cacheGroups.framework) {
        cacheGroups.preact = Object.assign({}, cacheGroups.framework, {
          test: preactModules
        });
        cacheGroups.commons.name = 'framework';
      }
      else {
        cacheGroups.preact = {
          name: 'commons',
          chunks: 'all',
          test: preactModules
        };
      }
    }

    // inject Preact DevTools
    if (dev && !isServer) {
      const entry = config.entry;
      config.entry = () => entry().then(entries => {
        entries['main.js'] = ['preact/debug'].concat(entries['main.js'] || []);
        return entries;
      });
    }

    return config;
  }
};

module.exports = withPrefresh(config);

If you already have custom Next configuration you'll need to sort out how to merge it with this. This configuration does a few things:

  1. Adds a few experimental flags that reduce the redundant dependencies and polyfills sent down to the browser (which drops support for older browsers).
  1. Configures a chunk called framework that'll contain all the preact related javascript for long term caching. This is mostly just a performance optimization.
  1. It injects the code needed to use preact dev-tools in dev mode

That's essentially it. Everything else will work exactly the same. Keep things referring to react like you normally would.

Wrapping up

Thanks for reading! This was a pretty loose, high-level overview but if you're interested in learning more you can check out the source here.