# Create an MDX-powered Blog with Next.js

> Let's create an MDX-powered blog and portfolio starter that you can deploy right away with Next.js and Tailwind CSS

*Published: 2022-03-30*
*Canonical: https://makerkit.dev/blog/tutorials/create-a-blog-mdx-nextjs*

---

In this blog post, I want to build with you an MDX-powered blog with Next.js and Tailwind CSS: at the end of this tutorial, we will have built a free blog template that you can use to deploy your portfolio and your blog today. In addition, we will also be using Typescript and Tailwind CSS.

## Getting Started

### Bootstrapping the Next.js template with create-next-app

Let's kick-start the project with `create-next-app` - a Vercel utility to build a minimal Next.js repository. Fire up your terminal and run the following command:

```bash
npx create-next-app --ts
```

The command should prompt you for the project's name; type it out and continue. The command will then install the packages, and if it all goes well, you should see the output below:

```
Initialized a git repository.

Success! Created mk-next-blog-kit at /Users/MakerKit/mk-next-blog-kit
Inside that directory, you can run several commands:
```

It should create a folder structure similar to the one below:

{% img layout="intrinsic" src="/assets/images/posts/cra-folder-structure.png" width="294" height="352" /%}

### Installing dependencies

Run the command below to install other dependencies we need:

```
npm i --save rehype-slug rehype-highlight rehype-autolink-headings gray-matter feed @mdx-js/mdx next-sitemap date-fns
```

### Adding Tailwind CSS

Tailwind CSS is the hottest CSS utility framework at the time of writing, and it will likely be for a while.

Create a file named `postcss.config.js` in your root folder:

 ```js {% title="postcss.config.js" %}
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};
```

And now paste the following configuration to a new file named `tailwind.config.js`:

 ```js {% title="tailwind.config.js" %}
const plugin = require('tailwindcss/plugin');

module.exports = {
  content: ['./**/*.tsx'],
  darkMode: 'class',
  corePlugins: {
    container: false,
  },
  theme: {
    container: {
      center: true,
      padding: {
        DEFAULT: '1rem',
        sm: '2rem',
      },
    },
    fontFamily: {
      serif: ['Bitter', 'serif'],
      sans: [
        'SF Pro Text',
        'Inter',
        'system-ui',
        'BlinkMacSystemFont',
        'Segoe UI',
        'Roboto',
        'Ubuntu',
      ],
      monospace: [`SF Mono`, `ui-monospace`, `Monaco`, 'Monospace'],
    },
  },
  plugins: [customContainerPlugin, plugin(ellipisfyPlugin)],
};

function ellipisfyPlugin({ addUtilities }) {
  const styles = {
    '.ellipsify': {
      overflow: 'hidden',
      'text-overflow': 'ellipsis',
      'white-space': 'pre',
    },
  };

  addUtilities(styles);
}

function customContainerPlugin({ addComponents }) {
  addComponents({
    '.container': {
      '@screen lg': {
        maxWidth: '1024px',
      },
      '@screen xl': {
        maxWidth: '1166px',
      },
    },
  });
}

```

Of course, feel free to customize the configuration as you prefer.

The last step to activate Tailwind is to edit your `styles/global.css` file and prepend the following content to the existing styles:

 ```css {% title="global.css" %}
@tailwind base;
@tailwind components;
@tailwind utilities;

# rest of your file goes here...
```

Et voilá, we're all set to start using Tailwind CSS across our pages! Now on to building the entities of our blog.

### Blog Entities

To start, we create 3 empty directories in the root directory, in which we can place the three main entities of our blog: `_posts`, `_collections`, and `_authors`.

{% img layout="intrinsic" src="/assets/images/posts/blog-entities.png" width="347" height="111" /%}

Additionally, we create two more folders, `lib` in which we place logic and API, and `components,` which hosts the components of our codebase (except for pages, which we add to the `pages` folder).

#### Authors

Here we add the authors of our blog; if you're a solo writer, it can be overkill. If you have a team of writers, you can add them to the `_authors` folder as a JSON file with the following interface:

 ```tsx {% title="author.tsx" %}
type Author = {
    name: string;
    picture: string;
    url: string;
};

export default Author;
```

#### Collections

Collections are, as the name says, a collection of posts. For example, we included this blog post in the `tutorials` collection. If you pay attention to the URL of this blog post, you can notice that we structured it with the slug of the collection `tutorials` and the slug of the blog post.

Collections have the following interface:

 ```tsx {% title="collection.tsx" %}
interface WithEmoji {
    emoji?: string;
}

interface WithLogo {
    logo?: string;
}

interface Collection extends WithEmoji, WithLogo {
    name: string;
    slug: string;
    emoji: string;
}

export default Collection;
```

