我發現部落格系統時常會透過 contentLayer
進行內容管理,於是翻了一下官方文件發現了:
Content layer 是有助於我們管理靜態文件的工具
環境設定
在 Next.js
** 專案中安裝:**
pnpm add contentlayer next-contentlayer date-fns
在 next.config.js
中導入並使用 withContentLayer
包住我們的輸出:
// next.config.ts
const { withContentlayer } = require('next-contentlayer')
/** @type {import('next').NextConfig} */
const nextConfig = { reactStrictMode: true, swcMinify: true }
module.exports = withContentlayer(nextConfig)
如果你使用 TS,記得要改 tsconfig.json
:
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
// ^^^^^^^^^^^
"paths": {
"contentlayer/generated": ["./.contentlayer/generated"]
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".contentlayer/generated"
// ^^^^^^^^^^^^^^^^^^^^^^
]
}
為了確保每一次 build production 都保持最新的內容,必須在 .gitignore
中新增 .contentlayer
:
# .gitignore
# ...
# contentlayer
.contentlayer
程式設定
在 root 新增一個 contentlayer.config.ts
:
// contentlayer.config.ts
import { defineDocumentType, makeSource } from 'contentlayer/source-files'
export const Post = defineDocumentType(() => ({
name: 'Post',
filePathPattern: `**/*.md`,
fields: {
title: { type: 'string', required: true },
date: { type: 'date', required: true },
},
computedFields: {
url: { type: 'string', resolve: (post) => `/posts/${post._raw.flattenedPath}` },
},
}))
export default makeSource({ contentDirPath: 'posts', documentTypes: [Post] })
試試看
我們的 .md
文件大致上會長這樣:
---
title: My First Post
date: 2021-12-24
---
Ullamco et nostrud magna commodo nostrud ...
post 這邊的資料夾結構:
posts/
├── post-01.md
├── post-02.md
└── post-03.md
在 homepage 秀出 post list ,這邊有兩個部分,post card 的 layout,以及把我們的 posts 展示出來
// app/page.tsx
import Link from 'next/link'
import { compareDesc, format, parseISO } from 'date-fns'
import { allPosts, Post } from 'contentlayer/generated'
function PostCard(post: Post) {
return (
<div className="mb-8">
<h2 className="mb-1 text-xl">
<Link href={post.url} className="text-blue-700 hover:text-blue-900 dark:text-blue-400">
{post.title}
</Link>
</h2>
<time dateTime={post.date} className="mb-2 block text-xs text-gray-600">
{format(parseISO(post.date), 'LLLL d, yyyy')}
</time>
<div className="text-sm [&>*]:mb-3 [&>*:last-child]:mb-0" dangerouslySetInnerHTML={{ __html: post.body.html }} />
</div>
)
}
export default function Home() {
const posts = allPosts.sort((a, b) => compareDesc(new Date(a.date), new Date(b.date)))
return (
<div className="mx-auto max-w-xl py-8">
<h1 className="mb-8 text-center text-2xl font-black">Next.js + Contentlayer Example</h1>
{posts.map((post, idx) => (
<PostCard key={idx} {...post} />
))}
</div>
)
}
透過 [slug]
folder 為貼文建立動態路由:
// app/posts/[slug]/page.tsx
import { format, parseISO } from 'date-fns'
import { allPosts } from 'contentlayer/generated'
export const generateStaticParams = async () => allPosts.map((post) => ({ slug: post._raw.flattenedPath }))
export const generateMetadata = ({ params }: { params: { slug: string } }) => {
const post = allPosts.find((post) => post._raw.flattenedPath === params.slug)
if (!post) throw new Error(`Post not found for slug: ${params.slug}`)
return { title: post.title }
}
const PostLayout = ({ params }: { params: { slug: string } }) => {
const post = allPosts.find((post) => post._raw.flattenedPath === params.slug)
if (!post) throw new Error(`Post not found for slug: ${params.slug}`)
return (
<article className="mx-auto max-w-xl py-8">
<div className="mb-8 text-center">
<time dateTime={post.date} className="mb-1 text-xs text-gray-600">
{format(parseISO(post.date), 'LLLL d, yyyy')}
</time>
<h1 className="text-3xl font-bold">{post.title}</h1>
</div>
<div className="[&>*]:mb-3 [&>*:last-child]:mb-0" dangerouslySetInnerHTML={{ __html: post.body.html }} />
</article>
)
}
export default PostLayout