2023 Website Refresh

Shubham Gulati

It’s time to reflect upon the work that has gone into making 2023 a satisfying year of updates for this website.

Content Management

Last year, I started building my website using contentlayer to manage MDX content. It was definitely great DX to manage content with full type safety. But, as of writing this, contentlayer is not maintained. I was looking for tools that would be reliable and scalable, and then I found out the team behind Next.js released @next/mdx, which natively provides MDX support for Next.js.

I was immediately interested in using it for my next website refresh. While the documentation around @next/mdx could be better, I don’t mind getting my hands dirty and discovering the nitty-gritty details. I’m sure my design isn’t the best out there, but I am still happy with the amount of functionality I could achieve with just this one package. No contentlayer, no remark-frontmatter, no gray-matter—just some copy-pastable JavaScript code and Node.js.

Next.js has an experimental flag mdxRs which enables a Rust compiler for MDX files. The only catch is that it doesn’t support Remark or Rehype plugins. Since I didn’t need those plugins anymore, I was able to enable the Rust compiler to add the cherry on top.

I am excited to share how I leveraged @next/mdx to eliminate these dependencies. Let’s start by looking at some code. Feel free to copy this to your own projects.

contentlayer

The first—and possibly the biggest—of the dependencies that I removed.

I use a getAllFrontmatter helper to parse all MDX files and then leverage @next/mdx to read the frontmatter. I have seen some devs read MDX source and parse the frontmatter line-by-line. It works, but I prefer taking this approach.

import type { MDXModule } from "mdx/types";
import glob from "glob";

import type { Frontmatter } from "~/types/frontmatter";

const ROOT_DIR = process.cwd();

export const getAllFrontmatter = async (
  dataDir: string,
  contentDir = "",
): Promise<Frontmatter[]> => {
  const mdxFilePathPattern = `${ROOT_DIR}${dataDir}${contentDir}/**/*.mdx`;
  const mdxFilePaths = glob.sync(mdxFilePathPattern);

  // Resolve all frontmatter promises so we can perform filtering and sorting
  let allFrontmatter: Frontmatter[] = await Promise.all(
    mdxFilePaths.map(async (mdxFilePath) => {
      const modulePath = mdxFilePath
        .replace(`${ROOT_DIR}`, "")
        .replace(`${dataDir}`, "")
        .replace("/index.mdx", "");

      const { metadata } = (await import(
        `/public${modulePath}/index.mdx`
      )) as MDXModule;

      return {
        ...(metadata as Frontmatter),
        filePath: mdxFilePath.replace(`${ROOT_DIR}`, ""),
        slug: mdxFilePath
          .replace(`${ROOT_DIR}`, "")
          .replace(`${dataDir}`, "")
          .replace("/index.mdx", ""),
        slugAsParams: mdxFilePath
          .replace(`${ROOT_DIR}`, "")
          .replace(`${dataDir}`, "")
          .replace(`${contentDir}`, "")
          .replace("/index.mdx", "")
          .split("/")
          .slice(1)
          .join("/"),
      } as Frontmatter;
    }),
  );

  // Filter out items where any part of slug starts with '_'
  // This behavior matches Next.js app dir where adding _ disables the route
  allFrontmatter = allFrontmatter.filter(
    (frontmatter: Frontmatter) =>
      !frontmatter.slug.split("/").some((s) => s.startsWith("_")),
  );

  // Sort items by publishedAt in descending order
  allFrontmatter = allFrontmatter.sort(
    (a: Frontmatter, b: Frontmatter) =>
      Number(new Date(b.publishedAt ?? new Date())) -
      Number(new Date(a.publishedAt ?? new Date())),
  );

  return allFrontmatter;
};

And `allRoutes` to use all frontmatter in the app, similar to how contentlayer
provides.

```ts
import type { Frontmatter } from "~/types/frontmatter";
import { getAllFrontmatter } from "./mdx";

export type AppRoute = {
  label: string;
  pages: Frontmatter[];
}

export type AllRoutes = Record<
  | "projects"
  | "blog"
  AppRoute
>;

export const allRoutes: AllRoutes = {
  projects: {
    label: "Projects",
    pages: [
      ...(await getAllFrontmatter({ contentDir: "/projects" })).map(
        (page: Frontmatter) => {
          page.icon = "CubeIcon";
          return page;
        },
      ),
    ],
  },
  blog: {
    label: "Blog Posts",
    pages: [
      ...(await getAllFrontmatter({ contentDir: "/blog" })).map(
        (page: Frontmatter) => {
          page.icon = "FileTextIcon";
          return page;
        },
      ),
    ],
  },
};

My MDX files are not as clean as I would prefer them to be, but I am okay with the results for now.

Instead of the markdown looking like this:

---
title: 2023 Website Refresh
description: A satisfying year of updates.
---

Its time to reflect upon the work that has gone in making 2023 a satisfying year
of updates for this website.

it looks more like this…

export const metadata = {
  title: "2023 Website Refresh",
  description: "A satisfying year of updates.",
};

Its time to reflect upon the work that has gone in making 2023 a satisfying year
of updates for this website.

to be continued…