Contentlayer 블로그 만들기

2023 7분

Next.js와 Contentlayer를 사용해서 블로그 템플릿 구현하기

NextjsTypeScriptContentlayer
목차

Next.js와 ContentLayer로 블로그로 만들었던 과정을 Blog template을 만들면서, 정리해보려고 한다.

기술 스택

아래 기술들을 베이스로 사용해서 제작했다.

구현 과정

1. 프로젝트 생성

프로젝트 생성
npx create-next-app@latest
세부 설정
What is your project named? mdx-blog
Would you like to use TypeScript? No / **Yes**
Would you like to use ESLint? No / **Yes**
Would you like to use Tailwind CSS? No / **Yes**
Would you like to use `src/` directory? **No** / Yes
Would you like to use App Router? (recommended) No / **Yes**
Would you like to customize the default import alias? **No** / Yes

2. 라이브러리 추가

라이브러리
npm install contentlayer next-contentlayer date-fns

3. ContentLayer 설정하기

  1. next.config.js
next.config.js
 
**import { withContentlayer } from "next-contentlayer";**
/** @type {import('next').NextConfig} */
const nextConfig = {
  **reactStrictMode: true,
  swcMinify: true,**
};
 
**module.exports = withContentlayer(nextConfig);**
  1. tsconfig.json
tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "baseUrl": ".",
    "paths": {
      "@/*": ["./*"],
      "contentlayer/generated": ["./.contentlayer/generated"]
    }
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts",
    ".contentlayer/generated"
  ],
  "exclude": ["node_modules"]
}

추가로, 로컬에서 빌드한 결과물은 GitHub에 올라가지 않도록, .gitignore 파일에 아래 내용을 추가한다

.gitignore
# contentlayer
.contentlayer

이제 루트(최상단) 디렉토리에 contentlayer.config.ts 파일을 만들어야 한다.

직접 만들어도 되고, 터미널에서 아래 명령어로 생성할 수도 있다.

touch contentlayer.config.ts

contentlayer.config.ts 파일의 구성은 아래와 같다

contentlayer.config.ts
import { defineDocumentType, makeSource } from 'contentlayer/source-files';
 
export const Post = defineDocumentType(() => ({
  name: 'Post',
  filePathPattern: `**/*.mdx`,
  contentType: 'mdx',
  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] });

4. 게시글 생성

이제 루트 디렉토리에 posts 폴더를 생성하고, 게시글을 작성해준다.

posts 폴더인 이유는 contentDirPath: "posts" 로 설정했기 때문이다.

---
title: My First Post
date: 2023-08-10
---
 
This is My First Post!! :)

게시글을 날짜순으로 보여주기 위해, 서로 다른 날짜로 설정한 게시글들을 생성해준다.

---
title: My First Post
date: 2023-08-08
---
 
## Test 1
 
This is My First Post 😀 Ha Ha Ha
---
title: My Second Post
date: 2023-08-09
---
 
## Test 2
 
This is My Second Post 😀 Ha Ha Ha
---
title: My Third Post
date: 2023-08-10
---
 
## Test 3
 
This is My Third Post 😀 Ha Ha Ha

5. 메인 페이지 구현

게시글까지 작성했으면, 게시글을 보여주기 위해 홈페이지(app 디렉토리의 page.tsx) 를 수정해야 한다.

app/page.tsx
import { allPosts } from '@/.contentlayer/generated';
import { compareDesc } from 'date-fns';
 
export default function Home() {
  const posts = allPosts.sort((a, b) =>
    compareDesc(new Date(a.date), new Date(b.date))
  );
 
  return (
    <div>
      {posts.map((post) => (
        <h2 key={post._id}>{post.title}</h2>
      ))}
    </div>
  );
}

작성한 게시글 제목들의 목록이 보일 것이다.

6. Card 컴포넌트 구현

이제 제목만 보여주는 대신 각 게시글들을 보여줄 카드 컴포넌트를 만들어서 좀 더 꾸며보도록 하자.

app 디렉토리에 components 폴더를 생성하고, PostCard.tsx 파일을 생성한다.

components/PostCard.tsx
import { Post } from '@/.contentlayer/generated';
import { format, parseISO } from 'date-fns';
import Link from 'next/link';
 