As you can see, we added the possibility to assign an image or an emoji to the collection.

#### Posts

This entity contains the data of our blog post, made up of the content of our MDX files and some relative content retrieved using references.

 ```tsx {% title="blog-post.ts" %}
import Author from './author';
import Collection from './collection';

type BlogPost = {
    author: Author;
    collection: Collection;
    image: string;
    description: string;
    slug: string;
    title: string;
    publishedAt: string;
    live: boolean;
    tags: string[];
    readingTime: number;
    content: string;
};

export default BlogPost;
```

#### Adding our first blog post

Now that we are aware of the shape of our entities and where we can place them, we can add our first blog post. Let's start by adding our first Author, Giancarlo!

Let's create a file named `giancarlo.json` at `_authors/giancarlo.json` with the following content:

 ```json {% title="giancarlo.json" %}
{
    "name": "Giancarlo",
    "picture": "/assets/images/authors/giancarlo.png",
    "url": "https://twitter.com/gc_psk"
}
```

Next, we can add a collection; let's name it `tutorials` just like this post.

Let's create a file named `tutorials.json` at `_collections/tutorials.json` with the following content:

 ```json {% title="tutorials.tsx" %}
{
    "name": "tutorials",
    "emoji": "🖥️"
}
```

Finally, we can create a blog post with the name `create-blog-post.mdx` at `_posts/create-blog-post.mdx`.
NB: in this case, we're writing an MDX file. Therefore the extension is `.mdx`.

You can use the content below or anything else you may think of:

 ```yaml {% title="create-blog-post.mdx" %}
---
title: 'Create an MDX-powered Blog with Next.js'
collection: '_collections/tutorials.json'
author: '_author/giancarlo.json'
publishedAt: 2022-03-30
status: "published"
image: '/assets/images/posts/create-blog-mdx-nextjs.png'
description: "Let's create an MDX-powered blog and portfolio starter that you can deploy right away with Next.js and Tailwind CSS"
---

## My first blog post

Yay!
```

And yay, we added our first blog post!

As you may have noticed, both `collection` and `author` are references to paths, which we resolve when building the blog posts.

### Why add authors and collections as files?

Good question! Eventually, you will want to use a CMS (such as Forestry,
Netlify CMS, Tina, etc.); adding this metadata as files means you can add and
update them using the CMS rather than being forced to deploy a new version of
your code.

This will make it easier to make edits, especially if the blog post will be
maintained by non-technical users.

## Blog API

We need to define the functions to read the blog posts, collections, and authors.

We create a Typescript file named `api.ts` at `lib/blog/api.ts` in which we define the utilities to create our blog.

At the top of this file, we define a couple of constants we need to use:

 ```tsx {% title="api.ts" %}
const POSTS_DIRECTORY_NAME = '_posts';
const COLLECTIONS_DIRECTORY_NAME = `_collections`;
const AUTHORS_DIRECTORY_NAME = `_authors`;
const CWD = process.cwd();

const postsDirectory = join(CWD, POSTS_DIRECTORY_NAME);
const collectionsDirectory = join(CWD, COLLECTIONS_DIRECTORY_NAME);
const authorsDirectory = join(CWD, AUTHORS_DIRECTORY_NAME);
```

Next, let's define the function that allows us to retrieve the list of JSON files at the path specified:

```tsx
function readJson(directoryName: string) {
  return readdirSync(directoryName)
        .map((slug) => {
            const path = join(CWD, directoryName, slug);

            if (existsSync(path)) {
                const json = readFileSync(path, 'utf-8');

                try {
                    const data = JSON.parse(json) as Collection;
                    const realSlug = slug.replace('.json', '');

                    return {
                        data,
                        slug,
                        realSlug
                    };
                } catch (e) {
                    console.warn(`Error while reading JSON file`, e);
                }
            }
        }
    })
    .filter(Boolean);
}
```

To read an MDX file, we need a small utility to read the `frontmatter` of the markdown file. To do so, we can use the dependency `gray-matter`:

 ```tsx {% title="utils.ts" %}
import matter from 'gray-matter';

export function readFrontMatter(fullPath: string) {
  try {
    const fileContents = readFileSync(fullPath, 'utf-8');

    return matter(fileContents);
  } catch (e) {
    console.warn(`Error while reading Front matter at ${fullPath}`, e);
  }
}
```

The function below allows us to read a blog post using its slug and return its data and contents.

