I just converted my blog to use the Next.js 'app' directory, and thought I'd show how you can make your own static site with it. Static export of the Next.js 'app' directory was added in version 13.3 https://nextjs.org/blog/next-13-3#static-export-for-app-router
See here
https://github.com/cmdcolin/nextjs-appdir-blog
If you take nothing else away from this article, let it be that
The 'app' dir, with react server components, can be used to create static websites
That react server components (RSC) can be "async function components" whereas this does not exist on the client side. It's quite interesting
The exact step-by-step here is probably unnecessary but just trying to show the raw basics
I accepted all defaults but added "YES" to the use experimental app directory
$ npx create-next-app myblog
✔ Would you like to use TypeScript with this project? … No / YES
✔ Would you like to use ESLint with this project? … No / YES
✔ Would you like to use `src/` directory with this project? … NO / Yes
✔ Would you like to use experimental `app/` directory with this project? … No / YES
✔ What import alias would you like configured? … @/*
Creating a new Next.js app in /home/cdiesh/myblog.
Using npm.
Initializing project with template: app
Installing dependencies:
- react
- react-dom
- next
- typescript
- @types/react
- @types/node
- @types/react-dom
- eslint
- eslint-config-next
Add 'output': 'export' to the next.config.js file
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
}
module.exports = nextConfig
Create a new directory like this on the command line, yes there are square brackets in the directory name
mkdir -p app/posts/\[id\]/
Then make a file page.tsx in there so it is 'app/posts/[id]/page.tsx'
Then on this page, fill in with this
import { getPostById, getAllPosts } from '@/lib/api'
// Generate the post, note that this is a "react server component"! it is
// allowed to be async
export default async function Post({
params: { id },
}: {
params: { id: string }
}) {
const { html, title, date } = await getPostById(id)
return (
<article>
<h1>{title}</h1>
<h4>{date}</h4>
<div dangerouslySetInnerHTML={{ __html: html }} />
</article>
)
}
// This function can statically allow nextjs to find all the posts that you
// have made, and statically generate them
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map(post => ({
id: post.id,
}))
}
// Set the title of the page to be the post title, note that we no longer use
// e.g. next/head in app dir, and this can be async just like the server
// component
export async function generateMetadata({
params: { id },
}: {
params: { id: string }
}) {
const { title } = await getPostById(id)
return {
title,
}
}
In the last post, I use functions "getAllPosts" and "getPostById", these are functions that I define that parse my markdown blog posts.
So let's make those functions. The code below performs markdown parsing manually using unified/remark/rehype and allows github flavored markdown. See Footnote 1 for more info. So first install these
npm install gray-matter unified remark-gfm remark-parse remark-rehype rehype-stringify rehype-autolink-headings rehype-slug @leafac/rehype-shiki shiki
And then, add this code
// lib/api.ts
import fs from 'fs'
import matter from 'gray-matter'
import path from 'path'
import { unified } from 'unified'
import remarkGfm from 'remark-gfm'
import rehypeSlug from 'rehype-slug'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import rehypeStringify from 'rehype-stringify'
import rehypePrettyCode from 'rehype-pretty-code'
const postsDirectory = path.join(process.cwd(), '_posts')
function getPostFiles() {
return fs.readdirSync(postsDirectory)
}
function getParser() {
return unified()
.use(remarkParse)
.use(remarkRehype)
.use(remarkGfm)
.use(rehypePrettyCode, {
theme: 'one-dark-pro',
})
.use(rehypeStringify)
.use(rehypeStringify)
.use(rehypeSlug)
.use(rehypeAutolinkHeadings, {
content: arg => ({
type: 'element',
tagName: 'a',
properties: {
href: `#${String(arg.properties?.id)}`,
style: 'margin-right: 10px',
},
children: [{ type: 'text', value: '#' }],
}),
})
}
// small speedup from caching this parser
const parser = getParser()
export async function getPostById(id: string) {
const realId = id.replace(/\.md$/, '')
const fullPath = path.join(postsDirectory, `${realId}.md`)
const { data, content } = matter(await fs.promises.readFile(fullPath, 'utf8'))
const html = await parser.process(content)
const date = data.date as Date
return {
...data,
title: data.title as string,
id: realId,
date: `${date.toISOString().slice(0, 10)}`,
html: html.value.toString(),
}
}
export async function getPageMarkdown(string_: string) {
const { data, content } = matter(
fs.readFileSync(path.join('_pages', string_), 'utf8'),
)
const html = await parser.process(content)
return {
...data,
html: html.value.toString(),
}
}
export async function getAllPosts() {
const posts = await Promise.all(getPostFiles().map(id => getPostById(id)))
return posts.sort((post1, post2) => (post1.date > post2.date ? -1 : 1))
}
Edit the app/page.tsx
file, and replace with the below: this is the root of
your site
// app/page.tsx
import Link from 'next/link'
import { getAllPosts } from '@/lib/api'
export default async function Page() {
const posts = await getAllPosts()
return (
<div>
<h1>My blog</h1>
<h2>All posts:</h2>
<ul>
{posts.map(post => {
const { id, date, title } = post
return (
<li key={id}>
<Link href={`/posts/${id}`}>
{date} - {title}
</Link>
</li>
)
})}
</ul>
</div>
)
}
_posts
dirExample
_posts/2023-04-08-first-post.md
---
title: First post
date: 2023-04-08
---
Hello world!
Create your repo named yourname.github.io
under yourname account
Then run
npm install gh-pages
Then add a "predeploy" and "deploy" script to package.json
{
"private": true,
"scripts": {
"dev": "next",
"build": "next build",
"start": "next dev",
"lint": "next lint",
"predeploy": "npm run build && touch out/.nojekyll",
"deploy": "gh-pages -d out -t true",
"postdeploy": "git push --follow-tags"
},
...
}
Then when you use npm run deploy
it will build, push to github pages, and push
to the the regular content to the main branch too
Source https://github.com/cmdcolin/nextjs-appdir-blog
Deployed https://cmdcolin.github.io/nextjs-appdir-blog
I manually added basePath: "/nextjs-appdir-blog",
to next.config.js
to
deploy it to that sub-URI, if you deploy without a sub-URI, you won't need this
I use the 'unified/remark/rehype' stack for parsing my blogpost markdown. My
unified/remark/rehype setup allows github flavored markdown (so e.g. links are
automatically converted to <a href>
s) and code will be highlighted by
https://github.com/shikijs/shiki which I found did a pretty good job with e.g.
typescript (I look forward to maybe a treesitter powered coloring, if you find
this let me know)
There may be some temptation to use things like MDX for your blog, and indeed, I
used MDX with my original blog setup with the pages
directory
(https://cmdcolin.github.io/posts/2021-12-26-nextjs#using-mdx-for-blogposts-in-nextjs)
but to me it is overkill and harder to understand. If it is desirable or needed
to use it for your setup, see
https://github.com/hashicorp/next-mdx-remote#react-server-components-rsc--nextjs-app-directory-support