首页  >  文章  >  web前端  >  【个人网站】Next如何集成Notion数据库

【个人网站】Next如何集成Notion数据库

DDD
DDD原创
2024-10-12 16:37:021043浏览

要将 Notion 数据库集成到 Next.js 项目中,您可以使用 Notion 作为内容管理系统 (CMS) 并将其内容显示在您的网站上。下面是一个简单的分步指南,可帮助您将 Notion 数据库集成到 Next.js 中。

基本准备

获取Notion API Key和数据库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数据库作为全栈开发】

使用官方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;
};

确保将您的 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 数据库 ID 作为环境变量存储在 NOTION_DATABASE_ID 中。

NOTION_DATABASE_ID=your_database_id

第 4 步:部署和验证

最后,将您的 Next.js 项目部署到 Vercel 或其他平台,并验证您是否可以成功从 Notion 数据库获取并显示数据。

其他提示

  • 您可以在页面上显示Notion页面的不同属性(如名称、标签、日期等)。
  • 考虑使用 getServerSideProps 在每个请求上获取数据,或将 getStaticProps 与 ISR(增量静态再生)结合使用来优化性能。

按照以下步骤,您可以成功地将 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 项目中,创建一个文件,例如 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 页面中获取 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数据库的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn