Skip to content

How to Build a Blog with Next.js, Tailwind CSS and MDX

ezgif-2-7a6cb5acba

🔗Intro

Today, we will build a blog website with Next.js, Tailwind, and MDX without using a CMS at all!

🔗What is MDX?

MDX is a combination of Markdown and JavaScript. MDX allows you to use JSX in your markdown content. You can import components, such as interactive charts or alerts, and embed them within your content. This makes writing long-form content with components a blast.

🔗Why will we use MDX?

We will use MDX because it’s so powerful, and you can write more interactive articles with it! Also, you will not need to pay any money for a CMS!

🔗Let’s Start

Here is the project that we will build using Next.js, Tailwind CSS and MDX Blog.

First, let’s create a new Next.js project:

npx create-next-app next-blog

After it’s done, go to the project directory:

cd next-blog

Then, run the dev server:

npm run dev

image

🔗Install Tailwind CSS

Installing Tailwind CSS with Next.js is simple.

First, we need to add some dev dependencies.

npm install -D tailwindcss postcss autoprefixer

Then, Initialize Tailwind CSS.

npx tailwindcss init -p

You will find that there are two new files that have been added to the project

  • tailwind.config.js
  • postcss.config.js

Now, we need to tell Tailwind CSS where to find the classes. Go to tailwind.config.js, and add these lines under content.

content: [
    "./pages/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
],

Go to the styles/globals.css and add these three lines:

@tailwind base;
@tailwind components;
@tailwind utilities;

And restart the dev server. You will find that the design has been changed like that:

image

That means it has been installed successfully 🎉

🔗Build the Layout

We will create a very simple layout. Just add the main content in between a header and a footer.

Let's create a new folder called components, and add three new components inside it.

  • Layout.js
  • Header.js
  • Footer.js

I'll leave the Header.js and Footer.js to you. Let's start from Layout.js:

import Header from "./Header";
import Footer from "./Footer";

function Layout({ children }) {
  return (
    <div className="container mx-auto max-w-5xl flex flex-col min-h-screen px-4">
      <Header />
      <main className="flex-1">{children}</main>
      <Footer />
    </div>
  );
}

export default Layout;

Important Tip: For some accessibility each page should have one <header></header>, one <main></main>, and one <footer></footer>.

Next Step, we need to wrap the whole application with our Layout.js but how?

This is possible in Next.js since the pages/_app.js is a wrapper for the whole app.

function MyApp({ Component, pageProps }) {
  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  );
}

export default MyApp;

Finally let's go to pages/index.js and delete everything with this code.

export default function Home() {
  return (
    <div>
      <h1>Hello world</h1>
    </div>
  );
}

image

🔗getPosts function

We have the layout now! Let's work on the functionality.

Create a new folder in the root directory and call it posts. Then, add some articles.

image

Then, create another folder called helpers where we will add most used functions.

Create a new file helpers/getPosts.js. After it, we will need to install a package called gray-matter. It's necessary to parse the mdx file content.

npm i gray-matter

And we will use this package to separate between the frontmatters, and the actual content of each post.

We need only three important items in each post: title, date and description. For example:

image

And this is how gray-matter works:

import matter from "gray-matter";

const { data, content } = matter(fileContent)

where data is an object with the frontmatters data and content is the actual post.

Second, we need to get a list of all the files inside the posts folder.

import fs from "fs";
import path from "path";

const files = fs.readdirSync(path.join("posts"));

files should return an array with the file names.

[
  "hello-world.mdx",
  "markdown-guide.mdx"
]

Then, we need to loop through to get each one's data one by one.

const allPostsData = files.map((fileName) => {
    const slug = fileName.replace(".mdx", "");
    const fileContents = fs.readFileSync(
      path.join(`posts/${slug}.mdx`),
      "utf8"
    );
    const { data } = matter(fileContents);
    return {
      slug,
      data,
    };
  });

And return allPostsData from the getPosts function.

🔗getPost function

It's a simpler version from getPosts function, we will pass the slug, and it should return the post data and content.

const getPost = (slug) => {
  const fileContents = fs.readFileSync(path.join(`posts/${slug}.mdx`), "utf8");
  const { data, content } = matter(fileContents);
  return {
    data,
    content,
  };
};

🔗Display the posts menu on the home page

First, we need to create getStaticProps function on the home page.

export const getStaticProps = () => {
  const posts = getPosts();

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

Then, let's get the posts in the page component.

export default function Home({ posts }) {
  return (
    <div>
      <h1 className="mt-24 mb-12 font-bold text-3xl">Latest Posts</h1>
      {posts.map((post) => (
        <PostCard
          key={post.slug}
          title={post.data.title}
          date={post.data.date}
          description={post.data.description}
          slug={post.slug}
        />
      ))}
    </div>
  );
}

image

🔗The post page

This is the final step in the project when we will display the posts. In Next.js, to add a page with parameter in its path, you should name it between brackets. In our example, we will add [slug].js in the pages directory.

We need to handle things in this page:

🔗getStaticPaths:

We need this function in Next.js to tell it how many pages should be build in the build time from this page.

export const getStaticPaths = async () => {
  const posts = await getPosts();
  const paths = posts.map((post) => ({ params: { slug: post.slug } }));
  return {
    paths,
    fallback: false,
  };
};
🔗Install next-mdx-remote

It allows us to compile the MDX to HTML.

npm i next-mdx--remote
🔗getStaticProps:
import { serialize } from "next-mdx-remote/serialize";

export const getStaticProps = async ({ params }) => {
  const post = await getPost(params.slug);
  const mdxSource = await serialize(post.content);
  return {
    props: {
      data: post.data,
      content: mdxSource,
    },
  };
};
🔗use MDXRemote to display the post:
import { MDXRemote } from "next-mdx-remote";

function Post({ data, content }) {
  return (
    <div>
      <h1 className="font-bold text-7xl mt-24 mb-12">{data.title}</h1>
      <time className="text-gray-500 italic">{data.date}</time>
      <p className="mt-12">
        <MDXRemote {...content} />
      </p>
    </div>
  );
}

🔗Style the post content

After applying all of the above, you will find the post page like this.

image

So we have to style it, install @tailwindcss/typography plugin.

npm i @tailwindcss/typography

Then, add it to plugins in tailwind.config.js.

plugins: [
  require("@tailwindcss/typography")
]

Finally, add the prose class to the MDXRemote container.

Final result:

ezgif-2-7a6cb5acba

Here, you can find the code for the complete project: Next.js, Tailwind CSS and MDX Blog


This Dot Labs is a development consultancy focused on providing staff augmentation, architectural guidance, and consulting to companies.

We help implement and teach modern web best practices with technologies such as React, Angular, Vue, Web Components, GraphQL, Node, and more.

You might also like

Javascript

Getting Started with RxJS

Javascript

Testing Web Components with Cypress and TypeScript

Javascript

Web Components Integration using LitElement and TypeScript

Javascript

Navigation Lifecycle using Vaadin Router, LitElement and TypeScript