Skip to content

NextJS 13 Blog Starter

Posted on:November 14, 2022

Table of contents

Open Table of contents

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.

Pages directory image

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:

and 2 css files (you should be familiar with these):

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:

Page directory example image

This would be routed by NextJS as 3 different pages /about , /blog , and /.

New app directory:

App directory example image

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>
  );
}

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.

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="mb-6 text-3xl">Recent Posts</p>
        <div className="md:grid-cols-2 md:gap-32 mx-auto grid grid-cols-1 gap-8">
          {recentPosts.map(post => (
            <div key={post.title}>
              <PostPreview post={post} />
            </div>
          ))}
        </div>
        <div className="h-16"></div>
        <Link
          href="/blog"
          className="hover:text-gray-300 text-3xl 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="md:grid-cols-2 lg:gap-32 grid grid-cols-1 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.