NB: don't worry about the functions within we haven't defined. We have skipped their implementations for simplicity, but you can find the complete source code at the end of the article.

 ```tsx {% title="utils.ts" %}
function getPostBySlug(slug: string) {
  const postFileName = `${slug}.mdx`;
  const postPath = join(postsDirectory, postFileName);
  const file = readFrontMatter(postPath);

  if (!file) {
    return;
  }

  const content = file.content;
  const data = file.data;
  const empty = Object.keys(data).length === 0;

  if (empty) {
    return;
  }

  const readingTime = getReadingTimeInMinutes(content);

  const post: Partial<BlogPost> = {
    live: data.live,
    readingTime,
  };

  for (const field in data) {
    if (field === 'slug') {
      post[field] = slug;
      continue;
    }

    if (field === 'collection') {
      post[field] = getCollection(data[field]);
      continue;
    }

    if (field === 'author') {
      post[field] = getAuthor(data[field]);
      continue;
    }

    if (field === 'content') {
      post[field] = content;
      continue;
    }

    if (field === 'date' && data.date) {
      try {
        post[field] = new Date(data.date).toISOString();
        continue;
      } catch (e) {
        console.error(`Error processing blog post date ${data.date}`);
      }
    }

    if (data[field]) {
      Object.assign(post, {
        [field]: data[field],
      });
    }
  }

  return post as BlogPost;
}
```

Let's summarize what happens in the function above:

- we read the blog post's matter by using its slug
- we iterate the fields of the MDX file and transform them into a processed object which has the shape `BlogPost`
- the collection and the author's data are also collected using the file system and transformed into their relative objects

Time to get our hands dirty with some components.

## Add a Next.js Blog Post page

As you may know, Next.js uses file-system routing. As such, we need to define the following path:

```
[collection]
    - [...slug].tsx
```

The `collection` folder represents the collection name, while `[slug]` is the blog post's slug, which we use to find our blog post. The `slug` parameter reflects the way we named our MDX file.

Our goals are the following:
- fetch the correct post using the `slug` parameter provided
- fetch a few more posts to display belonging to the same collection
- compile the MDX content to raw HTML

Let's define a function to compile the raw MDX to HTML. To do so, we use the packages we installed in the very beginning:

 ```tsx {% title="compile-mdx.ts" %}
import rehypeHighlight from 'rehype-highlight';
import rehypeSlug from 'rehype-slug';
import rehypeAutoLinkHeadings from 'rehype-autolink-headings';

export async function compileMdx(markdown: string) {
  const { compile } = await import('@mdx-js/mdx');

  const code = await compile(markdown, {
    outputFormat: 'function-body',
    rehypePlugins: [rehypeHighlight, rehypeSlug, rehypeAutoLinkHeadings],
  });

  return String(code);
}
```

It's time to define the `[...slug].tsx` page, which is our blog post page.

### Generating Blog Posts using getStaticPaths

When creating dynamic pages with SSG, we use `getStaticPaths,` a special Next.js function that allows you to create a list of paths generated as static files.

Let's explain the flow behind generating static paths with `getStaticPaths`
for each of our blog posts:

- **Collecting the posts**: initially, we collect all the blog posts from the
file-system
- **Adding the URL parameters**: for each blog post collected, we need to
pass an object containing the dynamic URL parameters of the page. In our
case, we need to pass the collection and the post's slugs

 ```tsx {% title="[...slug].tsx" %}
export function getStaticPaths() {
  const posts = getAllPosts();

  const paths = posts.map((post) => {
    const slug = post.slug;
    const collection = post.collection.name.toLowerCase();

    return {
      params: {
        collection,
        slug,
      },
    };
  });

  return {
    paths,
    fallback: false,
  };
}
```

We build the array `paths` with items such as:

```tsx
{
    collection,
    slug,
}
```

These parameters are used to match the URL `/[collection]/[slug]`; the parameters get passed to the function `getStaticProps,` another specific Next.js function that takes the parameters collected and returns data fetched at build-time (i.e., when we build and deploy the Next.js application to Vercel).

We use the `getStaticProps` function to fetch a blog post from the
file-system and return it
to the page component, which is responsible for rendering the page's data.

### Passing data to the page component using getStaticProps

Using the data collected by the `getStaticPaths` function hook, we can
now retrieve the necessary data for rendering our blog posts. We
use the API functions we defined above in the post.

Let's summarise the flow:

- **Getting the Post**: we use the slug (retrieved from the URL parameter)
to fetch the post from the file-system using the function `getPostBySlug`
- **Getting similar posts**: retaining readers on blog is hard: it's very
valuable to get your readers' attention by displaying other blog posts they
could be interested in; we do so by getting the latest posts with the same
collection using the function `getPostsByCollection`
- **Compiling the MDX**: finally, we compile the MDX string to a format that
our component `MDXRenderer` can display
- **Passing data to the page component**: the data collected will be passed
to the page as component properties

