>  기사  >  웹 프론트엔드  >  [개인 홈페이지] Next에서 Notion Database를 통합하는 방법

[개인 홈페이지] Next에서 Notion Database를 통합하는 방법

DDD
DDD원래의
2024-10-12 16:37:021004검색

Notion 데이터베이스를 Next.js 프로젝트에 통합하려면 Notion을 콘텐츠 관리 시스템(CMS)으로 사용하고 해당 콘텐츠를 웹사이트에 표시할 수 있습니다. 다음은 Notion 데이터베이스를 Next.js에 통합하는 데 도움이 되는 간단한 단계별 가이드입니다.

기본 준비

Notion API 키 및 데이터베이스 ID 얻기

  1. Notion API 키 받기: Notion 개발자 포털로 이동하여 새로운 통합을 생성하세요. 생성되면 API 키를 받게 됩니다.
  2. 데이터베이스 ID 가져오기: 통합하려는 Notion 데이터베이스로 이동하고 데이터베이스 페이지의 URL을 복사하세요. 데이터베이스 ID는 URL의 https://www.notion.so/와 ?v= 사이의 문자열입니다.

[Personal Website] How to Integrate Notion Database in Next

还不清楚的可以看上篇文章:【01.【个人网站】如何使用Notion WORK为数据库进行全栈开发】

공식 SDK 사용

1단계: 종속성 설치

먼저 Notion API와 통신하기 위해서는 Notion의 공식 SDK인 @notionhq/client를 설치해야 합니다. npm 또는 Yarn을 사용하여 설치할 수 있습니다.

npm install @notionhq/client
# or
yarn add @notionhq/client

2단계: Notion 클라이언트 설정

Next.js 프로젝트의 루트 디렉터리에 lib/notion.js 파일을 생성하고 다음과 같이 Notion 클라이언트를 구성합니다.

// lib/notion.js
import { Client } from '@notionhq/client';

const notion = new Client({
  auth: process.env.NOTION_API_KEY,
});

export const getDatabase = async (databaseId) => {
  const response = await notion.databases.query({ database_id: databaseId });
  return response.results;
};

Notion API 키를 환경 변수 NOTION_API_KEY에 저장하세요.(.env.local)

NOTION_API_KEY=your_secret_api_key

3단계: 데이터베이스 콘텐츠 가져오기

Next.js 페이지에서 getStaticProps 또는 getServerSideProps를 사용하여 Notion 데이터베이스에서 콘텐츠를 가져올 수 있습니다.

// pages/index.js
import { getDatabase } from '../lib/notion';

export const getStaticProps = async () => {
  const databaseId = process.env.NOTION_DATABASE_ID;
  const posts = await getDatabase(databaseId);

  return {
    props: {
      posts,
    },
    revalidate: 1, // ISR (Incremental Static Regeneration)
  };
};

export default function Home({ posts }) {
  return (
    <div>
      <h1>My Notion Blog</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            {post.properties.Name.title[0].plain_text}
          </li>
        ))}
      </ul>
    </div>
  );
}

NOTION_DATABASE_ID에 환경 변수로 Notion 데이터베이스 ID가 저장되어 있는지 확인하세요.

NOTION_DATABASE_ID=your_database_id

4단계: 배포 및 확인

마지막으로 Next.js 프로젝트를 Vercel 또는 다른 플랫폼에 배포하고 Notion 데이터베이스에서 데이터를 성공적으로 가져오고 표시할 수 있는지 확인하세요.

추가 팁

  • 페이지에 Notion 페이지의 다양한 속성(예: 이름, 태그, 날짜 등)을 표시할 수 있습니다.
  • getServerSideProps를 사용하여 모든 요청에 ​​대해 데이터를 가져오거나 ISR(증분 정적 재생)과 함께 getStaticProps를 사용하여 성능을 최적화하는 것을 고려해 보세요.

이 단계를 따르면 Notion 데이터베이스를 Next.js 프로젝트에 성공적으로 통합하고 이를 사용하여 콘텐츠를 관리하고 표시할 수 있습니다.

래핑된 URL과 함께 Notion API 사용

마지막으로 React-notion-x 컴포넌트와 통합할 때 편의상 이 방법을 사용하게 되었습니다.

Notion 데이터베이스 ID와 Notion API 키를 .env.local 파일에 추가하세요.

NOTION_DATABASE_ID=your_database_id
NOTION_API_KEY=your_secret_api_key

1단계: 필요한 종속성 설치

먼저 notion-types, notion-utils와 같은 필수 종속성을 설치하고 Notion API에 대한 요청을 처리해야 합니다.

npm install notion-types notion-utils got p-map

2단계: Notion API 상호 작용을 캡슐화하는 파일 만들기

