To integrate a Notion database into a Next.js project, you can use Notion as a content management system (CMS) and display its content on your website. Below is a simple step-by-step guide to help you integrate the Notion database into Next.js.
Basic Preparation
Obtain Notion API Key and Database ID
- Get Notion API Key: Go to the Notion Developer Portal and create a new integration. Once created, you will receive an API key.
- Get Database ID: Navigate to the Notion database you want to integrate, and copy the URL of the database page. The database ID is the string of characters between https://www.notion.so/ and ?v= in the URL.
还不清楚的可以看上篇文章:【01.【个人网站】如何使用Notion作为数据库进行全栈开发】
Using the Official SDK
Step 1: Install Dependencies
First, you need to install Notion’s official SDK, @notionhq/client, to communicate with the Notion API. You can install it using npm or yarn:
npm install @notionhq/client
# or
yarn add @notionhq/client
Step 2: Set up the Notion Client
Create a file lib/notion.js in the root directory of your Next.js project and configure the Notion client as follows:
// 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;
};
Make sure to store your Notion API key in an environment variable NOTION_API_KEY.(.env.local)
NOTION_API_KEY=your_secret_api_key
Step 3: Fetch Database Content
In your Next.js page, you can use getStaticProps or getServerSideProps to fetch the content from the Notion database.
// 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>
);
}
Ensure that you have your Notion database ID stored in NOTION_DATABASE_ID as an environment variable.
NOTION_DATABASE_ID=your_database_id
Step 4: Deployment and Verification
Finally, deploy your Next.js project to Vercel or another platform, and verify that you can successfully fetch and display data from the Notion database.
Additional Tips
- You can display different properties of Notion pages (like Name, Tags, Date, etc.) on your page.
- Consider using getServerSideProps to fetch data on every request or use getStaticProps with ISR (Incremental Static Regeneration) to optimize performance.
By following these steps, you can successfully integrate a Notion database into your Next.js project and use it to manage and display content.
Using Notion API with Wrapped URLs
Lastly, I ended up using this method for convenience when integrating with the react-notion-x components.
Make sure to add your Notion Database ID and Notion API Key to the .env.local file:
NOTION_DATABASE_ID=your_database_id
NOTION_API_KEY=your_secret_api_key
Step 1: Install the necessary dependencies
First, install the necessary dependencies such as notion-types, notion-utils, and got to handle requests to the Notion API.
npm install notion-types notion-utils got p-map
Step 2: Create a file to encapsulate Notion API interactions
In your Next.js project, create a file, for example, lib/NotionAPI.ts, which will encapsulate interactions with the Notion API. This file will contain methods for calling Notion API endpoints to fetch data from pages and collections.
// 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
