Markdown is a language that is widely used by developers to write different types of content, like posts, blogs, and documents. If you haven’t used it, chances are you’ve already seen it in action. For example, the README.md
files in GitHub and npm are written in Markdown.
Markdown provides an easy syntax for writing and structuring content compared to regular HTML or other writing tools. For example, in Markdown we can say
## Hello World! I am here to see [John Doe](https://johndoe.com)
But in HTML, the same text would be:
<h2>Hello World!</h2> <p>I am here to see <a href="https://johndoe.com">John Doe</a></p>
Writing in Markdown feels like writing plain text, with a tiny bit of extra detail to structure your content. This is why Markdown is mostly used by developers to write documentation and articles.
But, there are limitations to Markdown, one of which is writing interactive content.
Oftentimes in our articles or docs, we may want to add an interactive one-off widget to demonstrate something to the reader, but we can’t in Markdown.
This is where MDX comes in. MDX is a language that makes this possible because it allows you to use JSX in Markdown.
So, in this article, we will look at how we can use this tool to build this blog in Next.js with no Gatsby or Strapi — all from scratch. We’ll cover:
If you’d like to follow along, here is the code on my GitHub.
Before we jump into building this blog, let’s talk a little about what MDX is and what we can do with it.
It is necessary to be familiar with both Markdown and JSX before working with MDX because MDX is simply a combination of them.
MDX doesn’t have a new or special syntax because it’s just a combination of Markdown and JSX in one place. Also, it is possible to save an MDX file with the .mdx
extension.
You can use Markdown dynamically and interactively, like importing components or writing XML-like syntax in your Markdown:
import Head from 'next/head'; <Head> <title>Hello World - My Blog</title> </Head> # Hello World
In the example above, we import a component Head
from Next.js and use it to specify a title for our article. You can also notice the “Hello World” message with an #
(which is an h1
in markdown). This is Markdown and JSX working together.
The v2 release of MDX makes it possible to write expressions in MDX, just like you would in JSX. A simple example is the following:
# Hello world I think 2 + 2 is {2+2}
This can be useful in docs or articles when you want to refer to a JavaScript variable, function expression, object, array — you name it.
MDX also supports importing and exporting. For instance, the BlogTitle
component below can be used as any other JSX component:
import Head from 'next/head'; export const BlogTitle = () => <h3>Hello World</h3> <Head> <title>Hello World - My Blog</title> </Head> # Hello World <BlogTitle />
Apart from components, non-components like objects, variables, and functions can be exported and imported:
import { year } from '../utils/date.js' # Top Ten Languages To Learn In {year}
Since we can use expressions in MDX, we can just pass in a year into our header easily. Think of any data you pass as expressions in JSX; now you can do it in your Markdown too:
import { posts } from '../posts/node.js' # A List of My NodeJS Posts <ul> {posts.map(post => <li>{post.title}</li>)} </ul>
It is also possible to create a variable in MDX like we saw with a functional component (BlogTitle
), but we need to prefix it with the export
keyword:
export const languages = ['Java', 'JavaScript', 'Python'] export const year = new Date().getFullYear(); # Top Ten Languages in {year} <ul> {languages.map(language => <li>{language}</li>)} </ul>
Prefixing every declaration with export
can be weird, but the good news is you don’t have to — you can simply declare it once elsewhere and import it.
There is one exception where you may need to declare and use an object in your Markdown. That is when you use metadata:
export const meta = { title: "Your article doesn't beat this", publishedOn: "January, 6th. 2022", slug: "your-article-doesnt-beat-this" } # {meta.title}
While it’s not compulsory, it helps organize your content. You can also simply use Front Matter (which we’ll use in our blog later).
One interesting thing about MDX is that its files are compiled into JSX components. Their compilers (integrations from MDX) compile both MDX files (files in .mdx
) and Markdown files (files in .md
) into JSX components that can be used as any other components.
Say we have the example below saved as article.mdx
:
import Head from 'next/head'; export const BlogTitle = () => <h3>Hello World</h3> <Head> <title>Hello World - My Blog</title> </Head> # Hello World
We can import it as such:
import Article, { BlogTitle } from 'article.mdx'; const MyBlog = () => { <Article />; <BlogTitle />; }; export default MyBlog;
The article.mdx
file is compiled into a JSX component and exported by default, so we can import it into a .js/.jsx/.mdx
file as default. Also, recall that the BlogTitle
component was also exported from article.mdx
.
“If they can use it as any other JSX component, then they should also be able to pass props, right?” so I presume the MDX developers thought.
Well, they must have, as it is possible to pass data through props to any component, including the ones you create and the ones that compile to components by default.
We can refactor our last code block to look like the following:
import Article, { BlogTitle } from 'article.mdx'; import { year } from '../utils/date.js'; const MyBlog = () => { <Article year={year} />; <BlogTitle />; }; export default MyBlog;
Our article.mdx
now looks like the following:
import Head from 'next/head'; export const BlogTitle = () => <h3>Hello World</h3> <Head> <title>Hello World - My Blog</title> </Head> # Hello World This year is {props.year}
Note that not all JSX syntax is allowed in MDX. There are exceptions between them, and you can find them on GitHub.
There are also extensions that provide language support like syntax highlighting for MDX for different text editors and IDEs. If you’re using VS Code, simply install MDX, and you’ll be good to go.
Enough with the introductions, let’s jump into code and see MDX in action.
While creating this blog, we will do the following:
getStaticProps
and api
If you have never used Next.js before, you will still be able to follow along as long as you know React, Markdown, and JSX.
Pardon me if this is obvious to you, but I want to try to carry everyone along.
To create your Next.js app, run the command below:
npx create-next-app@latest
You’ll be prompted to name your app; you can give yours any name, but for this tutorial, I will name the project PressBlog
.
Let’s install all our dependencies here and now. To begin, run the command below:
npm install @mdx-js/loader@next @mdx-js/react@next gray-matter remark-frontmatter rehype-highlight @reach/tooltip @reach/disclosure
You must use @next
to ensure you’re using the current version of mdx-js
. At the time of this writing, the current version is 2.0.0. Now, let’s review what each of these dependencies does.
@mdx-js/loader@next
is the integration, provided by MDX for webpack bundlers, that compiles Markdown and MDX into JavaScript. We will configure it in our next.config.js
file@mdx-js/react@next
provides the context for our app which we can wrap our components with and easily style our markdown contentsgray-matter
: we will use Front Matter in this blog, and gray-matter will parse it into an object of metadataremark-frontmatter
: MDX does not provide support for Front Matter, so we need to use this package to ignore Front Matter when we view our MDX files as pages in the browser@reach/tooltip
provides a component we will use to create a tooltip in our blog article@reach/disclosure
provides a component we will use for writing a disclosure in our blog article.To configure the Next.js app, copy the following into your next.config.js
:
import remarkFrontmatter from 'remark-frontmatter'; import rehypeHighlight from 'rehype-highlight'; export default { webpack: (config, options) => { config.module.rules.push({ test: /\.mdx?$/, use: [ options.defaultLoaders.babel, { loader: '@mdx-js/loader', options: { providerImportSource: '@mdx-js/react', remarkPlugins: [remarkFrontmatter], rehypePlugins: [rehypeHighlight], }, }, ], }); return config; }, reactStrictMode: true, pageExtensions: ['js', 'jsx', 'md', 'mdx'], };
Notice that we use the ES2015 import
and export
statement because we can only import remark-frontmatter
, instead of using the require
statement. So with that, you can rename next.config.js
to next.config.mjs
.
The code above simply configures Next.js so we can use .mdx
files as pages in Next.js. The options
parameter is where we can input our plugins (remark and rehype). They are different and work separately: remark plugins parses Markdown, while rehype plugins parses HTML.
Also, in the options
parameter, we have the providerImportSource
to parse our React context provider from @mdx-js/react
, which we installed earlier.
Just at the end of the webpack
method, we passed in some other Next.js configurations. But, let’s add one more configuration for our Next.js Image
loader:
export default { //.. pageExtensions: ['js', 'jsx', 'md', 'mdx'], images: { loader: 'imgix', path: 'https://images.unsplash.com/', }, //... };
Under the pages
directory, create a new file called about.mdx
. Now, you can write any Markdown or/and JSX content, or simply copy-paste the following:
import Head from 'next/head'; <Head> <title>Hello World - PressBlog</title> </Head> # Hello World
Start your Next.js server with npm run dev
and visit your new /about
page in the browser. You should receive the “Hello World” message with a “Hello World – PressBlog” as the title.
This usually would throw a 404 error, but thanks to the configuration we added in next.config.mjs
, we can now create pages with MDX.
Now that we have everything set up, let’s start organizing our blog, shall we?
How would you organize and structure your files? We just created a page in MDX, right? So, should we create all of our pages in MDX, and then create custom components (widgets, you might say) in JavaScript? But, if we can create custom components in MDX too, then what’s stopping us from just creating everything and anything in MDX?
Well, the truth is, organizing your blog with MDX can be confusing at first. Now that you have two tools that can do almost the same thing, the question you should be asking is, which should you use and when should you use it?
To answer this, we need to first understand that the purpose of MDX is not to replace JSX in all use cases. The purpose of MDX is to help us easily produce interactive and dynamically rich content in Markdown.
So, although MDX can be used to make a header
component that only contains the name and logo of the blog with some links, for example, we shouldn’t use it because that would just be a waste of a tool.
When you have lots of content to write, you should use MDX rather than JSX. In a blog like this, we only have lots of content in our articles. That’s why, in most MDX use cases, you can hardly find an .mdx
file in the components folder — it’s because they don’t need it there.
Now that we decided to only use MDX for our articles, we must now decide how our articles will be structured. We’ve seen earlier that it is possible to create a page in MDX. Wouldn’t it be simpler to have all of our posts/articles as a page in MDX?
What this means is we can have a posts
directory under the pages
directory in our Next.js app, and we can simply write all our posts in MDX in this directory.
For example, our articles would look like this:
pages/posts/what-is-react-native.mdx
pages/posts/where-to-learn-react-native.mdx
Traditionally, and alternatively, in Next.js, you would have a posts
directory (that contains your articles in Markdown) outside of the pages
directory, and another posts
directory inside the pages
directory with only one dynamic file (route).
That is, for example, pages/posts/[slug].js and fetching the required post in [slug].js based on the slug.
This is still possible, but it kind of kills MDX’s usefulness. In this article, we will use the first approach only, but I will also show you how you can fetch and parse MDX files from a directory, which is basically how the second approach works.
At the end of this tutorial, this is how the structure of our project will look (not including unchanged directories and files that come with Next.js by default):
PressBlog | |___ next.config.mjs | |___ components | |___ Header.js | |___ MDXComponents.js | |___ MeetMe.js | |___ Meta.js | |___ PostItem.js | |___ layouts | |___ Layout.js | |___ api | |___ posts.js | |___ pages | |___ index.js | |___ about.mdx | |___ _app.js | |___ _document.js | |___ posts | |___ learn-react-navigation.mdx | |___ what-is-react-native.mdx | |___ scripts | |___ utils.js | |___ styles | |___ globals.css | |___ Header.module.css | |___ Home.module.css | |___ Markdown.module.css
In this tutorial, we will only use the Next.js styling approach; no Tailwind, no Bootstrap. But, it is possible to use styled-components or any utility-first framework.
Let’s apply all used styles (which are minimal) for this blog here and now.
First, let’s add Header.module.css
, which applies to our header only. So, copy and paste the following styles into it:
.header { padding: 10px 0; background-color: rgb(164, 233, 228); } .header > div { display: flex; justify-content: space-between; align-items: center; } .header li { list-style: none; display: inline-block; margin: 0 10px; }
Next, we’ll update globals.css
, which already exists, so you don’t have to create it, just add the following styles to it:
a { color: inherit; text-decoration: underline transparent; transition: 0.25s ease-in-out; } a:hover { text-decoration-color: currentColor; } .max-width-container { max-width: 680px; margin-left: auto; margin-right: auto; padding: 0 10px; } .max-width-container.main { padding: 50px 30px; } p { line-height: 1.6; }
Then, apply the Home.module.css
style (replace any available styles), which will only apply to the home page:
.mainContainer { padding: 15px 30px; padding-top: 45px; } .articleList { margin-top: 55px; } .desc { color: #3b3b3b; font-style: italic; font-size: 0.85rem; } .postItem { margin-bottom: 40px; } .img { max-width: 100%; height: auto; border-radius: 50%; } .button { display: block; max-width: 300px; width: 100%; cursor: pointer; border-radius: 4px; margin: 0 auto; padding: 10px; border: none; background-color: #3b3b3b; color: #fff; }
Finally, use Markdown.module.css
, which will apply only to our articles’ contents:
.postTitle { border-bottom: 2px solid rgb(164, 233, 228); padding-bottom: 13px; } .link { color: rgb(0, 110, 255); } .tooltipText { background: hsla(0, 0%, 0%, 0.75); color: white; border: none; border-radius: 4px; padding: 0.5em 1em; }
For now, let’s work on our About page. Recall that we already created an about.mdx
page; now, we can insert Lorem ipsum text into it. But before that, let’s first create a component called MeetMe.js
. This component will contain an image and a short description of the author.
By creating it as a component, we can use it on the home page as well. So, go ahead and create a components
directory in the root directory of the app. Then, create a file called MeetMe.js
and copy and paste the following into it:
import Image from 'next/image'; import styles from '../styles/Home.module.css'; const MeetMe = () => { return ( <div> <Image src='photo-1618077360395-f3068be8e001?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NHx8bWFufGVufDB8MnwwfHw%3D&auto=format&fit=crop&w=500&q=60' alt='john doe avatar' width={150} height={150} className={styles.img} /> <p className={styles.p}> Hey, I am <strong>John Doe</strong>. I love coding. Lorem ipsum dolor sit, amet consectetur adipisicing elit. Reiciendis commodi numquam incidunt blanditiis quibusdam atque natus inventore sunt autem iusto. </p> </div> ); }; export default MeetMe;
Not a lot is happening here; we are simply importing the Image
component from next/image and also applying styles to the paragraph and image using the Home.module.css
styles, which we already created.
Now that we have the MeetMe
component set up, let’s use it in about.mdx
:
import MeetMe from '../components/MeetMe.js'; <MeetMe /> #### Let's dive into more of me Lorem, ipsum dolor sit amet consectetur adipisicing elit. Ea deserunt ab maiores eligendi nemo, ipsa, pariatur blanditiis, ullam exercitationem beatae incidunt deleniti ut sit est accusantium dolorum temporibus ipsam quae. Lorem, ipsum dolor sit amet consectetur adipisicing elit. Ea deserunt ab maiores eligendi nemo, ipsa, pariatur blanditiis, ullam exercitationem beatae incidunt deleniti ut sit est accusantium dolorum temporibus ipsam quae. Lorem, ipsum dolor sit amet consectetur adipisicing elit. Ea deserunt ab maiores eligendi nemo, ipsa, pariatur blanditiis, ullam exercitationem beatae incidunt deleniti ut sit est accusantium dolorum temporibus ipsam quae.
Lastly, before we check out of this page, let’s create a Meta
component that will contain our dynamic meta tags and title. So, head up to the components
directory, create a new file called Meta.js
, and copy and paste the following:
import Head from 'next/head'; const Meta = ({ title }) => { return ( <Head> <title>{title}</title> <meta name='keywords' content='react native, blog, John Doe, tutorial, react navigation' /> </Head> ); }; export default Meta; // let's set a default title Meta.defaultProps = { title: 'PressBlog - Your one stop blog for everything React Native', };
To keep things simple, we will only use static meta keywords and a dynamic title. In a real blog, you would need to optimize for search engines. next-seo is a good tool for that.
Let’s import our new component Meta.js
into about.mdx
, and use as such
{ /* .. */ } import Meta from '../components/Meta.js'; <Meta title='About John Doe - PressBlog' />; { /* .. */ }
With that, we can save our files and test our about page.
The next thing we’ll work on is the layout of the blog. Before we proceed, we need a header component, so let’s create it. Head up to the components
directory and create a new file called Header.js
and copy and paste the following:
import Link from 'next/link'; import styles from '../styles/Header.module.css'; const Header = () => { return ( <header className={styles.header}> <div className='max-width-container'> <h2> <Link href='/'>PressBlog</Link> </h2> <ul> <li> <Link href='/about'>About</Link> </li> </ul> </div> </header> ); }; export default Header;
Here, we use the Link
component from next/link
. Also, we use the Header.module.css
styles, which we created earlier.
There is a tiny bit of change for the styles, however. Notice that we used the class name max-width-container
, which is not from Header.module.css
. It is not a typo; the class name was previously used in globals.css
, this is because we need it globally.
Next, let’s create a directory called layouts
in the root directory and create a file in it called Layout.js
:
import Header from '../components/Header'; const Layout = ({ children }) => { return ( <div> <Header /> <main className='max-width-container main'>{children}</main> </div> ); }; export default Layout;
Here, we import the Header
component we just created and pass it as the header of our blog layout. You can do the same with a footer, but we are not using a footer for this blog.
Now, to apply the Layout
component to our app, head up to pages/_app.js
and wrap the Layout
component around the Component
component:
//.. return ( <Layout> <Component {...pageProps} /> </Layout> ); //..
In this section, we will create two articles. We will use these articles later on the home page., but for now, we will just create and test them as single pages.
Let’s create a new directory called posts under the pages
directory. In this new directory, create the following MDX files.
We’ll start with learn-react-navigation.mdx
:
--- title: React Navigation Tutorial publishedOn: January, 6th. 2022 excerpt: React Navigation is a number one tool to learn in React Native. You would always use them; these are some of the best practices in using React Navigation --- import Meta from '../../components/Meta'; import { Disclosure, DisclosureButton, DisclosurePanel } from "@reach/disclosure"; <Meta title='React Navigation Tutorial - PressBlog' /> # React Navigation Tutorial React Navigation is one of the best things that happened to [**React Native**](https://reactnative.org) ## Benefits of using React Navigation 1. It is cool to use 2. It is simple to learn 3. It has an extensive community 4. And more and more <Disclosure> <DisclosureButton as='div'>React Navigation won't be here forever. Click to find why</DisclosureButton> <DisclosurePanel>Nothing lasts forever, yeah! That's all I got to say</DisclosurePanel> </Disclosure>
The Disclosure
components and their counterparts only demonstrate using components and widgets in MDX, and how useful they can be for your articles. Our next article uses a tooltip, so let’s check it out by using what-is-react-native.mdx
:
--- title: What is React Native? publishedOn: January, 7th. 2022 excerpt: Lorem, ipsum dolor sit amet consectetur adipisicing elit. Ea deserunt ab maiores eligendi nemo, ipsa, pariatur --- import Meta from '../../components/Meta.js'; import Tooltip from '@reach/tooltip'; import '@reach/tooltip/styles.css'; <Meta title='What is React Native - PressBlog' /> # What is React Native? React Native is a lovely language <Tooltip label="It's not actually a language though"> <button>Fact about React native</button> </Tooltip> ### React Native App Example: ```js const App = () => { return ( <View> <Text>Hello World Example</Text> </View> ); }; export default App; ```
You can test each of your articles in the browser to see if it works. To test, use the URL /posts/what-is-react-native
or /posts/learn-react-navigation
. Notice that the part that contains our metadata doesn’t show up in the browser, which is a good thing because we don’t want our readers to see this.
This is possible with remark-frontmatter
, which we installed and configured in next.config.mjs
. The JavaScript code we wrote is not styled, though we have configured rehype-highlight
— this is because we would need a highlight.js theme. We can also use the GitHub dark theme available on cdnjs by adding it to our app with the HTML link
tag.
We can add global and static HTML link tags in our Next.js app by updating the _document.js
file. There isn’t any yet, so let’s create one. Head up to the pages
directory and create a new file _document.js
, then paste the following:
import { Html, Head, Main, NextScript } from 'next/document'; const MyDocument = () => { return ( <Html lang='en'> <Head> <link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.4.0/styles/github-dark.min.css' /> </Head> <body> <Main /> <NextScript /> </body> </Html> ); }; export default MyDocument;
We have created a custom document that renders every page in Next.js. Now when you check the article at the URL, the code should be highlighted and styled.
Let’s add styles that are specific to our blog articles only. With the approach we will use, we will be able to style any element in MDX.
Note that when I say element, I mean when it compiles to JSX, for example, # Hello world
in MDX compiles to h1
in JSX. So, with this approach, we can style all h1
elements in our MDX.
The way this is done is by wrapping our app components with the MDXProvider
from @mdx-js/react
, and providing an object of components for mapping as many elements as needed.
So, head up to the components directory, create a new file called MDXComponents.js
, and copy and paste the following code into it:
import styles from '../styles/Markdown.module.css'; const MDXComponents = { p: (props) => <p {...props} className={styles.p} />, a: (props) => <a {...props} className={styles.link} />, h1: (props) => <h1 {...props} className={styles.postTitle} />, }; export default MDXComponents;
Here, we import the styles we created earlier for Markdown contents only. Each key of the MDXComponents
object is a component that corresponds to whatever MDX compiles to.
You can now create custom components for any JSX elements:
import { Sparky } from 'sparky-text'; // a dummy module - does not exist import Link from 'next/link'; import styles from '../styles/Markdown.module.css'; const MDXComponents = { strong: ({ children }) => <Sparky color='gold'>{children}</Sparky>, p: (props) => <p {...props} className={styles.p} />, a: (props) => <Link {...props} className={styles.link} />, h1: (props) => <h2 {...props} className={styles.postTitle} />, // for some reasons I want h1 to be mapped, styled and displayed as h2 }; // please do not use this example, this is only a demonstration
Right now, our MDXComponents.js
has no effect. So, to fix that, head up to pages/_app.js
and import MDXProvider
from @mdx-js/react
, and the MDXComponents
component we just created.
Wrap the MDXProvider
around the Layout
component and pass MDXComponents
as a component prop for MDXProvider
. In summary, your _app.js
should look like this:
import '../styles/globals.css'; import { MDXProvider } from '@mdx-js/react'; import MDXComponents from '../components/MDXComponents'; import Layout from '../layouts/Layout'; function MyApp({ Component, pageProps }) { return ( <MDXProvider components={MDXComponents}> <Layout> <Component {...pageProps} /> </Layout> </MDXProvider> ); } export default MyApp;
The home page contains all of our articles. We will have ten articles by duplicating the available articles by increments of 5
each. This will be useful for demonstrating pagination.
Let’s begin by fetching the two available articles using the Node.js file system and path module. Let’s head up to the pages/api
directory and create a new file posts.js
.
The api
directory is where we can build API routes for our app. We only need one route to fetch all posts (available in the pages/posts
directory) in MDX.
We will not make any requests to this route in Next.js’ getStaticProps
function, the reason being that the page it’s used on will not prerender in the production build. Next.js recommends that server-side code written in an api route
and meant to be used in the getStaticProp
function should be written in the getStaticProp function. We will eventually need the posts api
route for pagination.
This will all be clearer soon. For now, just know that fetching the articles will be done in both an api
route and a getStaticProps
function.
Create a scripts
directory in the root directory and create a utils.js
file in it. This file will contain a getPosts
function (and some other functions) that fetches the articles.
This is how we can fetch all posts in the getPosts
function:
.mdx
files in pages/posts
using the Node.js system module’s readdirSync
and readFileSync
methodsgray-matter
Now, let’s see the code. In scripts/utils.js
, copy and paste the code below:
import fs from 'fs'; import path from 'path'; import matter from 'gray-matter'; export const getPosts = (pageIndex) => { const dirFiles = fs.readdirSync(path.join(process.cwd(), 'pages', 'posts'), { withFileTypes: true, }); const posts = dirFiles .map((file) => { if (!file.name.endsWith('.mdx')) return; const fileContent = fs.readFileSync( path.join(process.cwd(), 'pages', 'posts', file.name), 'utf-8' ); const { data, content } = matter(fileContent); const slug = file.name.replace(/.mdx$/, ''); return { data, content, slug }; }) .filter((post) => post); return posts };
This is quite a lot, but let’s go through each line.
The first thing we do is import our modules, which are fs
(from Node.js), path
(from Node.js), and matter
(from gray-matter
).
The readdirSync
gets all files in the pages/posts
directory with their file extension and is assigned to the dirFiles
variable.
Then we loop through the dirFiles
to filter out all files that aren’t MDX. While in the loop, we can read each file’s content using the readFileSync
method, and pass the content as a parameter for the matter
function.
The matter
function returns an object in which we only need data
and content
. data
is the metadata that we used Front Matter to write in each of our articles, and content
is the remaining content of the articles.
Finally, in the loop, we generate a slug from the filename (without the extension) and return an object of data
, content
, and slug
from our loop function.
You want to make sure you’re not getting all of your posts asynchronously, which is the reason for the readdirSync
and readFileSync
because they are both synchronous.
Now, let’s head back to api/posts.js
and create a function to handle the request to this route. Copy and paste this in api/posts.js
:
import { getPosts } from '../../scripts/utils'; export default function handler(req, res) { const posts = getPosts(2); // argument will change res.status(200).json(posts); }
The handler
function is just like the one in Express.js — the req
and res
parameters are used for viewing request parameters and handling responses, respectively. Save this file, we will need it later.
For now, we can open up pages/index.js
and clear out all tags (in the return
statement) and import statements (they come by default for every Next.js app). Create a new function, getStaticProps
, outside the Home
function, and copy and paste the code below into it:
export const getStaticProps = () => { const posts = getPosts(1); // the argument has no effect yet return { props: { posts, }, }; };
If you’ve worked with Next.js, this is very straightforward. But if you haven’t, it is a function used for data fetching in Next.js. Next.js uses the props returned from the exported getStaticProps
function to prerender our index.js
page at build time. This process is called static site generation.
You can perform lots of filtering and sorting here. For instance, if you want to get the last ten published articles — not just any ten — you can sort it out with the publishedOn
metadata returned from each post.
However, note that you must export the getStaticProps
function, or else it would just behave like any other function. In this function, you can fetch data from a CMS database because, technically, it runs in the server, not in the client.
Next.js gives us the comfort of using the props returned from the getStaticProps
in our Home
function. What this means is our Home
function on this page (pages/index.js
) will have posts as props passed into it:
const Home = ({ posts }) => { return ( <> <Meta /> <MeetMe /> <Link href='/about'>More about me</Link> <div className={styles.articleList}> <p className={styles.desc}>Newly Published</p> {posts.map((post) => ( <p key={post.slug}>{post.data.title}</p> ))} </div> </> ); }; export default Home;
We are using the Meta
component to add meta tags to our page. The MeetMe
component displays a short description of the author, in which there is a link to a full description on the About page.
Lastly, we loop through the posts
array coming as a prop from our Home
function. You should import the components and functions used in the Home
function including styles
from Home.module.css
.
As for now, we are only displaying each article’s title in a paragraph, which is far from what we want. Let’s go ahead and create a new component called PostItem
.
To do this, create a file in the components
directory called PostItem.js
, and copy and paste in the code below:
import Link from 'next/link'; import styles from '../styles/Home.module.css'; const PostItem = ({ post }) => { return ( <div className={styles.postItem}> <h3> <Link href={`/posts/${post.slug}`}>{post.data.title}</Link> </h3> <p>{post.data.excerpt}</p> <Link href={`/posts/${post.slug}`}>Read more</Link> </div> ); }; export default PostItem;
Now, head back to pages/index.js
and import the PostItem
component we just created. Then, replace the paragraph that displays the title of each post with <PostItem key={post.slug} post={post} />
:
//.. <div className={styles.articleList}> <p className={styles.desc}>Newly Published</p> {posts.map((post) => ( <PostItem key={post.slug} post={post} /> ))} </div> //..
Finally, save the files and test the home page in your browser. This is the same principle we will apply to the blog page.
Loading all articles at once is really not a good practice for large datasets. Having a large amount of data on a page can affect the performance of your app, so it’d be best if we load them in chunks. For example if we had ten articles, we could load five per page.
Let’s demonstrate this in our blog. Create ten articles by creating a function in scripts/utils.js
to duplicate our existing articles. Paste the code below before or after the getPosts
function.
const createMultiplePosts = (posts) => { const multiplePosts = []; posts.forEach((post) => { for (let i = 0; i < 5; i++) { multiplePosts.push(post); } }); return multiplePosts; };
We loop through any available article and produce five of each article.
The function we require to handle the pagination will also go here, in utils.js
, so paste the code below, before or after the createMultiplePosts
function.
const filterPostsByPageIndex = (posts, pageIndex) => { const postPerPage = 5; // get the total posts from page 1 to current page const totalPagePosts = +pageIndex * postPerPage; // get the total posts from page 1 to previous page const prevPagePosts = totalPagePosts - postPerPage; return posts.filter( (post, index) => index < totalPagePosts && index >= prevPagePosts ); };
This function will help us incrementally add five articles to the page whenever a request is made to it.
Now, let’s update the getPosts
function to return a duplicated and filtered post. Recall that we already have a pageIndex
parameter passed into the function, so we can replace the return
statement with this:
return filterPostsByPageIndex(createMultiplePosts(posts), pageIndex);
We also have to update the posts
API route to request a page index as a query whenever a request is made to it. Get the page query and add this to the beginning of the api/posts.js
handler function:
const { page } = req.query;
So you would have:
export default function handler(req, res) { const { page } = req.query; //...
Lastly, change the getPosts
argument from 2 to page,
i.e.:
//.. const posts = getPosts(page); //..
Here is how the above works:
getStaticProps
function and prerenderWith that said, there are going to be a ton of changes to the Home
function in pages/index.js
. Paste the code below to replace the Home
function:
const Home = ({ posts }) => { const [filteredPosts, setFilteredPosts] = useState(posts); const [currentPageIndex, setCurrentPageIndex] = useState(1); const loadMorePosts = async () => { const res = await fetch(`/api/posts?page=${currentPageIndex + 1}`); // absolute url is supported here const posts = await res.json(); setFilteredPosts((_posts) => [..._posts, ...posts]); setCurrentPageIndex((_pageIndex) => _pageIndex + 1); }; return ( <> <Meta title='PressBlog - Your one stop blog for anything React Native' /> <MeetMe /> <Link href='/about'>More about me</Link> <div className={styles.articleList}> <p className={styles.desc}>Newly Published</p> {filteredPosts.map((post, index) => ( <PostItem key={index} post={post} /> ))} <button onClick={loadMorePosts} className={styles.button}> Load more </button> </div> </> ); }; export default Home;
When the loadMorePosts
function is clicked, it will make a get request to our API posts
route and fetch the next five articles. So in summary, this is the pages/index.js
page:
import MeetMe from '../components/MeetMe.js'; import Link from 'next/link'; import PostItem from '../components/PostItem'; import styles from '../styles/Home.module.css'; import Meta from '../components/Meta'; import { useState } from 'react'; const index = ({ posts }) => { const [filteredPosts, setFilteredPosts] = useState(posts); const [currentPageIndex, setCurrentPageIndex] = useState(1); const loadMorePosts = async () => { const res = await fetch(`/api/posts?page=${currentPageIndex + 1}`); // absolute url is supported here const posts = await res.json(); setFilteredPosts((_posts) => [..._posts, ...posts]); setCurrentPageIndex((_pageIndex) => _pageIndex + 1); }; return ( <> <Meta title='PressBlog - Your one stop blog for anything React Native' /> <MeetMe /> <Link href='/about'>More about me</Link> <div className={styles.articleList}> <p className={styles.desc}>Newly Published</p> {filteredPosts.map((post, index) => ( <PostItem key={index} post={post} /> ))} <button onClick={loadMorePosts} className={styles.button}> Load more </button> </div> </> ); }; export default index; export const getStaticProps = () => { const posts = getPosts(1); return { props: { posts, }, }; }
Now, save the file and test the new blog page (visit /
in your browser). With that, we’ve successfully built a simple blog with Next.js and MDX.
You might be wondering why we used the getPosts
function in the loadMorePosts
function, rather than creating an API route. Well, this is because the getPosts
function is server-side code, and it will lead to an error if used on the client side. And, of course, you can’t use any internal API route in getStaticProps
in production.
As a result, this is the only simple approach to achieving pagination with static site generation.
Creating an interactive blog or documentation has never been this easy with Markdown. If you followed along, you are one step away from having a blog of your own. You can decide whether to add more features before deploying.
If you hope to build documentation, this tutorial can help you as well, or you can simply use Nextra. Nextra uses Next.js and MDX to create a website for your documentation, and of course, you get to write all your content in MDX.
To round it all up, in this article we learned about an interesting tool called MDX, what we can do with it, like writing expressions and passing props, and creating a blog with MDX and Next.js.
In building this blog, we saw how we can configure MDX to work in Next.js, how we should structure our apps, and how we can fetch and parse MDX files into an object of metadata (from Front Matter) and content.
So, that’s it. Thanks for reading.
Debugging Next applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Next.js app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your Next.js apps — start monitoring for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
One Reply to "Create a Next.js and MDX blog"
Thank you so much for such a complete article!