How to parse MDX in Next.js using TypeScript?


Yet another strategy to store and parse MDX metadata in Next.js using TypeScript and without using any external database/CMS.
Posted On: Wednesday, 01-May-2024
How to parse MDX in Next.js using TypeScript?How to parse MDX in Next.js using TypeScript?

While creating this blog, I was stuck in a situation where I was unable to extract the metadata from the markdown (MDX) files. I needed the metadata to create a database/registry of posts which I could use to search, list and filter the posts. I did not want to use next-mdx-remote, as it was still an experimental feature while writing this blog and I didn't want to overcomplicated the project.



So, without complicating it too much I decided to rearrange the directory structure of the posts like so:


Directory Structure

I went for simple directory structure, where the metadata is stored in a separate file and the markdown file is stored in the same directory. "The directory name becomes the slug of the post". I don't have the worry about the uniqueness of the slugs because the filesystem won't allow me to create a file with the same name. Also, /app routing from NextJS works out of the box with this structure.



πŸ”΄πŸŸ‘πŸŸ’
Directory-Structure
πŸ“‚devy.in
    β”œβ”€β”€πŸ“‚ src
    |   β”œβ”€β”€πŸ“‚ app
    |   |   β”œβ”€β”€πŸ“‚ post
    |   |   |   |β”€β”€πŸ“‚ my-first-blog
    |   |   |   |   β”œβ”€β”€πŸ“˜ metadata.ts
    |   |   |   |   β”œβ”€β”€πŸ“œ page.mdx
    |   |   |   |β”€β”€πŸ“‚ another-blog
    |   |   |   |   β”œβ”€β”€πŸ“˜ metadata.ts
    |   |   |   |   β”œβ”€β”€πŸ“œ page.mdx
    |   |   |   |β”€β”€πŸ“‚ yet-another-blog
    |   |   |   |   β”œβ”€β”€πŸ“˜ metadata.ts
    |   |   |   |   β”œβ”€β”€πŸ“œ page.mdx
    |   |   |   |β”€β”€πŸ“˜ layout.tsx
    |   |   |   |β”€β”€πŸ“˜ page.tsx
    |   |   |β”€β”€πŸ“‚ types
    |   |   |   β”œβ”€β”€πŸ“˜ metadata.types.ts
    |   |β”€β”€πŸ“‚ library
    |   |   β”œβ”€β”€πŸ“‚ utils
    |   |   |   |β”€β”€πŸ“˜ crawler.ts
    |   |   |   |β”€β”€πŸ“˜ postsRegistry.ts
    |   |   β”œβ”€β”€πŸ“‚ logger
    |   |   |   |β”€β”€πŸ“˜ logger.ts
    |   |── πŸ“˜ mdx-components.tsx
    |β”€β”€πŸ—’οΈ.env
    |β”€β”€πŸ—’οΈ.gitignore
    |β”€β”€πŸ“’ next.config.js
    |β”€β”€πŸ“’ package.json
    |β”€β”€πŸ“œ README.md
    |β”€β”€πŸ“’ tsconfig.json
There is room for improvement for sure, but let's keep it simple for this tutorial

Contents of relevant files


metadata.types This file contains the type of the metadata object in addition to categories list. Static list of categories prevents from spamming the category/tags list. I can always come back and add more categories later. Also, i have extended MetaData type from next.js to add the my custom properties such as slug, categories, published etc. Moreover, I can simply loop through this list to generate SSR category pages using generateStaticParams.

πŸ”΄πŸŸ‘πŸŸ’
/src/app/types/metadata.types.ts
import { Metadata } from "next";

export const CATEGORIES = [
  "blog",
  "react",
  "typescript",
  "nextjs",
  "personal",
  "life",
];
export type Category = (typeof CATEGORIES)[number];
export interface MetaData extends Metadata {
  author: string;
  createDate: Date;
  updateDate?: Date;
  categories: Category[];
  slug: string;
  published?: boolean;
}

~my-first-blog/metadata.ts This file contains the metadata for each post.

πŸ”΄πŸŸ‘πŸŸ’
/src/app/post/my-first-blog/metadata.ts
import { MetaData } from "@/app/types/metadata.types";
import dayjs from "dayjs";

const slug = "my-first-blog";

export const metadata: MetaData = {
  author: "My Name",
  title: "My First Blog",
  slug,
  description:"My First Blog using Next JS",
  createDate: dayjs("01-May-2024", "DD-MMM-YYYY").toDate(),
  updateDate: dayjs("03-May-2024", "DD-MMM-YYYY").toDate(),
  categories: ["blog", "nextjs"],
  published: true,
};


~my-first-blog/page.mdx This file contains our blog in markdownX language.

πŸ”΄πŸŸ‘πŸŸ’
/src/app/post/my-first-blog/page.mdx
import { metadata } from './metadata';
export { metadata };

## {metadata.title}
##### {metadata.description}
***
My First Blog using Next JS πŸ₯³πŸŽˆπŸŽ‰

πŸ‘‰πŸ» Demo


How do I use the metadata.ts file?

Now that we have the basic structure of the blog, we need to somehow do the following:


πŸ”΄πŸŸ‘πŸŸ’
/src/library/utils/crawler.ts
import { MetaData } from "@/app/types/metadata.types";
import { readFile, readdir } from "fs/promises";
import path from "path";
import ts from "typescript";
import logger from "../logger/logger"; // Ignore this. Just use console.log() instead

const POST_DIRECTORY = path.join("src", "app", "post");
const METADATA_FILE = "metadata.ts";

const crawlerLogger = logger("crawler.ts");

export const readAllPosts = async () => {
  // 1. Read all the folders within the /src/app/post directory.
  const postDirectories = await readdir(POST_DIRECTORY, {
    withFileTypes: true,
  });

  const dirNodes = postDirectories
    .filter((dirent) => !dirent.name.startsWith("_")) // Ignores any _drafts folder
    .filter((dirent) => !dirent.isFile()); // Ignores any page.tsx or layout.tsx files in the directory

  // 2. Read the `metadata.ts` file from each folder.
  const readAllMetaDataAsync = dirNodes.map(async (dirent) => {
    const post = await readFile(
      path.join(dirent.path, dirent.name, METADATA_FILE),
      "utf-8"
    );

   // 3. Somehow transpile the typescript code within `metadata.ts` to javascript.
   const jsCode = ts.transpile(post, { esModuleInterop: true })

   // 4. Evaluate the transpiled javascript to a `metadata` object.
   return eval(jsCode);
  });

  const allPosts = await Promise.all(readAllMetaDataAsync);
  crawlerLogger.log(`All Posts read. [allPosts.length=${allPosts.length}]`);
  // 5. Return the objects as a array: `Metadata[]`.
  return allPosts as MetaData[];
};


In the above code I used typescript to transpile the typescript code to javascript. ts.transpile() takes in the typescript code as a string and returns the transpiled javascript code as a string. The key gotcha in this section is to pass { esModuleInterop: true } as one of the TranspileOption so that the TS compiler can understand the module imports in the TS file headers. Next, I used eval to evaluate the transpiled javascript code to a metadata object.


In the next section, we will use the metadata exported to create a simple blog list and also see how can we filter the posts based on the category.

AN
Abhilash Nayak
Last Updated on: 03-05-2024

/> Devy.in© 2024Made from scratch with ❀️ & 🍡