npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@hansolbangul/notion-render

v0.0.3

Published

Reusable Notion collection parsing, rendering, SEO, and Next.js helpers.

Readme

@hansolbangul/notion-render

@hansolbangul/notion-render는 공개 Notion 페이지를 CMS처럼 읽어서 Next.js 블로그나 문서 사이트에 연결할 때 쓰는 라이브러리입니다.

이 패키지는 아래 흐름을 재사용 가능하게 분리하는 데 초점을 둡니다.

  • Notion 컬렉션 페이지 읽기
  • 페이지 속성 정규화
  • 게시글 필터링
  • 태그/카테고리 집계
  • generateStaticParams 생성
  • react-notion-x 기반 렌더링
  • Next.js SEO 메타데이터 생성
  • SSG / ISR 연결

중요:

  • 이 패키지는 공식 Notion REST API가 아니라 notion-client 기반의 public page recordMap 방식을 사용합니다.
  • 즉, 공개 가능한 Notion 페이지를 읽는 구조입니다.

이 패키지가 하는 일

  • Notion 컬렉션 페이지에서 row 목록을 가져옵니다.
  • 각 row의 속성을 읽어 일반적인 JS 객체로 바꿉니다.
  • slug, status, type, date 같은 값을 기준으로 게시 가능한 글만 골라냅니다.
  • 태그, 카테고리 같은 다중 선택 속성을 집계합니다.
  • 상세 페이지 렌더링용 recordMap을 가져옵니다.
  • react-notion-x 렌더러를 감싼 공용 컴포넌트를 제공합니다.
  • Next.js Metadata와 JSON-LD 생성 헬퍼를 제공합니다.

이 패키지가 하지 않는 일

  • 카드 UI, 리스트 UI, 헤더, 푸터를 만들지는 않습니다.
  • 댓글, 공유 버튼, 테마 토글까지 포함하지는 않습니다.
  • 프로젝트별 고유한 BlogPost 타입을 강제로 고정하지는 않습니다.

설치

이미 Next.js + React 앱이 있다면 보통 아래 한 줄이면 됩니다.

yarn add @hansolbangul/notion-render

직접 의존성을 함께 명시해서 설치하고 싶다면:

yarn add @hansolbangul/notion-render notion-client notion-types notion-utils react-notion-x prismjs katex
yarn add next react react-dom

모듈 구성

@hansolbangul/notion-render/server/notion-api

서버에서 Notion 데이터를 읽을 때 사용합니다.

주요 export:

  • createNotionClient
  • getCollectionItems
  • getPageRecordMap
  • resolvePageProperties

@hansolbangul/notion-render/server/filters

정규화된 row 데이터를 실제 게시글 목록으로 다듬을 때 사용합니다.

주요 export:

  • filterPublishedItems
  • countMultiValueOptions
  • toStaticParams
  • findAdjacentSlugs

@hansolbangul/notion-render/next/metadata

Next.js App Router 기준 SEO 메타데이터를 만들 때 사용합니다.

주요 export:

  • buildSiteMetadata
  • buildArticleMetadata
  • buildWebsiteJsonLd

@hansolbangul/notion-render/react/NotionContent

클라이언트 컴포넌트에서 recordMap을 렌더링할 때 사용합니다.

주의:

  • Notion 렌더링 스타일 CSS는 라이브러리 내부에서 자동 주입하지 않습니다.
  • 앱 루트 레이아웃에서 아래 CSS를 직접 import 해야 합니다.
import "react-notion-x/src/styles.css";
import "prismjs/themes/prism-tomorrow.css";
import "katex/dist/katex.min.css";

주요 export:

  • NotionContent
  • defaultMapPageUrl

빠른 시작

1. 컬렉션 페이지 읽기

import { getCollectionItems } from "@hansolbangul/notion-render/server/notion-api";

type BlogPost = {
  id: string;
  title: string;
  slug: string;
  status: string[];
  type: string[];
  tags?: string[];
  summary?: string;
  thumbnail?: string;
  date?: {
    start_date?: string;
  };
  createdTime: string;
  fullWidth: boolean;
};

export async function getPosts(pageId: string) {
  return getCollectionItems<BlogPost>({ pageId });
}

2. 게시 가능한 글만 필터링

import { filterPublishedItems } from "@hansolbangul/notion-render/server/filters";

const publishedPosts = filterPublishedItems(posts, {
  acceptStatus: ["Public"],
  acceptType: ["Post"],
});

3. 정적 params 생성

import { toStaticParams } from "@hansolbangul/notion-render/server/filters";

export async function generateStaticParams() {
  const posts = await getPosts(process.env.NOTION_PAGE_ID!);
  return toStaticParams(posts);
}

4. 상세 페이지용 recordMap 읽기

import { getPageRecordMap } from "@hansolbangul/notion-render/server/notion-api";

export async function getPostDetail(postId: string) {
  return getPageRecordMap(postId);
}

5. Notion 페이지 렌더링

"use client";

import { NotionContent } from "@hansolbangul/notion-render/react/NotionContent";
import type { ExtendedRecordMap } from "notion-types";

type Props = {
  recordMap: ExtendedRecordMap;
  title: string;
};

export function ArticleBody({ recordMap, title }: Props) {
  return (
    <NotionContent
      recordMap={recordMap}
      className="bg-white"
      pageTitle={<h1>{title}</h1>}
    />
  );
}

SEO 설정 예시

사이트 메타데이터

import {
  buildSiteMetadata,
  buildWebsiteJsonLd,
} from "@hansolbangul/notion-render/next/metadata";