```tsx
export async function getStaticProps({ params }: Params) {
  const { slug, collection } = params;
  const maxReadMorePosts = 6;
  const post = getPostBySlug(slug);

  if (!post) {
    return {
      notFound: true,
    };
  }

  const morePosts = getPostsByCollection(collection)
    .filter((item) => item.slug !== slug)
    .slice(0, maxReadMorePosts);

  const content = await compileMdx(post.content ?? '');

  return {
    props: {
      post,
      content,
      morePosts,
    },
  };
}
```

NB: to build the collections' and tags' pages, we do something quite similar to the above, but instead of fetching the posts, we fetch a unique set of collections and tags listing the articles assigned to them.

For simplicity, you can read the source code linked at the bottom of this article to check how we do it.

### Creating the Blog Post's Page Component

Our blog post's page looks like something similar to the below:

- **Header**: at the top of the page, we display the page's header
- **Post Head**: the head contains information and metadata about the post,
such as the Open Graph tags, Rich Results structured data, and other useful
SEO meta tags. For more information about this, check out the post about
[boosting your Next.js application SEO and get more traffic to your blog
posts](/blog/tutorials/improve-seo-nextjs)
- **Post**: the content of the blog post, such as the title and the MDX content
- **Posts List**: the list of the latest posts with the same collection

 ```tsx {% title="[...slug].tsx" %}
const PostPage = ({ post, morePosts, content }) => {
  return (
    <>
      <Header />

      <Layout>
        <PostHead post={post} />

        <Post content={content} post={post} />

        <PostsList posts={morePosts} />
      </Layout>
    </>
  );
};

export default PostPage;
```

For simplicity, we won't be building all the components listed in this
post, but you are free to copy and read the code of the Next.js MDX Blog
Starter Template, which was the inspiration for this blog post.

## Rendering MDX content as HTML

To convert and render the compiled MDX code to HTML, we need to use a runtime function provided by the package `@mdx-js/mdx`.

Below, you can find the component that renders its relative HTML node, given compiled MDX code.

 ```tsx {% title="MDXRenderer.tsx" %}
import * as runtime from 'react/jsx-runtime.js';
import { runSync } from '@mdx-js/mdx';

type MdxComponent = React.ExoticComponent<{
  components: Record<string, React.ReactNode>;
}>;

function MDXRenderer({ code }: { code: string }) {
  const { default: MdxModuleComponent } = runSync(code, runtime) as {
    default: MdxComponent;
  };

  return <MdxModuleComponent components={{}} />;
}

export default MDXRenderer;
```

Take a good look at the property `components` of the `MdxModuleComponent` component: at the moment, it's empty. However, we can pass a list of components that we want to be able to render by default in our MDX files.

For example, we can augment the default `img` tag with our own. In fact, a common practice is to swap the default `img` tag with Next's own and optimized `Image` component. Let's see how it's done:

 ```tsx {% title="MDXComponents.tsx" %}
type ImageLayout = 'fixed' | 'fill' | 'intrinsic' | 'responsive' | undefined;
type StringObject = Record<string, string>;

const NextImage: React.FC<StringObject> = (props: StringObject) => {
  const width = props.width ?? '4';
  const height = props.height ?? '3';

  return (
    <img
      width={width}
      height={height}
      layout={(props.layout as ImageLayout) ?? 'responsive'}
      className={props.class}
      src={props.src}
      alt={props.alt}
      {...props}
    />
  );
};

const MDXComponents = {
    img: NextImage,
    Image: NextImage,
}

export default MDXComponents;
```

After having defined the object `MDXComponents`, we can edit the `MDXRenderer` component to include the augmentd components listed above:

```tsx
return <MdxModuleComponent components={MDXComponents} />;
```

 At this point, all the images will be rendered using Next.js's incredible `Image` component; furthermore, we can use the component `Image` directly in our MDX files, which allows us to use all the properties available:

 ```md
 <img
    width={2012}
    height={680}
    alt={'Your Image txt'}
    src={'/assets/images/posts/image.png'}
/>
```

The Makerkit's Next.js Blog Starter template includes the following components: `Image,` `ExternalLink`, and `Video`.

## Final Result

Below is the final result of what we built, and what you will find in [Blog
Starter](https://github.com/makerkit/next-blog-kit):

{% Video src="/assets/images/posts/blog-post-demo.mp4" /%}

## Blog Starter Source Code

The final result of this starter template is a fast, responsive, dark-mode
ready, and functional blog that you can use as the foundation for yours. In
addition, we have already optimized it for Search Engines, so SEO is also taken care of for you.

[You can find the complete source code on GitHub](https://github.com/makerkit/next-blog-kit). It's free and open-source, which means you can use it, fork it, and edit it.
