In the previous article, we looked into how to setup Tailwind with Next.js and Nx workspace.
In this article we are going to learn how to use Next.js to read files from the file system, to parse the Markdown, and to render it to HTML. In particular, we're going to see how Nx helps us generate code and organize the features into Nx libraries. Rendering Markdown files is an essential part of creating a JAMStack application. For our blog platform, we are going to write articles in Markdown, which should then be parsed and rendered properly.
Install dependencies
First of all, let's install a couple of libraries which we're gonna need as we develop this new functionality.
$ yarn add gray-matter remark remark-html
Create the markdown file
We want to have all of our article markdown files in a single _articles
directory at the root of our workspace. For now, let's keep things simple and place a single markdown demo file there: _articles/dynamic-routing.md
.
---
title: 'Dynamic Routing and Static Generation'
excerpt: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Praesent elementum facilisis leo vel fringilla est ullamcorper eget. At imperdiet dui accumsan sit amet nulla facilities morbi tempus.'
date: '2020-03-16T05:35:07.322Z'
author:
name: JJ Kasper
---
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Praesent elementum facilisis leo vel fringilla est ullamcorper eget. At imperdiet dui accumsan sit amet nulla facilities morbi tempus. Praesent elementum facilisis leo vel fringilla. Congue mauris rhoncus aenean vel. Egestas sed tempus urna et pharetra pharetra massa massa ultricies.
## Lorem Ipsum
Tristique senectus et netus et malesuada fames ac turpis. Ridiculous mus mauris vitae ultricies leo integer malesuada nunc vel. In mollis nunc sed id semper. Egestas tellus rutrum tellus pellentesque. Phasellus vestibulum lorem sed risus ultricies tristique nulla. Quis blandit turpis cursus in hac habitasse platea dictumst quisque. Eros donec ac odio tempor orci dapibus ultrices. Aliquam sem et tortor consequat id porta nibh. Adipiscing elit duis tristique sollicitudin nibh sit amet commodo nulla. Diam vulputate ut pharetra sit amet. Ut tellus elementum sagittis vitae et leo. Arcu non odio euismod lacinia at quis risus sed vulputate.
Next.js Fundamentals - Data Handling
Before we dive straight into the loading, parsing and rendering of our Markdown file, letβs first go through some of the fundamentals that we need to understand first.
There are three functions that play a major role when it comes to data fetching in Next.js:
-
getStaticProps
- (Static Generation) to fetch data at build time -
getStaticPaths
- (Static Generation) to specify dynamic routes that get prerendered at build-time. -
getServerSideProps
- (Server-side Rendering) to fetch data on each request
To get started, for our blog platform we mostly need the first two. You can read all the details on the official Next.js docs. But letβs quickly go over the main parts.
GetStaticProps
If our Next.js page has an async
export named getStaticProps
, that page is pre-rendered with the information returned by that function.
export const getStaticProps: GetStaticProps = async (context) => {
// your logic
return {
props: {}
}
});
The context object is well defined and has a couple of useful properties. The most important one in our case is the params
property. It is the one that contains the route parameters when rendering dynamic routes. All the data is passed from the getStaticPaths
function which we'll see next.
There are other properties that are being passed to the getStaticProps
function. Read all about it on the docs.
GetStaticPaths
Whenever we have a dynamic Next.js route, we need to get the path of the route to find the corresponding markdown file. If we don't implement it, we get the following error:
The getStaticPaths
needs to return a list of paths that need to be rendered to HTML at build time.
Say we have a file pages/articles/[slug].tsx
and we invoke the URL /articles/dynamic-routes
.
We have our Markdown articles in the _articles
directory. Say we have a file dynamic-routing.mdx
and nextjs-update.mdx
. To target a given article, our URL will be /articles/<filename>
. As such, the getStaticPaths
should return all these so-called "slug" entries in the following form:
[
{ "params": { "slug": "dynamic-routing" } },
{ "params": { "slug": "nextjs-update" } }
]
We're going to explore the detailed implementation in a minute.
GetServerSideProps
Use this function if you want to dynamically render pages for each request. The props returned from this function will directly get passed to the React component. Obviously using this function means you need to deploy your Next.js app on an environment that supports running a Node server. You cannot use this if you plan to deploy your site statically to some CDN.
Generate our Next.js page to render Articles
The first step to rendering our Markdown articles is to create a new Next.js page that does the rendering. If you already followed one of the previous article of this series you should already have a apps/site/pages/articles/[slug].tsx
file.
Alternatively, you can generate it now. Instead of manually creating the file, use Nx to generate it. The goal is to generate a file apps/site/pages/articles/[slug].tsx
. [slug]
in particular, because that is the dynamic part.
npx nx generate @nrwl/next:page --name=[slug] --project=site --directory=articles
If you are not the console type of person, you can use Nx Console for VSCode to generate the Next.js page.
Select @nrwl/next - page
as the generator from the command menu.
When you're ready to generate, click the "Run" button.
Let's adjust the generated CSS module file from [slug].module.css
into articles.module.css
and adjust the import on the [slug].tsx
// articles/[slug].tsx
import styles from './articles.module.css';
...
Retrieve a list of paths
As we've learned in the previous section about the Next.js Data Handling basics, we need to implement the getStaticPaths
function for our dynamic articles/[slug].tsx
route.
The user should be able to enter /article/<some-title>
where for simplicity, some-title
corresponds to the name of our file.
Note, you might want to have some
url
property in your article frontmatter to guard yourself against breaking existing links if you rename your file.
Here's the implementation:
// apps/site/pages/articles/[slug].tsx
import fs from 'fs';
import { join } from 'path';
...
interface ArticleProps extends ParsedUrlQuery {
slug: string;
}
const POSTS_PATH = join(process.cwd(), '_articles');
...
export const getStaticPaths: GetStaticPaths<ArticleProps> = async () => {
const paths = fs
.readdirSync(POSTS_PATH)
// Remove file extensions for page paths
.map((path) => path.replace(/\.mdx?$/, ''))
// Map the path into the static paths object required by Next.js
.map((slug) => ({ params: { slug } }));
return {
paths,
fallback: false,
};
};
Read and Parse the Markdown file content
Now that we have the list of paths provided by getStaticPaths
, we retrieve the actual content in the getStaticProps
function.
We need to:
- Read the content of the markdown file from the file system
- Parse the Markdown and according frontmatter
- Convert the Markdown content into HTML
- Pass the rendered HTML and frontmatter data to the React component
Generate a Nx library to handle Markdown operations
We wouldn't want to have all logic of reading, parsing and rendering markdown within our getStaticProps
function. In Nx the recommendation is to move most of the logic into your libs
. This makes your functionality more reusable and helps define a clearer API from the get go, compared when you're just placing things in a simple folder.
npx nx generate @nrwl/workspace:lib --name=markdown
We use the simple Nx workspace library which comes with just TypeScript support and does not have any framework specific setup. We could also use @nrwl/node
and generate a Node library that already comes with the Node types and more. But it is pretty quick to adjust the Nx workspace library tsconfig.lib.json
to add node
to the types
array as well as adding allowSyntheticDefaultImports
(read more on the TS docs).
// libs/markdown/src/tsconfig.lib.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
...
"allowSyntheticDefaultImports": true,
"types": ["node"]
},
...
}
Read and parse Markdown
In our new markdown
lib, let's create a new markdown.ts
file. First we create a new function getParsedFileContentBySlug
which given a slug
(e.g. dynamic-routing
) reads the _articles/dynamic-routing.mdx
file.
// libs/markdown/src/lib/markdown.ts
import fs from 'fs';
import { join } from 'path';
...
export const getParsedFileContentBySlug = (
slug: string,
postsPath: string
) => {
const postFilePath = join(postsPath, `${slug}.mdx`);
const fileContents = fs.readFileSync(postFilePath);
...
};
As you can see, we get the slug
and article MD files location postsPath
as params and simply use Node.js API to read from the file system.
Next we use gray-matter
to parse the Markdown content into the frontmatter and actual content piece.
// libs/markdown/src/lib/markdown.ts
import fs from 'fs';
import { join } from 'path';
import matter from 'gray-matter';
export const getParsedFileContentBySlug = (
slug: string,
postsPath: string
) => {
const postFilePath = join(postsPath, `${slug}.mdx`);
const fileContents = fs.readFileSync(postFilePath);
const { data, content } = matter(fileContents);
return {
frontMatter: data,
content,
};
};
Given we're using TypeScript, let's enhance our signatures with some TypeScript interfaces. For that, create a new file markdown-document.ts
:
// libs/markdown/src/lib/types.ts
export interface FrontMatter {
[prop: string]: string;
}
export interface MarkdownDocument {
frontMatter: FrontMatter;
content: string;
}
And consequently add it as return type:
// libs/markdown/src/lib/markdown.ts
...
import { MarkdownDocument } from './types';
export const getParsedFileContentBySlug = (
slug: string,
postsPath: string
): MarkdownDocument => {
...
return {
frontMatter: data,
content,
};
};
We can now call getParsedFileContentBySlug
from the getStaticProps
function in our articles/[slug].tsx
file of the Next.js app. First we need to make sure to export the functions and required types from our Nx library.
// libs/markdown/src/index.ts
export * from './lib/types';
export * from './lib/markdown';
Note: The
index.ts
of every Nx library is like a public API for the other workspace projects. Only what gets exported via this public API can be shared and used by others. The fact that you have to think about what to expose and what not, implicitly leads to a better architectural design (compared to for instance just a folder structure).
Then, in our [slug].tsx
invoke the function from the getStaticProps
. We can simply import them from @juridev/markdown
as if it was an external NPM package. This is thanks to the TypeScript paths mappings, which Nx automatically added to the tsconfig.base.json
when we generated the library.
// apps/site/pages/articles/[slug].tsx
import {
getParsedFileContentBySlug
} from '@juridev/markdown'
...
export const getStaticProps: GetStaticProps<ArticleProps> = async ({
params,
}: {
params: ArticleProps;
}) => {
// read markdown file into content and frontmatter
const articleMarkdownContent = getParsedFileContentBySlug(
params.slug,
POSTS_PATH
);
return {
props: {
slug: params.slug,
},
};
};
export const getStaticPaths: GetStaticPaths<ArticleProps> = async () => {...}
With that we have the Markdown content loaded. We now need to convert the Markdown into HTML.
Note, how we can directly call Node APIs (e.g. from within our
getParsedFileContentBySlug
) and directly invoke such functions from thegetStaticProps
andgetStaticPaths
functions. This is because both,getStaticProps
andgetStaticPaths
run at build-time, thus in a Node environment.
Convert Markdown to HTML
Again, we make use of our markdown
lib in libs/markdown
of our Nx workspace.
We accomplish the HTML rendering itself with remark
. The logic for that is private to our markdown
lib meaning we don't export it in our libs/markdown/src/index.ts
. This is simply because it is an implementation detail how and with which library we render our Markdown.
Let's create a new markdownToHtml.ts
file in the libs/markdown
lib of our workspace.
//libs/markdown/src/lib/markdownToHtml.ts
import remark from 'remark';
import html from 'remark-html';
export async function markdownToHtml(markdown) {
const result = await remark().use(html).process(markdown);
return result.toString();
}
Let's call the public API function renderMarkdown
and define it in the markdown.ts
file of our lib. We can call the markdownToHtml
function directly from there.
// libs/markdown/src/lib/markdown.ts
...
export const renderMarkdown = async (
markdownContent: string
): Promise<string> => {
return await markdownToHtml(markdownContent || '');
};
Finally, we can wrap everything up and call our renderMarkdown
from the [slug].tsx
as well. Here's the full code:
// apps/site/pages/articles/[slug].tsx
import {
getParsedFileContentBySlug,
MarkdownRenderingResult,
renderMarkdown,
} from '@juridev/markdown'
import fs from 'fs';
import { join } from 'path';
import { GetStaticPaths, GetStaticProps } from 'next';
...
export const getStaticProps: GetStaticProps<MarkdownRenderingResult> = async ({
params,
}: {
params: ArticleProps;
}) => {
// read markdown file into content and frontmatter
const articleMarkdownContent = getParsedFileContentBySlug(
params.slug,
POSTS_PATH
);
// generate HTML
const renderedHTML = await renderMarkdown(articleMarkdownContent.content);
return {
props: {
frontMatter: articleMarkdownContent.frontMatter,
content: renderedHTML
},
};
};
export const getStaticPaths: GetStaticPaths<ArticleProps> = async () => {...}
You might have noticed the MarkdownRenderingResult
type. We define it in our markdown
lib's type.ts
file as well:
// libs/markdown/src/lib/types.ts
export interface FrontMatter { ... }
export interface MarkdownRenderingResult {
frontMatter: FrontMatter;
html: string;
}
Next section rendering the content with our React component.
Render the article
We have all data prepared now and can basically just take care of the rendering. I'm not going to create a fully styled rendering of an article (I'll leave that to you ;)).
// pages/articles/[slug].tsx
...
export function Article({ frontMatter, html }) {
return (
<div className="md:container md:mx-auto">
<article>
<h1 className="text-3xl font-bold hover:text-gray-700 pb-4">
{frontMatter.title}
</h1>
<div>by {frontMatter.author.name}</div>
<hr />
<main dangerouslySetInnerHTML={{ __html: html }} />
</article>
</div>
);
}
By navigating to /articles/dynamic-routing
you should see something like the following:
Visualize our Nx Workspace
Now that we have our pyarage rendered, let's have a look at how our Nx workspace looks from a code organization perspective. Nx has a handy feature called the "Dependency Graph". To visualize it, run
npx nx dep-graph
You should see the rendering of our app site
and library markdown
.
Conclusion
We've covered quite a lot in this article.
- Next.js Data fetching basics
- How to read and parse markdown files
- How to extract our logic for the reading, parsing and rendering of our Markdown into a dedicated Nx library
- How to reference our Nx
markdown
lib from our Next.js page - How you can visualize your Nx workspace with the
dep-graph
feature
GitHub repository
All the sources for this article can be found in this GitHub repository's branch: https://github.com/juristr/blog-serieus-nextjs-nx/tree/03-render-md-nextjs
Learn more
π§ Nx Docs
π©βπ» Nx GitHub
π¬ Nrwl Community Slack
πΉ Nrwl Youtube Channel
π₯ Free Egghead course
π§ Need help with Angular, React, Monorepos, Lerna or Nx? Talk to us π
Also, if you liked this, click the β€οΈ and make sure to follow Juri and Nx on Twitter for more!
Top comments (0)