Improving My Next.js MDX Blog

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


title: "Improving My Next.js MDX Blog" publishedAt: "2020-03-29" summary: "I revamped my personal site, adding a variety of improvements for the ideal Next.js + MDX blogging experience."

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.

<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.

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:

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:

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 🎉

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 🚀