Next.js 프로젝트에서 Notion API와의 상호 작용을 캡슐화하는 lib/NotionAPI.ts와 같은 파일을 만듭니다. 이 파일에는 페이지와 컬렉션에서 데이터를 가져오기 위해 Notion API 엔드포인트를 호출하는 메서드가 포함되어 있습니다.

// lib/NotionAPI.ts
import * as notion from "notion-types";
import got, { OptionsOfJSONResponseBody } from "got";
import {
  getBlockCollectionId,
  getPageContentBlockIds,
  parsePageId,
  uuidToId,
} from "notion-utils";
import pMap from "p-map";

// 定义权限记录接口
export interface SignedUrlRequest {
  permissionRecord: PermissionRecord;
  url: string;
}

export interface PermissionRecord {
  table: string;
  id: notion.ID;
}

export interface SignedUrlResponse {
  signedUrls: string[];
}

// 定义NotionAPI类
export class NotionAPI {
  private readonly _apiBaseUrl: string;
  private readonly _authToken?: string;
  private readonly _activeUser?: string;
  private readonly _userTimeZone: string;

  constructor({
    apiBaseUrl = "<https://www.notion.so/api/v3>",
    authToken,
    activeUser,
    userTimeZone = "America/New_York",
  }: {
    apiBaseUrl?: string;
    authToken?: string;
    userLocale?: string;
    userTimeZone?: string;
    activeUser?: string;
  } = {}) {
    this._apiBaseUrl = apiBaseUrl;
    this._authToken = authToken;
    this._activeUser = activeUser;
    this._userTimeZone = userTimeZone;
  }

  // 获取页面内容
  public async getPage(
    pageId: string,
    {
      concurrency = 3,
      fetchMissingBlocks = true,
      fetchCollections = true,
      signFileUrls = true,
      chunkLimit = 100,
      chunkNumber = 0,
      gotOptions,
    }: {
      concurrency?: number;
      fetchMissingBlocks?: boolean;
      fetchCollections?: boolean;
      signFileUrls?: boolean;
      chunkLimit?: number;
      chunkNumber?: number;
      gotOptions?: OptionsOfJSONResponseBody;
    } = {}
  ): Promise<notion.ExtendedRecordMap> {
    const page = await this.getPageRaw(pageId, {
      chunkLimit,
      chunkNumber,
      gotOptions,
    });

    const recordMap = page?.recordMap as notion.ExtendedRecordMap;

    if (!recordMap?.block) {
      throw new Error(`Notion page not found "${uuidToId(pageId)}"`);
    }

    recordMap.collection = recordMap.collection ?? {};
    recordMap.collection_view = recordMap.collection_view ?? {};
    recordMap.notion_user = recordMap.notion_user ?? {};
    recordMap.collection_query = {};
    recordMap.signed_urls = {};

    if (fetchMissingBlocks) {
      while (true) {
        const pendingBlockIds = getPageContentBlockIds(recordMap).filter(
          (id) => !recordMap.block[id]
        );

        if (!pendingBlockIds.length) {
          break;
        }

        const newBlocks = await this.getBlocks(
          pendingBlockIds,
          gotOptions
        ).then((res) => res.recordMap.block);

        recordMap.block = { ...recordMap.block, ...newBlocks };
      }
    }

    const contentBlockIds = getPageContentBlockIds(recordMap);

    if (fetchCollections) {
      const allCollectionInstances: Array<{
        collectionId: string;
        collectionViewId: string;
      }> = contentBlockIds.flatMap((blockId) => {
        const block = recordMap.block[blockId].value;
        const collectionId =
          block &&
          (block.type === "collection_view" ||
            block.type === "collection_view_page") &&
          getBlockCollectionId(block, recordMap);

        if (collectionId) {
          return block.view_ids?.map((collectionViewId) => ({
            collectionId,
            collectionViewId,
          }));
        } else {
          return [];
        }
      });

      await pMap(
        allCollectionInstances,
        async (collectionInstance) => {
          const { collectionId, collectionViewId } = collectionInstance;
          const collectionView =
            recordMap.collection_view[collectionViewId]?.value;

          try {
            const collectionData = await this.getCollectionData(
              collectionId,
              collectionViewId,
              collectionView,
              {
                gotOptions,
              }
            );

            recordMap.block = {
              ...recordMap.block,
              ...collectionData.recordMap.block,
            };

            recordMap.collection = {
              ...recordMap.collection,
              ...collectionData.recordMap.collection,
            };

            recordMap.collection_view = {
              ...recordMap.collection_view,
              ...collectionData.recordMap.collection_view,
            };

            recordMap.notion_user = {
              ...recordMap.notion_user,
              ...collectionData.recordMap.notion_user,
            };

            recordMap.collection_query![collectionId] = {
              ...recordMap.collection_query![collectionId],
              [collectionViewId]: (collectionData.result as any)
                ?.reducerResults,
            };
          } catch (err: any) {
            console.warn(
              "NotionAPI collectionQuery error",
              pageId,
              err.message
            );
          }
        },
        {
          concurrency,
        }
      );
    }

    if (signFileUrls) {
      await this.addSignedUrls({ recordMap, contentBlockIds, gotOptions });
    }

    return recordMap;
  }