export const metadata = buildSiteMetadata({
  title: "내 노션 블로그",
  description: "노션을 CMS로 사용하는 블로그",
  siteUrl: "https://example.com",
  keywords: ["blog", "notion"],
  locale: "ko_KR",
  siteName: "내 노션 블로그",
  image: {
    url: "/og.png",
    alt: "내 노션 블로그",
    width: 1200,
    height: 630,
  },
});

const websiteJsonLd = buildWebsiteJsonLd({
  title: "내 노션 블로그",
  description: "노션을 CMS로 사용하는 블로그",
  siteUrl: "https://example.com",
  siteName: "내 노션 블로그",
  image: {
    url: "/og.png",
    width: 1200,
    height: 630,
  },
});

게시글 메타데이터

import { buildArticleMetadata } from "@hansolbangul/notion-render/next/metadata";

export async function generateMetadata() {
  return buildArticleMetadata({
    title: post.title,
    description: post.summary || post.title,
    siteUrl: "https://example.com",
    path: `/post/${post.slug}`,
    keywords: post.tags,
    image: post.thumbnail
      ? {
          url: post.thumbnail,
          alt: post.title,
          width: 1200,
          height: 630,
        }
      : undefined,
    publishedTime: post.date?.start_date,
    authors: post.author?.map((author) => author.name).filter(Boolean),
  });
}

SSG / ISR 추천 패턴

권장 흐름은 아래와 같습니다.

  1. 서버에서 컬렉션 row 전체를 읽습니다.
  2. getCollectionItems로 일반 객체 배열을 만듭니다.
  3. filterPublishedItems로 게시 가능한 글만 남깁니다.
  4. toStaticParamsgenerateStaticParams에 연결합니다.
  5. unstable_cache로 목록과 상세 recordMap을 캐시합니다.
  6. revalidateTag, revalidatePath로 온디맨드 ISR을 연결합니다.

예시:

import { unstable_cache } from "next/cache";
import { filterPublishedItems } from "@hansolbangul/notion-render/server/filters";
import {
  getCollectionItems,
  getPageRecordMap,
} from "@hansolbangul/notion-render/server/notion-api";

const REVALIDATE_SECONDS = 3600;
const REVALIDATE_TAG = "notion-posts";

export const getCachedPosts = unstable_cache(
  async () =>
    filterPublishedItems(
      await getCollectionItems({
        pageId: process.env.NOTION_PAGE_ID!,
      }),
      {
        acceptStatus: ["Public"],
        acceptType: ["Post"],
      },
    ),
  [REVALIDATE_TAG],
  {
    revalidate: REVALIDATE_SECONDS,
    tags: [REVALIDATE_TAG],
  },
);

export const getCachedRecordMap = unstable_cache(
  async (pageId: string) => getPageRecordMap(pageId),
  ["notion-record-map"],
  {
    revalidate: REVALIDATE_SECONDS,
    tags: [REVALIDATE_TAG],
  },
);

타입은 고정인가?

아니요. 아래 예시의 BlogPost 타입은 고정 스키마가 아니라 "사용 예시"입니다.

type BlogPost = {
  id: string;
  title: string;
  slug: string;
  status: string[];
  type: string[];
  tags?: string[];
  summary?: string;
  thumbnail?: string;
  date?: {
    start_date?: string;
  };
  createdTime: string;
  fullWidth: boolean;
};

정확히는 이렇게 이해하시면 됩니다.

  • getCollectionItems<T>()는 제네릭 함수라서 사용자가 원하는 타입 T로 받을 수 있습니다.
  • 런타임에서 실제로 만들어지는 key는 Notion 컬렉션의 속성 이름을 기준으로 동적으로 결정됩니다.
  • 다만 공용 헬퍼인 filterPublishedItems는 기본적으로 title, slug, status, type, date 같은 key 이름을 기대합니다.
  • 만약 Notion 속성명이 다르면 옵션으로 key 이름을 바꿔서 사용할 수 있습니다.

예를 들어 속성명이 완전히 다를 수도 있습니다.

type ArticleRow = {
  id: string;
  제목: string;
  urlKey: string;
  공개상태: string[];
  문서종류: string[];
  발행일?: {
    start_date?: string;
  };
  createdTime: string;
  fullWidth: boolean;
};

const rows = await getCollectionItems<ArticleRow>({
  pageId: process.env.NOTION_PAGE_ID!,
});

const publishedRows = filterPublishedItems(rows, {
  titleKey: "제목",
  slugKey: "urlKey",
  statusKey: "공개상태",
  typeKey: "문서종류",
  dateKey: "발행일",
  acceptStatus: ["공개"],
  acceptType: ["아티클"],
});

즉:

  • 정적 타입은 사용자가 정의합니다.
  • 실제 데이터 key는 Notion 속성 이름에 따라 결정됩니다.
  • 블로그용 헬퍼는 기본 key 규약을 가지지만, 일부는 옵션으로 바꿀 수 있습니다.

추천 타입 형태

아래 형태는 블로그 프로젝트에서 가장 무난한 권장 예시입니다.

type Post = {
  id: string;
  title: string;
  slug: string;
  summary?: string;
  thumbnail?: string;
  status: string[];
  type: string[];
  tags?: string[];
  category?: string[];
  author?: {
    id: string;
    name?: string;
    profilePhoto?: string | null;
  }[];
  date?: {
    start_date?: string;
    end_date?: string;
  };
  createdTime: string;
  fullWidth: boolean;
};

개발

라이브러리 폴더에서:

yarn install
yarn build
yarn test

상위 추출 작업 레포에서:

yarn build:notion-render
yarn test:notion-render

배포

yarn install
yarn test
yarn npm publish --access public

배포 패키지 이름:

@hansolbangul/notion-render