Code for breakfast.

Blogging on Next.js: Generating static index pages

This is a sort of meta-post about how I hacked a simple blogging system on top of Next.js.

Since I was building a website that talked about the JavaScript ecosystem, I naturally wanted a blog engine that:

I've been a big fan of Next since it's release, so much so that I wanted to try using it as a publishing system.

I'd dabbled with libraries like Gatsby, but decided it wasn't for me. I found Gatsby's API to be overwhelmingly large and complex. I love the project's mission, and can see Gatsby being really useful for a big organization, but I like APIs with small footprints that mostly fit in my ever-depleting memory.

One of Next's core (and controversial) features is its PHP-style filesystem-based routing. This makes writing a new blog post as easy as creating a new file. I chose author my posts in MDX - a superset of markdown that allows you to sprinkle in React components. By adding @next/mdx to my next.config.js, Next rendered all those MDX files with it's file-based routing. Pretty sweet.

Unfortunately, there was one missing piece that I still needed - a way to collect a list of all my posts and generate an index page.

My goal was to create an index.tsx file, and leverage it's getStaticProps definition to generate a fully static index page at build-time.

Basically, I wanted something as easy as this:

import { getPages } from 'helpers/paths';

export const getStaticProps: GetStaticProps = async () => {
  return {
    props: { posts: await getPages('blog') },
  };
};

export default Index ({posts}) => {
  // render the index

Here's how I did I with a few lines of code.

0. Create a post metadata format

MDX files can specify a meta export with information about the document - think of this as Markdown's frontmatter, but in JavaScript. This is a convenient spot to store stuff like a post's title, publish date and relevant tags. Here's an example of this post's meta:

export const meta = {
  pub: true,
  title: 'Blogging on Next.js: Fun with static props',
  date: '2020-08-01T01:43:16Z',
  tags: ['typescript', 'tricks', 'meta'],
};

// the actual post content below...

In order to generate my index page, I had to collect the metadata from these every MDX file in my /blog directory. Fortunately, Next made this a breeze.

1. Reading the filesystem

Getting a list of posts was dead-simple - just use Node's fs. Since getStaticProps is always executed in a Node.js environment, you have access to any standard lib or NPM module you need.

import path from 'path';
import { promises as fs } from 'fs';

export async function getPages(
  rootPath: string,
): Promise<Post[]> {
  const postFileNames = await fs.readdir(rootPath);

Now that I was equipped with a list of file names, I had to figure out how to load and parse all these MDX files. At first I thought I had to read the files and find a runtime MDX parser, but then I wondered can I lean on @next/mdx?

2. Dynamically loading post metadata

It turns out @next/mdx patches Next's module loader to make MDX a first-class citizen! Importing an MDX file transpiles it to JS under the hood - the plugin ever overloads dynamic require calls:

const posts: Post[] = postFileNames.map((name) => {
  const blogPostModule = require(`../pages/${rootPath}/${name}`);

Again, all this happens at build time!

From here, I can just iterate over the modules and return the metadata:

const posts: Post[] = postFileNames.map((name) => {
  const blogPostModule = require(`../pages/${rootPath}/${name}`);
  const slug = path.parse(name).name;
  const urlPath = `/${rootPath}/${slug}`;

  return {
    ...blogPostModule.meta,
    path: urlPath,
    slug,
  };
});

return posts;

That's it! I'm really impressed with the level of power and flexibility Next exposes to the developer, and how I could replicate a lot of the value found in Gatsby without the excessive configuration or any layers of abstraction. I'm really excited to see the platform evolve, and to improve my index page strategy with new features like incremental static regeneration.

Stay tuned to learn how I extended this feature to statically generate an RSS feed.