在Next.js项目中集成Notion数据库可以使用Notion作为内容管理系统(CMS),并将其内容展示在网站上。以下是一个简单的步骤指南,帮助在Next.js中集成Notion数据库。
源码:https://github.com/Jessie-jzn/Jessie-Blog.dev
实现网站:https://jessie-blog.vercel.app/
基础准备
获取Notion API密钥和数据库ID
- 获取Notion API密钥:前往Notion开发者门户并创建一个新集成。创建后,会得到一个API密钥。
- 获取数据库ID:导航到想集成的Notion数据库,复制数据库页面的URL。数据库ID是URL中位于
https://www.notion.so/
与?v=
之间的一段字符串。
还不清楚的可以看上篇文章:【01.【个人网站】如何使用Notion作为数据库进行全栈开发】
使用官方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页面中,可以使用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>
);
}
确保在.env.local
文件中也添加Notion数据库ID:
NOTION_DATABASE_ID=your_database_id
步骤 4: 部署与验证
最后,将Next.js项目部署到Vercel或其他平台,并验证是否能够正确获取并展示Notion数据库中的数据。
额外提示
- 可以使用Notion页面的属性(如
Name
、Tags
、Date
等)来在页面上展示不同的数据。 - 考虑使用
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-types
、notion-utils
、got
等依赖,以便处理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数据库的集成,还提高了代码的可维护性。