NextJS 13 is here and you know what that means, time to create another dynamic markdown blog. While the NextJS blog starter is an amazing resource, it does not yet feature the new app directory file-based routing. It's time to take things into our own hands, at least until the new system is made public on NextJS 13.
Overview
In this project we will be taking a brief look at a handful of the changed features included in NextJS 13, however our main focus will be on NextJS's new file routing ecosystem.
Getting Started
Let's get things started with good old create-next-app, don't forget to add the experimental tag.
npx create-next-app@latest --experimental-app
Note: this install gives you the option to choose between using Typescript or Javascript, and toggling ESLint. We will be using Typescript for this tutorial but feel free to choose Javascript if you like to live life dangerously.
or manual installation
npm install next@latest react@latest react-dom@latest eslint-config-next@latest
Here are the rest of the dependencies we will be using for the blog. You can install them now, or later on as we go.
npm install remark remark-html gray-matter date-fns
remark and remark-html
remark is a powerful and versatile library that will allow us to start working with our markdown .md
blog posts. remark-html is a plugin for remark that allows us to serialize our markdown into HTML.
gray-matter
In order to get post information (such as author, title, date, etc.) from our HTML without having them be apart of our rendered post we need a way to parse YAML front matter, this is where gray-matter comes in hand.
Example first-post.md
:
---
postTitle: First Post!
date: 11-11-2022
---
<h1>This is my first post!</h1>
using gray-matter we are able to extract:
{
data: {
postTitle: 'First Post!',
date: '11-11-2022'
},
content: '<h1>This is my first post!</h1>'
}
Say goodbye to pages
For our first step on getting the blog up and running, it's out with the old and in with the new. NextJS has generously left the pages directory to allow users to slowly integrate their routing to the new file routing system, but for the purposes of this new project starting from scratch, let's delete it.
Introducing: app
Replacing the old pages directory in NextJS 13 is the new app directory. So what are the differences?
Inside the new app directory you will see 3 special files:
- page.tsx
- head.tsx
- layout.tsx
and 2 css files (you should be familiar with these):
- global.css
- page.module.css
page.tsx
At first glance it's safe to assume that page
has replaced index
in NextJS 13. But it's also important to note that there is another major difference with this new system: page.tsx
is what NextJS will be rendering NOT routing. Routing is now handled via the file path each page.tsx
is located in.
Old page directory example:
This would be routed by NextJS as 3 different pages /about
, /blog
, and /
.
New app directory:
The app directory is a route, much like the old pages directory, this means it needs its own page.tsx
to render as an index at example.com/
. As for the other routes, the main difference is that these pages are no longer different files all under pages
, instead each route is housed within its own directory, with the directory name being the route and the page.tsx
being its index render at that route.
Tip: You can think of this new system like having multiple
pages
directories nested within each other, with each one making up its own route base.
layout.tsx
I'm sure many people have created a Layout component at some point, one that stores components (such as a Header and Footer) and renders the page inside. Well the new layout.tsx
is essentially a built in version of this. Simply open the file, create your layout, and return a ReactNode child component inside and you're done. Believe it or not it really is that simple.
Note: This is also much more powerful and efficient than a typical layout component, I will be making a separate post on this in the future.
Other Special files
You may also notice there is a head.tsx
, for now we won't be worrying about this file. For this project we will only be dealing with page.tsx
and layout.tsx
. For more information check out the NextJS docs.
Styles
NextJS 13 keeps the same CSS system of global.css
along with module CSS files.
I love pure CSS just as much as the next developer, but why not get things done quick and easy with some Tailwind?
TailwindCSS Install
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
//tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
/*gobal.css*/
@tailwind base;
@tailwind components;
@tailwind utilities;
Let's make a blog!
Now that we have a better understanding of how routing works let's begin making a blog. We will be using the current NextJS blog starter as a reference.
Home page
Create a simple home (index) page to start with. Later we will add a way to fetch our recent posts to display and allow users to navigate to them.
// app/page.tsx
export default function Home() {
return (
<div className="container mx-auto">
<main>
<div className="space-y-4">
<h1 className="text-center text-5xl">NextJS 13 Blog</h1>
<p className="text-center text-xl">
Welcome to a dynamic markdown blog using NextJS 13.
</p>
</div>
</main>
</div>
);
}
Navbar
For easy navigation create a nav header and add it to our new special file layout.tsx
.
// components/Navbar.tsx
export default function Navbar() {
return (
<div className="bg-neutral-800">
</div>
)
}
We also want some pages to link to: Home
, Blog
, and Github
. Create a NavLink item to display our NextJS links.
- Home: blog information and recent posts.
- Blog: directory of all posts.
- Github: link to the project files.
type NavLink = {
href: string;
children: React.ReactNode;
};
const NavLink = ({ href, children }: NavLink) => {
return (
<Link className="hover:text-gray-300 hover:underline" href={href}>
{children}
</Link>
);
};
Add our new link items to the Navbar and we have a nice looking header.
// components/Navbar.tsx
import Link from "next/link";
export default function Navbar() {
return (
<div className="bg-neutral-800">
<nav className="container py-2 mx-auto">
<ul className="flex space-x-6 text-lg justify-center">
<li>
<NavLink href="/">Home</NavLink>
</li>
<li>
<NavLink href="/blog">Blog</NavLink>
</li>
<li>
<NavLink href="https://github.com/garrett-huggins/next13-blog-starter">
Github
</NavLink>
</li>
</ul>
</nav>
</div>
);
}
type NavLink = {
href: string;
children: React.ReactNode;
};
const NavLink = ({ href, children }: NavLink) => {
return (
<Link className="hover:text-gray-300 hover:underline" href={href}>
{children}
</Link>
);
};
Layout
Now let's import our new stylish Navbar into our layout:
// app/layout.tsx
import "./globals.css";
import Navbar from "../components/Navbar";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
{/*
<head /> will contain the components returned by the nearest parent
head.tsx. Find out more at https://beta.nextjs.org/docs/api-reference/file-conventions/head
*/}
<head />
<body className="bg-neutral-900 text-white">
<Navbar />
<div
id="page-top-spacer"
className="h-12 bg-gradient-to-t from-transparent to-neutral-800"
></div>
{children}
<div id="page-bottom-spacer" className="h-16"></div>
</body>
</html>
);
}
Fetching
We will need a way to fetch our posts and their front matter data. Let's grab the example api functions from the NextJS blog starter.
Note: we will be using the same functions from here but their implementation will be a little different.
Lib
Create a lib
folder to store our post getter functions:
// lib/api.ts
import fs from "fs";
import { join } from "path";
import matter from "gray-matter";
const postsDirectory = join(process.cwd(), "_posts");
export function getPostSlugs() {
return fs.readdirSync(postsDirectory);
}
export function getPostBySlug(slug: string, fields: string[] = []) {
const realSlug = slug.replace(/\.md$/, "");
const fullPath = join(postsDirectory, `${realSlug}.md`);
const fileContents = fs.readFileSync(fullPath, "utf8");
const { data, content } = matter(fileContents);
type Items = {
[key: string]: string;
};
const items: Items = {};
// Ensure only the minimal needed data is exposed
fields.forEach((field) => {
if (field === "slug") {
items[field] = realSlug;
}
if (field === "content") {
items[field] = content;
}
if (typeof data[field] !== "undefined") {
items[field] = data[field];
}
});
return items;
}
export function getAllPosts(fields: string[] = []) {
const slugs = getPostSlugs();
const posts = slugs
.map((slug) => getPostBySlug(slug, fields))
// sort posts by date in descending order
.sort((post1, post2) => (post1.date > post2.date ? -1 : 1));
return posts;
}
and
// lib/markdownToHtml.ts
import { remark } from "remark";
import html from "remark-html";
export default async function markdownToHtml(markdown: string) {
const result = await remark().use(html).process(markdown);
return result.toString();
}
Post cards
Before we start grabbing our posts to display, there is one last step: stylish cards to display our post front matter as preview cards.
Create a formatter for the post dates using date-fns:
// components/DateFormatter.tsx
import { parseISO, format } from 'date-fns'
type Props = {
dateString: string
}
const DateFormatter = ({ dateString }: Props) => {
const date = parseISO(dateString)
return <time className="text-slate-400" dateTime={dateString}>{format(date, 'LLLL d, yyyy')}</time>
}
export default DateFormatter
Then create some preview cards to display the post information:
// components/PostPreview
import DateFormatter from "./DateFormatter";
import Image from "next/image";
import Link from "next/link";
type Items = {
[key: string]: string;
};
export default function PostPreview({ post }: { post: Items }) {
return (
<div className="w-full mx-auto group">
<Link href={`/posts/${post.slug}`}>
{post?.coverImage && (
<Image
alt={`cover image for ${post.title}`}
src={post.coverImage}
width={400}
height={400}
style={{ width: "100%" }}
/>
)}
<div className="mt-4 space-y-2">
<p className="font-semibold text-xl group-hover:underline">
{post.title}
</p>
<DateFormatter dateString={post.date} />
<p>{post.excerpt}</p>
</div>
</Link>
</div>
);
}
// components/PostHero.tsx
import DateFormatter from "./DateFormatter";
import Image from "next/image";
import Link from "next/link";
import { getPostBySlug } from "../lib/api";
type Items = {
[key: string]: string;
};
export default function PostHero() {
const heroPost = getPostBySlug("hero-post", [
"title",
"excerpt",
"slug",
"date",
"coverImage",
]);
return (
<Link href={`/posts/${heroPost.slug}`}>
<div className="w-full mx-auto group">
<Image
alt={`cover image for ${heroPost.title}`}
src={heroPost.coverImage}
width={400}
height={400}
style={{ width: "100%" }}
/>
<div className="grid mt-4 md:grid-cols-2 grid-cols-1">
<div className="mb-2">
<p className="font-semibold text-xl group-hover:underline">
{heroPost.title}
</p>
<DateFormatter dateString={heroPost.date} />
</div>
<p>{heroPost.excerpt}</p>
</div>
</div>
</Link>
);
}
Dynamic posts
Let's use our getter functions to start displaying some posts. In NextJS 13 page.tsx
is a React server component by default, therefore we can say goodbye to getServerSideProps
, getStaticProps
, and getInitialProps
.
// app/page.tsx
import { getAllPosts } from "../lib/api";
import PostPreview from "../components/PostPreview";
import PostHero from "../components/PostHero";
import Link from "next/link";
export default function Home() {
const posts = getAllPosts(["title", "date", "excerpt", "coverImage", "slug"]);
const recentPosts = posts.slice(0, 2);
return (
<div className="container mx-auto px-5">
<main>
<div className="space-y-4">
<h1 className="text-center text-5xl">NextJS 13 Blog</h1>
<p className="text-center text-xl">
Welcome to a dynamic markdown blog using NextJS 13.
</p>
</div>
<div className="h-12"></div>
<PostHero />
<div className="h-16"></div>
<p className="text-3xl mb-6">Recent Posts</p>
<div className="grid md:grid-cols-2 grid-cols-1 mx-auto md:gap-32 gap-8">
{recentPosts.map((post) => (
<div key={post.title}>
<PostPreview post={post} />
</div>
))}
</div>
<div className="h-16"></div>
<Link
href="/blog"
className="text-3xl hover:text-gray-300 hover:underline"
>
Read More{" -> "}
</Link>
</main>
</div>
);
}
As you can tell the main difference here from the original NextJS starter blog is that we no longer need to use NextJS data fetching api's, instead we can just grab our data and use it right out of the gate.
Post Slugs
Another change in NextJS 13 is dynamic routing. Now that folders are used for routing and page.tsx
is used for rendering, there is no more [slug].tsx
. Instead the folders themselves can be turned into the slugs.
Inside our app directory create a posts
folder, and inside it create a [slug]
folder. This folder is now our search params for /posts/[slug]
.
Now create a dynamic page.tsx
inside our [slug]
directory to render the individual post pages.
Tip: you may also want to create a
module.css
to help style your HTML retrieved from the posts. Here are the styles I use.
// app/posts/[slug]
import { getPostBySlug } from "../../../lib/api";
import markdownToHtml from "../../../lib/markdownToHtml";
import markdownStyles from "./markdown-styles.module.css";
export default async function Post({ params }: { params: { slug: string } }) {
const post = getPostBySlug(params.slug, ["title", "author", "content"]);
const content = await markdownToHtml(post.content || "");
return (
<div className="container mx-auto">
<main>
<div className="w-full h-16 text-white">
<p className="text-2xl">{post.title}</p>
<p className="text-gray-400">{post.author}</p>
<div
className={markdownStyles["markdown"]}
dangerouslySetInnerHTML={{ __html: content }}
/>
</div>
</main>
</div>
);
}
We now can access our individual posts as dynamically routed pages using search paramaters.
Blog page
With a separate Home
page used to display a hero post, a few recent posts, along with any additional information, the last step is to create a blog home page that contains all of our posts.
Create a blog
folder and page.tsx
under our app
.
// app/blog/page.tsx
import { getAllPosts } from "../../lib/api";
import PostPreview from "../../components/PostPreview";
export default function Blog() {
const posts = getAllPosts(["title", "date", "excerpt", "coverImage", "slug"]);
return (
<div className="container mx-auto px-5">
<main>
<h1 className="text-center text-3xl">All Posts</h1>
<div className="h-12"></div>
<div className="grid md:grid-cols-2 grid-cols-1 lg:gap-32 gap-8">
{posts.map((post) => (
<div>
<PostPreview post={post} />
</div>
))}
</div>
</main>
</div>
);
}
We now have our very own dynamic markdown blog, all in the new and improved NextJS 13. Help yourself to the project repo for reference, or I encourage you to clone it to mess around with.
Top comments (3)
Great post, was very helpful for my use case as the Next examples in the docs don't seem to have been updated to not use
getServerSideProps
etc. yet.Great post, we're going to see more and more Next.js blogs over the years. My boilerplate is also written for Next.js App router: zippystarter.com
Obrigada, seu post me ajudou!