Improving My Next.js MDX Blog

I revamped my personal site, adding a variety of improvements for the ideal Next.js + MDX blogging experience.

March 29, 2020 (4y ago)

I recently decided to redesign and revamp my site. I had a few main goals:

  1. Easier content management for blog posts
  2. Simplified, minimal design
  3. Dark mode support

Simplified Design

Before this redesign, I hand-rolled all of my components using styled-components. I was trying to maintain a consistent design, so I'd extract shared values out into a theme.

export const colors = {
  accent: '#ff5252',
  background: '#0a6159',
  border: '#dcdcdc',
  grey: {
      100: '#F5F7FA',
      200: '#E4E7EB',
      300: '#CBD2D9',
      400: '#9AA5B1',
      500: '#7B8794',
      600: '#616E7C',
      700: '#52606D',
      800: '#3E4C59',
      900: '#323F4B',
      1000: '#1F2933'
  },
  light: '#606060',
  text: '#101010'
};
 
export const spacing = {
  extrasmall: '0.5em',
  small: '1em'
  normal: '1.5em',
  large: '2em',
  extralarge: '2.5em',
};

Front-end tooling has rapidly progressed, and projects like styled-system and Theme UI make it easy to create components that easily adhere to your design system.

My site isn't anything crazy – mostly a few simple static pages and blog posts. While coding everything myself is a fun learning experience, there are plenty of component libraries that contain everything necessary to achieve the design I was aiming for. That's why I chose to adopt Chakra UI for my redesign.

  • It uses styled-system under the hood, allowing me to use style props
  • The theme is extendable, allowing me to change fonts and add icons easily
  • It includes a great set of accessible components out of the box
  • Works well with Next.js and supports dark mode

Here's a quick example of Chakra UI and styled-system. This is part of the source for the newsletter subscription at the bottom of this post.

components/Subscribe.js
<Box
  border="1px solid"
  borderColor="blue.200"
  bg="blue.50"
  borderRadius={4}
  padding={6}
  my={4}
>
  <Heading as="h5" size="lg" mb={2}>
    Subscribe to the newsletter
  </Heading>
  <Text>
    Get emails from me about web development, tech, and early access to new
    articles.
  </Text>
  <InputGroup size="md" mt={4}>
    <Input
      aria-label="Email for newsletter"
      placeholder="tim@apple.com"
      type="email"
    />
    <InputRightElement width="6.75rem">
      <Button fontWeight="bold" h="1.75rem" size="sm">
        Subscribe
      </Button>
    </InputRightElement>
  </InputGroup>
</Box>

Using style props, I'm able to easily style my components while pulling values directly from my design system. For example, mb (short for margin-bottom) of 2 will translate to 0.5rem or ~8px.

Update 2020: I've switched to Tailwind CSS now.

Improved Content Management

I wanted to decrease the amount of friction it took to create new articles, as well as improve maintainability over time.

Previously, I maintained a JSON file containing all my articles.

data/articles.json
export default [
  {
    date: 'February 24, 2020',
    slug: 'fetching-data-with-swr',
    title:
      'Create a Dashboard with Next.js API Routes - Fetching Data with SWR',
  },
  {
    date: 'February 18, 2020',
    slug: 'google-analytics-api-nextjs',
    title: 'Create a Dashboard with Next.js API Routes - Google Analytics API',
  },
];

Then, I iterated over this list to display all articles when viewing leerob.io/blog. This approach worked, but it meant that I had two sources of truth.

Each .mdx blog post already contained this information, as well as other metadata passed to <Post />. Every time I added a new article, I had to change two places.

export const meta = {
  date: '2019-12-26',
  description: 'Highlights and reflections on 2019 and a look forward to 2020.',
  image: '/images/2019/banner.jpg',
  slug: '2019',
  title: '2019 Year in Review',
};
 
export default ({ children }) => <Post meta={meta}>{children}</Post>;

I decided to use Markdown front matter instead. Now, each .mdx file has a top section like this:

title: '2019 Year in Review';
publishedAt: '2019-12-26';
summary: 'Highlights and reflections on 2019 and a look forward to 2020.';
image: '/images/2019/banner.jpg';

Now, I can use native Next.js functionality like getStaticProps and getStaticPaths to fetch my MDX content:

pages/blog/[slug].js
export async function getStaticPaths() {
  const posts = await getFiles('blog');
 
  return {
    paths: posts.map((p) => ({
      params: {
        slug: p.replace(/\.mdx/, ''),
      },
    })),
    fallback: false,
  };
}
 
export async function getStaticProps({ params }) {
  const post = await getFileBySlug('blog', params.slug);
 
  return { props: { ...post } };
}

Update 2021: I've moved to Contentlayer.

MDX Plugins

mdx-bundler allows you to extend remark and rehype, providing external plugins to hook into the compilation process. Some plugins I've added:

lib/mdx.js
import { join } from 'path';
import { readFileSync } from 'fs';
import { bundleMDX } from 'mdx-bundler';
import readingTime from 'reading-time';
 
import rehypeSlug from 'rehype-slug';
import rehypeCodeTitles from 'rehype-code-titles';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypePrism from 'rehype-prism-plus';
 
export async function getFileBySlug(type, slug) {
  const source = readFileSync(
    join(process.cwd(), 'data', type, `${slug}.mdx`),
    'utf8'
  );
  const { code, frontmatter } = await bundleMDX(source, {
    xdmOptions(options) {
      options.rehypePlugins = [
        ...(options?.rehypePlugins ?? []),
        rehypeSlug,
        rehypeCodeTitles,
        rehypePrism,
        [
          rehypeAutolinkHeadings,
          {
            properties: {
              className: ['anchor'],
            },
          },
        ],
      ];
      return options;
    },
  });
 
  return {
    code,
    frontMatter: {
      wordCount: source.split(/\s+/gu).length,
      readingTime: readingTime(source),
      slug: slug || null,
      ...frontmatter,
    },
  };
}

This added some nice additional features:

  • Hover over a heading and click on # to link directly to it.
  • Use language:title to add titles to code snippets.
  • Reading time of articles.

Better Syntax Highlighting

Previously, I directly imported a prism.css theme alongside react-syntax-highlighter to provide syntax highlighting. This approach did not allow me to easily change styles based on the theme. Thus, I kept the code style always dark.

Instead, I switched to rehype-prism-plus and created two prism themes for dark/light mode. rehype-prism-plus also adds line highlighting capabilities 🎉

styles/prism.js
import { css } from '@emotion/react';
import { theme } from '@chakra-ui/react';
 
const prismBaseTheme = css`
  // Base styling
`;
 
export const prismLightTheme = css`
  // Light mode
`;
 
export const prismDarkTheme = css`
  // Dark mode
`;
const { colorMode } = useColorMode();
const prismTheme = colorMode === 'light' ? prismLightTheme : prismDarkTheme;

Update 2020: I've switched to Tailwind CSS now and use the native Dark Mode support. I also recommend Shiki.

Summary

Outside of my main goals, I was able to sneak in some other fun additions:

  • Show view counts for all blog posts, dynamically pulled from Firebase
  • Faster page load times
  • Switched to Inter as my main font
  • Fewer files, and a lot of deleted code!

The best part? It's all open source 🚀

Got a project?

Let's work together