In this article, we will explain how to add markdown to the Steady Cursor website. Initially, the vision was clear - the website would house articles serving as supporting material for YouTube episodes, catering to those who prefer reading over watching. To accomplish this, markdown was chosen for its simplicity and flexibility. The website was to be built on a TailwindCSS and Next.js template. During the process, I stumbled upon a method to render markdown on Next.js, which was a good starting point but not ideal as the returned object type with Front Matter from the remark() method was any, lacking specificity.

Further research led to the discovery of zod-matter, a library that perfectly solved this issue by allowing both markdown parsing and schema typing through the combination of matter and zod.

Implementation

To set up, you'll need to install both zod and zod-matter:

npm install zod zod-matter

Create the file /src/utils/getPostsData.ts with the following content:

import fs from 'fs';
import path from 'path';
import { z } from 'zod';
import { parse } from 'zod-matter';

const postsDirectory = path.join(process.cwd(), 'src', 'posts');

export const getPostsData = () => {
    const fileNames = fs.readdirSync(postsDirectory);
    const allPostsData = fileNames.map((fileName) => getPostData(fileName));

    return allPostsData;
};

const getPostData = (fileName: string) => {
    const slug = fileName.replace(/\.md$/, '');

    const fullPath = path.join(postsDirectory, fileName);
    const fileContents = fs.readFileSync(fullPath, 'utf8');

    const { data, content } = parse(
        fileContents,
        z.object({
            title: z.string(),
            date: z.string().datetime(),
        })
    );

    return {
        ...data,
        content,
        slug,
    };
};

This script is the core magic we need. It consists of two methods: getPostsData, which retrieves all markdown files from a specified directory, and getPostData, which processes each file's content and metadata using zod-matter to ensure type safety.

Usage

You can utilize this setup anywhere, like on the homepage, by fetching all posts in the getStaticProps method:

import Link from 'next/link';
import { getPostsData } from '@/utils/getPostsData';
import type { InferGetStaticPropsType } from 'next';

export default function Home({ posts }: InferGetStaticPropsType<typeof getStaticProps>) {
    return (
        <div>
            <h1>Blog posts</h1>

            <ul>
                {posts.map((post) => (
                    <li key={post.slug}>
                        <Link href={`/blog/${post.slug}`}>
                            {post.title} ({post.date})
                        </Link>
                    </li>
                ))}
            </ul>
        </div>
    );
}

export const getStaticProps = async () => {
    const posts = getPostsData();

    return {
        props: {
            posts,
        },
    };
};

Now, you have a typed array of posts at your disposal.

Conclusion

The addition of zod-matter ensures that missing or incorrectly typed Front Matter data in a markdown file will throw an error, preventing faulty content from reaching production. This setup offers a streamlined, type-safe approach to managing content.

For a more detailed walkthrough, check out the accompanying YouTube video below.

Remember, this is just a high-level overview. For the full code snippets and a step-by-step guide, be sure to watch the detailed video tutorial. Source code can be found for this episode on GitHub.

Source code is available on GitHub

The entire episode is available on YouTube

Do not miss a thing!