README.ko.md
Rama main · réplica de lectura del repositorio original.
<div align="center">
<a href="https://notion-as-blog.vercel.app">
<img src="docs/screenshots/readme-hero.svg" alt="Notion-As-Blog — Notion에서 쓰고 Next.js로 배포하기" width="100%" />
</a>
<h1>Notion-As-Blog</h1>
<p>
<strong>Write in Notion. Publish with Next.js.</strong><br />
<strong>Notion</strong>을 CMS로 쓰고 <strong>Next.js 16</strong>, <strong>Tailwind CSS 4</strong>로 배포하는 모던 블로그 템플릿입니다.
</p>
<p>
<a href="https://notion-as-blog.vercel.app"><strong>공식 문서</strong></a>
·
<a href="https://welcometogyuminworld.notion.site/Notion-As-Blog-30ab152141a480309a9ede1f8cac4cc7?source=copy_link"><strong>템플릿 복제</strong></a>
·
<a href="#빠른-시작"><strong>빠른 시작</strong></a>
·
<a href="README.md"><strong>English</strong></a>
·
<strong>한국어</strong>
·
<a href="README.zh-CN.md"><strong>简体中文</strong></a>
·
<a href="README.ja.md"><strong>日本語</strong></a>
</p>
<p>
<img alt="Next.js" src="https://img.shields.io/badge/Next.js-16-000000?style=for-the-badge&logo=next.js&logoColor=white" />
<img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-5-3178C6?style=for-the-badge&logo=typescript&logoColor=white" />
<img alt="Tailwind CSS" src="https://img.shields.io/badge/Tailwind_CSS-4-38BDF8?style=for-the-badge&logo=tailwindcss&logoColor=white" />
<img alt="Notion" src="https://img.shields.io/badge/Notion-CMS-000000?style=for-the-badge&logo=notion&logoColor=white" />
<img alt="License" src="https://img.shields.io/badge/License-MIT-22C55E?style=for-the-badge" />
</p>
</div>
---
## 왜 Notion-As-Blog인가요?
Notion-As-Blog는 복제한 Notion 템플릿을 빠르고 SEO 친화적인 프로덕션 블로그로 바꿔줍니다. 글 작성은 Notion 안에서 끝내고, 웹사이트는 Next.js 기반의 정적 생성, 안정적인 Notion 이미지 프록시, 검색, RSS, 사이트맵, 다크 모드, 저자 프로필까지 갖춘 형태로 배포할 수 있습니다.
<table>
<tr>
<td><strong>Notion 중심 작성 경험</strong><br />포스트, 페이지, 태그, 카테고리, 시리즈, 저자, 썸네일, 요약을 Notion에서 직접 관리합니다.</td>
<td><strong>프로덕션 기본값</strong><br />Next.js App Router, 정적 생성, 이미지 프록시, RSS, sitemap, robots.txt, 동적 OG 이미지, Organization JSON-LD를 포함합니다.</td>
</tr>
<tr>
<td><strong>처음부터 보기 좋은 UI</strong><br />반응형 레이아웃, 다크 모드, 포스트 애니메이션, 핀 포스트 슬라이드쇼, Giscus 댓글, 브랜딩 커스터마이징을 제공합니다.</td>
<td><strong>셀프 호스팅 친화적</strong><br />Vercel, Docker, 자체 Node.js 호스트에서 필요한 환경 변수와 캐시 동작을 명시적으로 설정할 수 있습니다.</td>
</tr>
</table>
---
## 미리보기
아래 이미지는 배포된 공식 문서 사이트 기준으로 촬영한 스크린샷입니다.
| 라이트 | 다크 |
|:---:|:---:|
|  |  |
<p align="center">
<img src="docs/screenshots/post-desktop.png" alt="문서 상세" width="70%" />
<br />
<img src="docs/screenshots/home-mobile.png" alt="모바일 문서 홈" width="260" />
</p>
---
## 주요 기능
- **Notion CMS** — Notion에서 직접 글을 작성하고 관리합니다.
- **멀티 저자 지원** — 아바타, 소개, 역할, 소셜 링크가 포함된 Authors data source를 선택적으로 사용할 수 있습니다.
- **카테고리, 태그, 시리즈** — 카테고리 페이지, 태그 필터링, 시리즈 이전/다음 내비게이션을 제공합니다.
- **전문 검색** — 검색 API, 즉시 결과 드롭다운, 전용 검색 페이지를 포함합니다.
- **다크 모드** — 내장 theme preference hook 기반 시스템 연동 테마 전환을 제공합니다.
- **SEO 최적화** — Open Graph, 동적 OG 이미지, sitemap, robots.txt, RSS, canonical URL, Organization JSON-LD를 지원합니다.
- **안정적인 Notion 이미지** — 만료되는 Notion 업로드 파일 URL을 signed image proxy로 안정적으로 렌더링합니다.
- **Giscus 댓글** — GitHub Discussions 기반 댓글을 포스트 상세 페이지에 붙일 수 있습니다.
- **반응형 UI** — Tailwind CSS 4 기반 모바일 퍼스트 레이아웃입니다.
- **커스텀 브랜딩** — 이름, 로고, 파비콘, 색상, 폰트, 푸터 링크, 소셜 링크, 카테고리, 문구를 설정할 수 있습니다.
- **Docker 지원** — 멀티 스테이지 프로덕션 Dockerfile과 compose 예시를 포함합니다.
- **온디맨드 갱신** — 보호된 revalidation endpoint로 Notion 변경사항을 즉시 반영할 수 있습니다.
---
## 빠른 시작
### 1. Notion 템플릿 복제
아래 공개 Notion 페이지를 열고 **Duplicate**를 눌러 전체 템플릿을 본인의 Notion 워크스페이스로 복제하세요.
> **[Notion-As-Blog 템플릿 복제](https://welcometogyuminworld.notion.site/Notion-As-Blog-30ab152141a480309a9ede1f8cac4cc7?source=copy_link)**
템플릿에는 **Posts** data source, 선택 사항인 **Authors** data source, 문서형 샘플 콘텐츠가 포함되어 있습니다. 복제 후에는 모든 DB row가 본인 워크스페이스에 속하므로 자유롭게 수정하거나 삭제할 수 있습니다.
### 2. Notion Integration 생성
1. [My Integrations](https://www.notion.so/profile/integrations)에서 **New integration** 클릭
2. 이름 입력, 예: `notion-as-blog`
3. 템플릿을 복제한 워크스페이스 선택
4. **Internal Integration Secret** 복사 — 이것이 `NOTION_API_KEY`입니다
### 3. 데이터베이스에 Integration 연결
1. Notion에서 **Posts** 데이터베이스 페이지 열기
2. 우측 상단 **···** → **연결(Connections)** → 생성한 Integration 선택 후 **연결**
3. 풍부한 저자 프로필을 쓰려면 **Authors** 데이터베이스에도 동일하게 반복
### 4. Data source ID 확인
`NOTION_DATA_SOURCE_ID`에는 URL에 보이는 database ID가 아니라 **Notion data source ID**를 넣어야 합니다. 최신 Notion API에서는 database ID와 data source ID가 다를 수 있습니다.
복제한 Posts 데이터베이스를 열고 Integration을 연결한 뒤 Posts data source ID를 `NOTION_DATA_SOURCE_ID`로 설정하세요. Authors data source를 쓰는 경우 Authors data source ID를 `NOTION_AUTHORS_DATA_SOURCE_ID`로 설정합니다. database ID를 잘못 넣으면 빌드 중 `object_not_found`가 발생할 수 있습니다.
### 5. 프로젝트 설치 및 설정
```bash
git clone https://github.com/catuscio/notion-as-blog.git
cd notion-as-blog
npm install
cp .env.example .env.local
```
`.env.local` 파일을 편집합니다:
```env
NOTION_API_KEY=secret_xxxxxxxxxxxxxxxxxxxxx
NOTION_DATA_SOURCE_ID=your_posts_data_source_id
NOTION_AUTHORS_DATA_SOURCE_ID=your_authors_data_source_id
TOKEN_FOR_REVALIDATE=any_random_secret_string
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
```
### 6. 로컬 실행
```bash
npm run dev
```
[http://localhost:3000](http://localhost:3000)에서 블로그를 확인할 수 있습니다.
---
## Notion에서 글 작성하기
### 새 포스트 작성
1. Notion에서 **Posts** 데이터베이스를 열기
2. 새 행을 추가하고 속성(property)들을 채우기
3. 페이지 본문에 글 내용을 작성 — 제목, 목록, 코드 블록, 이미지 등 모두 지원됩니다
4. 발행 준비가 되면 `status`를 **Public**으로 설정
### Posts 데이터베이스 컬럼
| 컬럼 | 타입 | 필수 | 설명 |
|---|---|---|---|
| **title** | Title | Yes | 포스트 제목 |
| **slug** | Rich text | No | URL 슬러그 (예: `my-first-post`). 비워두면 페이지 ID로 자동 생성 |
| **status** | Select | Yes | 발행 상태 (아래 참조) |
| **type** | Select | No | `Post` (기본값) 또는 `Page` |
| **date** | Date | Yes | 발행일. 이 필드를 기준으로 포스트가 정렬됩니다 |
| **category** | Select | Yes | `brand.ts`에 정의된 카테고리 이름과 일치해야 합니다 |
| **tags** | Multi-select | No | 자유 태그 (예: `Next.js`, `React`) |
| **series** | Select | No | 시리즈 이름. 같은 series 값을 선택한 포스트들은 이전/다음 네비게이션으로 연결됩니다 |
| **author** | People | No | Notion 워크스페이스 멤버. Authors data source의 `people` 속성과 Notion user ID로 매칭됩니다 |
| **summary** | Rich text | No | 포스트 카드와 SEO 메타에 표시되는 짧은 설명 |
| **thumbnail** | Files & media | No | 커버 이미지 (업로드 또는 외부 URL 붙여넣기) |
### Status 값
| 값 | 목록에 표시 | URL로 직접 접근 |
|---|---|---|
| `Public` | O | O |
| `PublicOnDetail` | X | O — 검색 색인에서도 제외되는 unlisted 콘텐츠이며 접근 제어 기능은 아님 |
| `Draft` | X | X |
| `Private` | X | X |
`PublicOnDetail`은 `Post`와 `Page`에 동일하게 적용됩니다. 피드, 검색, 카테고리·태그·시리즈·작성자 페이지, RSS, 사이트맵, 구조화 데이터, 댓글, 빌드 시점 경로 생성에서 제외됩니다. URL을 아는 사람은 누구나 접근하고 공유할 수 있으므로 민감한 콘텐츠에는 인증을 사용하세요.
### Type 값
| 값 | 설명 |
|---|---|
| `Post` | 일반 블로그 포스트. 홈 피드, 카테고리 페이지, 검색에 표시 |
| `Page` | 독립 페이지 (예: 랜딩 페이지). 포스트 목록에 표시되지 않음 |
### 시리즈 사용법
여러 포스트를 시리즈로 묶으려면, 각 포스트에서 같은 `series` 값을 선택하세요 (예: `Next.js Blog Tutorial`). 블로그는 포스트 상세 페이지에 이전/다음 링크를 렌더링하고 `/series/[name]` 페이지도 제공합니다.
### Authors data source (선택사항)
Notion 워크스페이스 멤버 이름 외에 더 풍부한 저자 프로필을 원한다면, 템플릿에 포함된 Authors data source를 사용하세요. 포스트의 `author` People 값과 Authors row의 `people` People 값이 같은 Notion user ID를 가질 때 프로필이 매칭됩니다.
| 컬럼 | 타입 | 설명 |
|---|---|---|
| **name** | Title | 저자 카드와 저자 페이지에 표시할 이름 |
| **people** | People | 이 프로필이 나타내는 Notion 워크스페이스 사용자. 포스트와 저자 프로필 매칭에 사용됩니다 |
| **role** | Rich text | 직함 또는 역할 (예: `프론트엔드 엔지니어`) |
| **bio** | Rich text | 짧은 소개글 |
| **avatar** | Files & media | 프로필 사진 |
| **email** | Rich text | 이메일 주소 |
| **github** | URL | GitHub 프로필 URL |
| **x** | URL | X (Twitter) 프로필 URL |
| **linkedin** | URL | LinkedIn 프로필 URL |
| **website** | URL | 개인 웹사이트 URL |
---
## 커스터마이징
모든 사이트 설정은 `src/config/brand.ts`에서 관리합니다:
### 사이트 정보
```ts
name: "My Blog",
title: "A Developer Blog",
highlight: "Developer", // 타이틀에서 강조할 단어
description: "Your blog description.",
url: "https://your-domain.com",
since: 2025, // 푸터 저작권 시작 연도
lang: "ko",
```
### 로고 & 파비콘
```ts
logo: {
image: "", // 로고 이미지 경로 (/public 기준). "" = 텍스트만 표시
showNameWithLogo: true, // 로고 옆에 블로그 이름 표시
png: "/logo.png", // JSON-LD, RSS 피드에 사용
ogWhite: "/logo-white.png", // OG 이미지에 오버레이되는 흰색 로고
favicon: "", // 커스텀 파비콘 경로. "" = 자동 생성 글자 아이콘
},
```
### 색상
`colors` 객체에서 HSL 값으로 색상 테마를 변경할 수 있습니다. 라이트 모드와 다크 모드 색상 모두 설정 가능합니다. 각 테마는 5가지 기본 값으로 구성됩니다:
- **brand** — 강조 색상 (버튼, 링크, 포커스 링)
- **bg** — 페이지 배경
- **text** — 본문 텍스트
- **surface** — 카드 및 음소거 영역 배경
- **edge** — 테두리 및 입력 필드 외곽선
### 폰트
```ts
fonts: {
sans: {
stack: 'Inter, -apple-system, BlinkMacSystemFont, system-ui, sans-serif',
},
mono: {
family: "JetBrains Mono",
cdn: "https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap",
preconnect: ["https://fonts.googleapis.com", "https://fonts.gstatic.com"],
},
og: {
family: "Pretendard",
url: "https://cdn.jsdelivr.net/.../Pretendard-Bold.otf",
},
},
```
- **sans** — 본문 텍스트용 폰트 스택. 비라틴 언어는 웹 폰트를 앞에 추가 (예: `'Pretendard, -apple-system, ...'`)
- **mono** — 코드 블록용 고정폭 폰트. Google Fonts CDN에서 로드
- **og** — 동적 OG 이미지 생성에 사용하는 폰트 (`.otf` 또는 `.ttf` URL)
### 카테고리
카테고리 이름은 Notion Posts 데이터베이스의 Select 값과 **정확히 일치**해야 합니다:
```ts
categories: [
{ name: "Development", slug: "development", color: "orange", icon: "dns", description: "..." },
{ name: "Design", slug: "design", color: "teal", icon: "palette", description: "..." },
{ name: "Product", slug: "product", color: "green", icon: "work", description: "..." },
],
```
### 소셜 링크
푸터에 표시되는 소셜 미디어 아이콘 링크입니다. `""`으로 설정하면 해당 아이콘이 숨겨집니다.
```ts
social: {
github: "https://github.com/your-username",
twitter: "",
instagram: "",
facebook: "",
youtube: "",
linkedin: "",
threads: "",
tiktok: "",
naverBlog: "",
},
```
### 푸터 링크
현재 푸터는 `src/components/layout/Footer.tsx`에서 단순하게 렌더링됩니다. 표시되는 항목은 다음과 같습니다.
- Home
- About
- `brand.templateUrl`로 연결되는 Template
- `brand.social`에서 URL이 비어 있지 않은 소셜 아이콘
더 큰 푸터 네비게이션이 필요하면 `Footer.tsx`를 수정하고, 라벨은 `src/config/copy.ts`에서 함께 관리하세요.
### SEO
```ts
keywords: ["Next.js", "blog", "frontend"], // <meta name="keywords"> — []로 두면 태그 생략
organization: { // Google 지식 패널용 Organization JSON-LD (선택사항)
name: "Your Company",
url: "https://your-domain.com",
logo: "/logo.png",
// ... address, contactPoint, sameAs 등
},
```
### Giscus 댓글
[Giscus](https://giscus.app/)를 설정한 후 config를 채우세요:
```ts
giscus: {
repo: "your-username/your-repo",
repoId: "R_...",
category: "Announcements",
categoryId: "DIC_...",
mapping: "pathname", // 포스트-디스커션 매핑 방식
strict: "0", // 엄격한 제목 매칭
reactionsEnabled: "1", // 리액션 버튼 표시
emitMetadata: "0", // 디스커션 메타데이터 전송
inputPosition: "bottom", // 댓글 입력 위치
},
```
### 뉴스레터 CTA
`enabled`를 `true`로 설정하면 홈 피드 하단에 구독 섹션이 표시됩니다. 실제 구독 로직은 별도로 구현해야 합니다.
```ts
newsletter: {
enabled: false,
headline: "Stay ahead of the curve",
description: "Join developers receiving the best content...",
placeholder: "Enter your email address",
cta: "Subscribe",
disclaimer: "No spam, unsubscribe anytime.",
},
```
### 포스트 애니메이션
```ts
postAnimation: {
enabled: true, // 상세 페이지 타이프라이터 제목 + 슬라이드업 전환
},
```
### 동작 설정
```ts
postsPerPage: 10, // 피드 페이지당 포스트 수
slideshow: { intervalMs: 5000 }, // 고정 포스트 슬라이드쇼 자동 전환 간격 (ms)
reading: { wordsPerMinute: 200 }, // 읽기 시간 계산 (영어 200–250, CJK 500–600)
search: {
dropdownLimit: 10, // 검색 드롭다운 최대 결과 수
pageLimit: 30, // /search 페이지 최대 결과 수
},
```
### 캐시
```ts
cache: {
revalidate: 1800, // ISR 갱신 간격 (초, 기본값: 30분)
feedTtl: 3600, // RSS Cache-Control max-age (초, 기본값: 1시간)
authorsRevalidate: 300, // 저자 캐시 재검증 주기 (초, 기본값: 5분)
},
```
### Notion 이미지 프록시
Notion에 업로드된 파일 URL은 만료되므로, Notion 파일은 안정적인 서명 URL로 렌더링됩니다.
```txt
/api/notion-image?...
```
API route는 요청 시점에 최신 Notion 파일 URL을 다시 확인하고 이미지 bytes를 프록시합니다. 기본 프록시 캐시 TTL은 Notion signed URL의 1시간 만료보다 짧은 3300초입니다.
```env
NOTION_IMAGE_CACHE_SECONDS=3300
NOTION_IMAGE_SIGNING_SECRET=optional-separate-secret
```
외부 이미지 URL은 그대로 렌더링됩니다. `NOTION_IMAGE_SIGNING_SECRET`을 설정한다면, 정적 페이지가 생성되는 환경과 `/api/notion-image`가 실행되는 런타임 환경에서 같은 값을 사용해야 합니다. 그렇지 않으면 빌드 시 생성된 signed image URL이 런타임에서 검증되지 않을 수 있습니다.
---
## 온디맨드 갱신 (Revalidation)
블로그는 성능을 위해 Notion 데이터를 캐싱합니다. Notion에서 포스트를 수정한 후 즉시 반영하려면 다음과 같이 요청하세요:
```bash
curl -X POST https://your-domain.com/api/revalidate \
-H "Authorization: Bearer YOUR_TOKEN_FOR_REVALIDATE"
```
Notion 자동화나 외부 서비스의 웹훅으로 연결할 수도 있습니다. 별도로 갱신을 트리거하지 않으면, 콘텐츠는 30분마다 자동으로 새로고침됩니다.
---
## 배포
### Vercel (권장)
1. GitHub에 레포지토리 푸시
2. [Vercel](https://vercel.com/new)에서 프로젝트 임포트
3. Project Settings → Environment Variables에서 환경변수 추가
4. 배포
### Docker
```bash
# docker compose로 빌드 및 실행
docker compose up -d
# 또는 직접 빌드 (NOTION_API_KEY는 정적 생성을 위해 빌드 시점에 필요)
docker build -t notion-as-blog \
--build-arg NOTION_API_KEY=your_key \
--build-arg NOTION_DATA_SOURCE_ID=your_data_source_id \
.
docker run -p 3000:3000 \
-e NOTION_API_KEY=your_key \
-e NOTION_DATA_SOURCE_ID=your_data_source_id \
-e NOTION_AUTHORS_DATA_SOURCE_ID=your_authors_data_source_id \
-e TOKEN_FOR_REVALIDATE=your_revalidate_token \
-e NOTION_IMAGE_SIGNING_SECRET=your_image_signing_secret \
-e NOTION_IMAGE_CACHE_SECONDS=3300 \
notion-as-blog
```
---
## 환경변수
| 변수 | 필수 | 설명 |
|---|---|---|
| `NOTION_API_KEY` | Yes | Notion Integration API 키 |
| `NOTION_DATA_SOURCE_ID` | Yes | Notion Posts data source ID |
| `NOTION_AUTHORS_DATA_SOURCE_ID` | No | Notion Authors data source ID |
| `TOKEN_FOR_REVALIDATE` | No | 온디맨드 갱신용 시크릿 토큰 (`/api/revalidate`) |
| `NOTION_IMAGE_SIGNING_SECRET` | No | 안정적인 Notion 이미지 프록시 URL을 위한 HMAC secret. 생략하면 `NOTION_API_KEY`를 사용합니다 |
| `NOTION_IMAGE_CACHE_SECONDS` | No | 프록시된 Notion 파일의 CDN 캐시 TTL. 최대 3300초로 제한됩니다 |
| `NEXT_PUBLIC_GA_ID` | No | Google Analytics 측정 ID |
---
## 라이선스
[MIT](LICENSE)