avatar
02.【个人网站】如何在Next中集成Notion数据库
🌐

02.【个人网站】如何在Next中集成Notion数据库

keywords
Next.js, Notion, Website, API, 数据库, 内容管理系统, 封装组件
published_date
最新编辑 2024年10月16日
Created time
2024年07月31日
slug
在Next.js项目中集成Notion数据库的步骤包括获取Notion API密钥和数据库ID、安装Notion官方SDK、设置Notion客户端、获取数据库内容、部署与验证,以及使用封装的Notion API进行交互。通过这些步骤,可以有效地将Notion作为内容管理系统,并利用react-notion-x组件渲染Notion页面,提升代码的可维护性。
tags
Notion
Next.js
Website
在Next.js项目中集成Notion数据库可以使用Notion作为内容管理系统(CMS),并将其内容展示在网站上。以下是一个简单的步骤指南,帮助在Next.js中集成Notion数据库。

基础准备

获取Notion API密钥和数据库ID

  1. 获取Notion API密钥:前往Notion开发者门户并创建一个新集成。创建后,会得到一个API密钥。
  1. 获取数据库ID:导航到想集成的Notion数据库,复制数据库页面的URL。数据库ID是URL中位于https://www.notion.so/?v=之间的一段字符串。
notion image

使用官方SDK

步骤 1: 安装依赖

首先,需要安装Notion的官方SDK @notionhq/client 来与Notion API进行通信。可以通过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; };
确保在项目的环境变量文件(.env.local)中添加Notion API密钥:
NOTION_API_KEY=your_secret_api_key

步骤 3: 获取数据库内容

在Next.js页面中,可以使用getStaticPropsgetServerSideProps来获取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> ); }
确保在.env.local文件中也添加Notion数据库ID:
NOTION_DATABASE_ID=your_database_id

步骤 4: 部署与验证

最后,将Next.js项目部署到Vercel或其他平台,并验证是否能够正确获取并展示Notion数据库中的数据。

额外提示

  • 可以使用Notion页面的属性(如NameTagsDate等)来在页面上展示不同的数据。
  • 考虑使用getServerSideProps在每次请求时获取最新的数据,或者使用getStaticProps结合ISR来优化性能。
这样,就可以在Next.js项目中成功集成Notion数据库,并利用它来管理和展示内容。

使用封装请求URL的NotionAPI

我最后是使用了这个方法,为了方便后续使用react-notion-x 的封装组件
确保在.env.local文件中添加Notion数据库ID和Notion API密钥:
NOTION_DATABASE_ID=your_database_id NOTION_API_KEY=your_secret_api_key

步骤1. 安装必要的依赖

首先,安装notion-typesnotion-utilsgot等依赖,以便处理Notion API的请求。
npm install notion-types notion-utils got p-map

步骤2. 创建封装Notion API的文件

在你的Next.js项目中创建一个文件,例如lib/NotionAPI.ts,用于封装与Notion API的交互。这个文件将包含与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页面中使用封装的NotionAPI类来获取Notion数据库的内容,并传递给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文件可以通过[pageId]参数匹配路由,这样可以动态渲染不同的Notion页面。

总结

通过以上步骤,可以在Next.js项目中封装Notion API请求,并使用react-notion-x组件来渲染Notion页面。这样不仅简化了与Notion数据库的集成,还提高了代码的可维护性。

系列文章

🌐
01.【个人网站】如何使用Notion作为数据库进行全栈开发
🌐
02.【个人网站】如何在Next中集成Notion数据库
🌐
03.【个人网站】如何做网站自定义页面 URL 映射
🌐
04.【个人网站】如何使用 Vercel 快速部署个人网站 | 全流程指南
🌐
05.【个人网站】基于i18n在Next项目中搭建国际化网站
🌐
06.【个人网站】使用Mailchimp做网站邮件订阅功能
 

关于我

My desire to practice my skills and share my acquired knowledge fuels my endeavors.

联系 : znjessie858@gmail.com

订阅

订阅我的文章,收到我的最新发布通知,可随时取消订阅!