In the last number of years, I'd completely come to rely on using Twitter as a way to "put things out into the world", as well as keep up to date with what's going on in the world.
The sale of Twitter had folks thinking about "owning their own platform" and I was no different. In late 2022, I decided to re-vitalize my website by building my own personal feed. Next.js 13 had just launched with the new appDir
beta, and I thought it would be a a great opportunity to test out some new tools.
Getting Started with Next.js 13
This article is going to be a multi-part series. I'm going to be building a personal blog using Next.js 13, Tailwind CSS, next-mdx-remote, Planetscale, and Prisma. I'm going to be writing about the process of building the site, as well as the tools I'm using.
- Getting Started with Next.js 13 (this post)
- Building the Blog
- Creating a Personal Feed with Planetscale and Prisma
- Adding Authentication with Next-Auth.js
The Stack
-
Next.js 13 (using
appDir
beta) -
Vercel
The Process
Setup Next.js 13
My personal website was already built using Next.js, so the first step was to upgrade to Next.js 13. I followed the upgrade guide and everything went smoothly. They allow you to move to the new appDir
beta incrementally by adding a next.config.js
file to the root of your project.
module.exports = {
experimental: {
appDir: true,
},
}
With that configured, I could start to move my pages
directory into the app
directory. I would also start converting my components to React Server Components.
I was previously using the @next/mdx
package for MDX support, but I wanted some additional flexibility to be able to work with MDX in a more dynamic way. I also wanted to be able to use traditional Markdown style front-matter. I decided to use next-mdx-remote
instead. As a result, I removed the withMDX
config from my next.config.js
file.
Since I was already using Tailwind CSS on the site, the only change I had to make to tailwind.config.js
was to add the app
directory to the content
array.
module.exports = {
content: [
'./app/**/*.{js,jsx,ts,tsx}', // added this line
'./pages/**/*.{js,jsx,ts,tsx}',
],
},
Converting Pages to React Server Components
Setting Up The New app
Directory
In previous versions of Next.js, you created a file in the pages
directory and exported a React component. With Next.js 13, you can now create a file in the app
directory and export a React Server Component.
With this new approach, routes are defined using folders rather than files, and you can continue to use the pages
directory for other things like API routes.
You can also create "route groups" using folder names with (parentheses). This is a great wya to organize your files in a meaninful way, without affecting the routing.
Here's how my pages
directory looked before:
/pages
├── _app.tsx
├── index.tsx
├── uses.mdx
├── posts
│ ├── post-name.mdx
│ └── another-post.mdx
And here's how it looks now:
/app
├── head.tsx
├── layout.tsx
├── page.tsx
├── blog
│ ├── [...slug]
│ │ ├── head.tsx
│ │ └── page.tsx
│ ├── head.tsx
│ └── page.tsx
├── (pages)
│ ├── [...slug]
│ │ ├── head.tsx
│ │ └── page.tsx
│ └── denied
│ └── page.tsx
Note the layout.tsx
, and page.tsx
files at the top of the directory. These are the bare minimum files you need to create a React Server Component route. I've also used the head.tsx
special file to define the <Head>
component for each route.
If there is no layout.tsx
file in a folder, it will bubble up to the root layout in the app
directory. If there is no page.tsx
file in a folder, it will bubble up to the root page in the app
directory.
Also note the (pages)
directory. Since this one is wrapped in parentheses, it will not be used for routing. It's just a way to organize your files.
I used dynamic catch-all segments with a [...slug]
for the blog sub-routes as well as the pages sub-routes. This allows me to create a single page.tsx
file for each route, and then use the slug
parameter to determine which page to render.
Converting Pages to React Server Components
Now that the new directory structure has been configured, it's time to start converting the pages to React Server Components.
I started with the root layout file, since this is going to define the overall base layout for the site. For brevity, I'm not going to post the entire contents of these files, but you can find the full source code on GitHub.
import '@styles/styles.css'
import Link from 'next/link'
const getYear = () => {
const now = new Date()
return now.getFullYear()
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang='en'>
<body class='mx-auto my-0 p-4 max-w-5xl font-sans text-black bg-white'>
<header class='mb-8 pb-4 border-b'>
<h1>
<Link href='/' class='text-black no-underline'>
Josiah Wiebe
</Link>
</h1>
</header>
<main>{children}</main>
<footer class='mt-12 pt-8 w-full'>
<p>© 2011—{getYear()} Josiah Wiebe. All rights reserved.</p>
</footer>
</body>
</html>
)
}
Note the {children}
prop. This is where the content for each page will be rendered.
Next, I created the page.tsx
file for the root route. This will be our index page. Just like the convention for the previous pages
directory, this file should simply export a React component.
export default async function IndexPage() {
return (
<section class='page-content grid items-center'>
<div class='flex flex-col gap-4'>
<h1 class='text-2xl font-bold'>Hello, world!</h1>
<p class='text-lg text-gray-500'>Welcome to my brand new website, built with Next.js 13.</p>
</div>
</section>
)
}
The head.tsx
file is a special file that will be used to define the <Head>
component for the route. This is where you can define the title, meta tags, and other things that will be rendered in the <head>
element of the page.
I created a custom PageHead
component that will be used to define the <Head>
component for each page. This component will accept a title
prop, and will automatically append the site name to the end of the title.
interface PageHeadProps {
params: {
title: string,
};
}
export default function PageHead({ params = { title: '' } }: PageHeadProps) {
const title = `${params.title} - Josiah Wiebe`
return (
<>
<meta charSet='utf-8' />
<title>{title}</title>
<meta httpEquiv='X-UA-Compatible' content='IE=edge' />
<meta name='viewport' content='width=device-width, initial-scale=1' />
</>
)
}
Now we can load that component in the head.tsx
file for the root route.
import PageHead from '@components/page-head'
export default function Head() {
return <PageHead params={{ title: 'Josiah Wiebe', overrideTitle: true }} />
}
Since we'll want each page to have a unique title, we'll have to create a head.tsx
special file in each route directory. We can re-use the PageHead
component, but we'll need to pass in the title for each page. This is probably my least favourite convention with the new appDir
directory structure, but creating a reusable component makes it easier.
That's all for the base layout and index page. At this point, we should have a functioning site.
In my next post, I'll walk through the method I used to setup the blog and pages routes. I'll also show you how I use next-mdx-remote
to render the blog posts and pages, combined with the power of dynamic catch-all route segments.
To keep up to date with my latest posts, you can follow me on Twitter or Mastodon, or subscribe to my RSS feed.