2023 Website Refresh
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…