Notion-As-Blog — Notion에서 쓰고 Next.js로 배포하기

Notion-As-Blog

Write in Notion. Publish with Next.js.
Notion을 CMS로 쓰고 Next.js 16, Tailwind CSS 4로 배포하는 모던 블로그 템플릿입니다.

공식 문서 · 템플릿 복제 · 빠른 시작 · English · 한국어 · 简体中文 · 日本語

Next.js TypeScript Tailwind CSS Notion License

--- ## 왜 Notion-As-Blog인가요? Notion-As-Blog는 복제한 Notion 템플릿을 빠르고 SEO 친화적인 프로덕션 블로그로 바꿔줍니다. 글 작성은 Notion 안에서 끝내고, 웹사이트는 Next.js 기반의 정적 생성, 안정적인 Notion 이미지 프록시, 검색, RSS, 사이트맵, 다크 모드, 저자 프로필까지 갖춘 형태로 배포할 수 있습니다.
Notion 중심 작성 경험
포스트, 페이지, 태그, 카테고리, 시리즈, 저자, 썸네일, 요약을 Notion에서 직접 관리합니다.
프로덕션 기본값
Next.js App Router, 정적 생성, 이미지 프록시, RSS, sitemap, robots.txt, 동적 OG 이미지, Organization JSON-LD를 포함합니다.
처음부터 보기 좋은 UI
반응형 레이아웃, 다크 모드, 포스트 애니메이션, 핀 포스트 슬라이드쇼, Giscus 댓글, 브랜딩 커스터마이징을 제공합니다.
셀프 호스팅 친화적
Vercel, Docker, 자체 Node.js 호스트에서 필요한 환경 변수와 캐시 동작을 명시적으로 설정할 수 있습니다.
--- ## 미리보기 아래 이미지는 배포된 공식 문서 사이트 기준으로 촬영한 스크린샷입니다. | 라이트 | 다크 | |:---:|:---:| | ![문서 홈 라이트](docs/screenshots/home-desktop.png) | ![문서 홈 다크](docs/screenshots/home-dark.png) |

문서 상세
모바일 문서 홈

--- ## 주요 기능 - **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"], // — []로 두면 태그 생략 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)