export default function PostCard(post: Post): React.ReactElement {
  return (
    <div className="mb-4 flex flex-col rounded-lg border-2 p-2">
      <Link href={post.url} className="mb-1 text-3xl text-blue-500">
        {post.title}
      </Link>
      <time dateTime={post.date}>
        {format(parseISO(post.date), 'LLLL d, yyyy')}
      </time>
    </div>
  );
}

7. 메인 페이지 수정

page.tsx에도 제목을 추가하고, 스타일을 추가하도록 하자.

app/page.tsx
import { allPosts } from '@/.contentlayer/generated';
import { compareDesc } from 'date-fns';
import PostCard from './components/PostCard';
 
export default function Home() {
  const posts = allPosts.sort((a, b) =>
    compareDesc(new Date(a.date), new Date(b.date))
  );
 
  return (
    <main className="mx-auto max-w-5xl">
      <h1 className="my-8 text-center text-3xl font-bold">
        Next.js & ContentLayer Blog Example
      </h1>
      {posts.map((post) => (
        <PostCard key={post._id} {...post} />
      ))}
    </main>
  );
}

8. 상세 페이지 구현

아직, 상세 페이지에 대한 코드를 작성하지 않아서, 각 Post를 클릭하면 404 페이지를 만나게 될 것이다.

이를 해결하기 위해 상세 페이지를 구현한다.

app/[slug]/page.tsx
import { allPosts } from '@/.contentlayer/generated';
import { notFound } from 'next/navigation';
import type { MDXComponents } from 'mdx/types';
import Link from 'next/link';
import { useMDXComponent } from 'next-contentlayer/hooks';
 
const mdxComponents: MDXComponents = {
  a: ({ href, children }) => <Link href={href as string}>{children}</Link>,
};
 
export const generatedStaticParams = async () => {
  allPosts.map((post) => ({ slug: post._raw.flattenedPath }));
};
 
export const generatedMetadata = ({ params }: { params: { slug: string } }) => {
  const post = allPosts.find((post) => post._raw.flattenedPath === params.slug);
  if (!post) notFound();
};
 
export default function PostPage({ params }: { params: { slug: string } }) {
  const post = allPosts.find((post) => post._raw.flattenedPath === params.slug);
  if (!post) notFound();
 
  const MDXContent = useMDXComponent(post.body.code);
 
  return (
    <article className="prose mx-auto">
      <div className="mb-8 text-center">
        <time dateTime={post.date} className="mb-1 text-xs text-gray-600">
          {new Intl.DateTimeFormat('en-US').format(new Date(post.date))}
        </time>
        <h1 className="text-3xl font-bold">{post.title}</h1>
      </div>
      <MDXContent components={mdxComponents} />
    </article>
  );
}

※ 404 페이지 구현

※ Next.js에서 제공해주는 404 페이지가 마음에 들지 않아 커스텀하고 싶은 경우, app 디렉토리에 not-found.tsx 파일로 404 페이지를 직접 구현할 수도 있다.

app/not-found.tsx
import Link from 'next/link';
 
export default function NotFound(): React.ReactElement {
  return (
    <div className="mx-auto my-20 flex max-w-4xl flex-col items-center justify-center">
      <h1 className="mb-10 text-center text-3xl text-orange-500">
        This Link is Not Valid
      </h1>
      <Link href="/" className="text-center text-blue-500">
        Go Home
      </Link>
    </div>
  );
}

9. 게시글에 스타일 추가

※ article의 style을 쉽게 하기 위해, **@tailwindcss/typography 를 사용할 수 있다.

사용하는 방법은 아래 명령어로 설치하고,

npm install -D @tailwindcss/typography

tailwind.config.js 에 해당 플러그인을 추가해준다.

tailwind.config.js
module.exports = {
  theme: {
    // ...
  },
  plugins: [
    **require('@tailwindcss/typography'),**
    // ...
  ],
}

완료되면 <article> 의 className에 prose를 추가해주면 된다!

<article className="prose mx-auto"> ... </article>

구현 결과

구현한 전체 템플릿은 아래 링크에서 확인할 수 있다.