  public async addSignedUrls({
    recordMap,
    contentBlockIds,
    gotOptions = {},
  }: {
    recordMap: notion.ExtendedRecordMap;
    contentBlockIds?: string[];
    gotOptions?: OptionsOfJSONResponseBody;
  }) {
    recordMap.signed_urls = {};

    if (!contentBlockIds) {
      contentBlockIds = getPageContentBlockIds(recordMap);
    }

    const allFileInstances = contentBlockIds.flatMap((blockId) => {
      const block = recordMap.block[blockId]?.value;

      if (
        block &&
        (block.type === "pdf" ||
          block.type === "audio" ||
          (block.type === "image" && block.file_ids?.length) ||
          block.type === "video" ||
          block.type === "file" ||
          block.type === "page")
      ) {
        const source =
          block.type === "page"
            ? block.format?.page_cover
            : block.properties?.source?.[0]?.[0];

        if (source) {
          if (!source.includes("secure.notion-static.com")) {
            return [];
          }

          return {
            permissionRecord: {
              table: "block",
              id: block.id,
            },
            url: source,
          };
        }
      }

      return [];
    });

    if (allFileInstances.length > 0) {
      try {
        const { signedUrls } = await this.getSignedFileUrls(
          allFileInstances,
          gotOptions
        );

        if (signedUrls.length === allFileInstances.length) {
          for (let i = 0; i < allFileInstances.length; ++i) {
            const file = allFileInstances[i];
            const signedUrl = signedUrls[i];

            recordMap.signed_urls[file.permissionRecord.id] = signedUrl;
          }
        }
      } catch (err) {
        console.warn("NotionAPI getSignedfileUrls error", err);
      }
    }
  }

  public async getPageRaw(
    pageId: string,
    {
      gotOptions,
      chunkLimit = 100,
      chunkNumber = 0,
    }: {
      chunkLimit?: number;
      chunkNumber?: number;
      gotOptions?: OptionsOfJSONResponseBody;
    } = {}
  ): Promise<notion.PageChunk> {
    const parsedPageId = parsePageId(pageId);

    if (!parsedPageId) {
      throw new Error(`invalid notion pageId "${pageId}"`);
    }

    const body = {
      pageId: parsedPageId,
      limit: chunkLimit,
      chunkNumber: chunkNumber,
      cursor: { stack: [] },
      verticalColumns: false,
    };

    return this.fetch<notion.PageChunk>({
      endpoint: "loadPageChunk",
      body,
      gotOptions,
    });
  }

  public async getCollectionData(
    collectionId: string,
    collectionViewId: string,
    collectionView?: any,
    {
      limit = 9999,
      searchQuery = "",
      userTimeZone = this._userTimeZone,
      loadContentCover = true,
      gotOptions,
    }: {
      limit?: number;
      searchQuery?: string;
      userTimeZone?: string;
      loadContentCover?: boolean;
      gotOptions?: OptionsOfJSONResponseBody;
    } = {}
  ) {
    const type = collectionView?.type;

    const isBoardType = type === "board";
    const groupBy = isBoardType
      ? collectionView?.format?.board_columns_by
      : collectionView?.format?.collection_group_by;

    let filters = [];
    if (collectionView?.format?.property_filters) {
      filters = collectionView.format?.property_filters.map(
        (filterObj

: any) => ({
          property: filterObj?.property,
          filter: {
            operator: "and",
            filters: filterObj?.filter?.filters,
          },
        })
      );
    }

    const body = {
      collection: {
        id: collectionId,
      },
      collectionView: {
        id: collectionViewId,
      },
      loader: {
        type: "reducer",
        reducers: {
          collection_group_results: {
            type: "results",
            limit,
            loadContentCover,
          },
        },
        userTimeZone,
        limit,
        loadContentCover,
        searchQuery,
        userLocale: "en",
        ...(filters.length > 0 ? { filters } : {}),
        ...(groupBy
          ? {
              groupBy,
            }
          : {}),
      },
    };

    return this.fetch<notion.CollectionInstance>({
      endpoint: "queryCollection",
      body,
      gotOptions,
    });
  }

  private async fetch<R>({
    endpoint,
    body,
    gotOptions,
  }: {
    endpoint: string;
    body: unknown;
    gotOptions?: OptionsOfJSONResponseBody;
  }) {
    const url = `${this._apiBaseUrl}/${endpoint}`;
    const json = true;
    const method = "POST";

    const headers: Record<string, string> = {
      "Content-Type": "application/json",
    };

    if (this._authToken) {
      headers.cookie = `token_v2=${this._authToken}`;
    }

    if (this._activeUser) {
      headers["x-notion-active-user-header"] = this._activeUser;
    }

    try {
      const res = await got.post(url, {
        ...gotOptions,
        json,
        method,
        body,
        headers,
      });

      return res.body as R;
    } catch (err) {
      console.error(`NotionAPI error: ${err.message}`);
      throw err;
    }
  }

  private async getSignedFileUrls(
    urls: SignedUrlRequest[],
    gotOptions?: OptionsOfJSONResponseBody
  ): Promise<SignedUrlResponse> {
    return this.fetch<SignedUrlResponse>({
      endpoint: "getSignedFileUrls",
      body: { urls },
      gotOptions,
    });
  }

  private async getBlocks(
    blockIds: string[],
    gotOptions?: OptionsOfJSONResponseBody
  ): Promise<notion.PageChunk> {
    return this.fetch<notion.PageChunk>({
      endpoint: "syncRecordValues",
      body: {
        requests: blockIds.map((blockId) => ({
          id: blockId,
          table: "block",
          version: -1,
        })),
      },
      gotOptions,
    });
  }
}

3단계: Next.js 페이지에서 캡슐화된 API 사용

Next.js 페이지에서 Notion 데이터베이스 콘텐츠를 가져오려면 캡슐화된 NotionAPI 클래스를 사용하고 검색된 데이터를 렌더링을 위해 React-notion-x 구성 요소에 전달할 수 있습니다.

// pages/[pageId].tsx
import { GetServerSideProps } from 'next';
import { NotionAPI } from '../lib/NotionAPI';
import { NotionRenderer } from 'react-notion-x';
import 'react-notion-x/src/styles.css';

export const getServerSideProps: GetServerSideProps = async (context) => {
  const { pageId } = context.params;
  const notion = new NotionAPI();
  const recordMap = await notion.getPage(pageId as string);

  return {
    props: {
      recordMap,
    },
  };
};

const NotionPage = ({ recordMap }) => {
  return <NotionRenderer recordMap={recordMap} fullPage={true} darkMode={false} />;
};

export default NotionPage;

4단계: Next.js에서 라우팅 구성

[pageId].tsx 파일이 다양한 Notion 페이지를 동적으로 렌더링할 수 있도록 하려면 Next.js에서 동적 라우팅을 설정해야 합니다. 이를 통해 pageId 매개변수를 일치시키고 해당 Notion 페이지 콘텐츠를 가져올 수 있습니다.

동적 경로를 구성하는 방법은 다음과 같습니다.

  1. 페이지 디렉토리에 동적 경로 파일을 만듭니다:

페이지 디렉토리에 [pageId].tsx라는 새 파일을 만듭니다.

pages/[pageId].tsx
  1. 동적 페이지 렌더링 구현:

[pageId].tsx 파일에서 URL의 pageId를 기반으로 Notion 콘텐츠를 가져와 동적으로 렌더링합니다.

예제 코드:

import { GetStaticProps, GetStaticPaths } from 'next';
import { NotionRenderer } from 'react-notion-x';
import { getNotionPageData } from '../lib/NotionAPI';
import 'react-notion-x/src/styles.css';

export default function NotionPage({ pageData }) {
  return (
    <div>
      <NotionRenderer recordMap={pageData} fullPage={true} darkMode={false} />
    </div>
  );
}

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const { pageId } = params!;
  const pageData = await getNotionPageData(pageId as string);

  return {
    props: {
      pageData,
    },
    revalidate: 10, // Revalidate content every 10 seconds (ISR)
  };
};

export const getStaticPaths: GetStaticPaths = async () => {
  return {
    paths: [], // We’ll use fallback to handle dynamic routes
    fallback: 'blocking', // Generate pages on the fly if not pre-rendered
  };
};
  1. Explanation:
  2. getStaticPaths: Since your content is dynamic, we return an empty array for paths and set fallback: 'blocking' to generate pages on demand.
  3. getStaticProps: Fetches the Notion page content based on the pageId passed in the URL.

Summary

By following the steps above, you can now encapsulate Notion API requests and render Notion pages dynamically in your Next.js project using react-notion-x. This setup allows you to efficiently integrate Notion as a CMS while ensuring scalability and maintainability in your Next.js application.

위 내용은 [개인 홈페이지] Next에서 Notion Database를 통합하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.