如何使用Next.js与Notion API来创建一个由Typescript代码驱动的博客应用

683 阅读7分钟

使用Typescript和Notion API的Next.js博客

Next.js是用来用JavaScript创建服务器端渲染(SSR)和静态网站生成(SSG)的。在浏览器加载网站的HTML页面后,该应用从服务器上获取额外的数据。

像SSG这样的技术往往要在源头的数据更新时重建应用,并在构建时渲染给用户,使网站的加载速度更快,创造更好的用户体验。

本指南将帮助读者学习如何使用Next.js与Notion API来创建一个由Typescript代码驱动的博客应用。

前提条件

要跟上这篇文章,具备以下条件很有帮助。

  • 在你的电脑上安装[Node.js]。
  • 使用Typescript和Next.js的基本知识。

在Notion上设置数据库

首先,创建一个Notion账户。如果你已经有一个账户,只需登录注册一个新账户。

一旦你创建了账户,将鼠标悬停在仪表板页面的Getting Started 部分,点击加号图标,添加一个新的notion页面。

Database ,在出现的弹出窗口中点击list 。一个样本骨架将被加载。继续前进,输入一个项目的标题。

list-database-skeleton

导航到第一个默认页,page 1 ,并点击它。根据你的喜好改变标题。

page-title-change

你可以选择使用Unsplash的免费图片来改变图标和封面图片。将鼠标悬停在页面标题上,点击Add cover 按钮来添加封面图片。

cover_image_change

cover_image_changed

每一个新创建的页面都是一张空白的画布,你可以在其中添加内容,如纯文本、列表和图片。要向页面添加内容,向下滚动到内容部分,并向你的博客页面添加一些prerequisites ,如下所示。

page-content-section

prerequisites_section

在文章正文中添加假的正文。

body_section

添加一张图片。 image_section

附上一些结论。

conclusion_section

完成后点击模版外。现在该帖子应该如下图所示列出。

database_posts

page 2page 3 重复同样的过程。你可以用同样的方法添加其他帖子。

在Notion上设置一个集成

导航到你的Notion仪表板页面的Settings & Members 部分。在出现的模版上点击Integrations 下的Workspace 部分。然后按照下面的步骤设置Notion集成。

notion_integrations

创建一个新的整合。要这样做,请点击Develop your Integration 。然后点击加号按钮来设置一个新的集成。

create_new_integrations

将整合命名为blog_app_integration ,然后点击Submit ,进行设置。一旦完成,你应该能够查看概念整合的设置,即整合令牌。

向下滚动并点击保存更改,以显示 notion-integration 密钥来保存这个集成。复制这个密钥,以便在连接Next.js时使用。

notion_secrets

回到上一步创建的工作区,也就是你创建页面的地方,即Latest posts ,然后点击Share

notion_share

单击 "邀请"。在出现的模式中,你应该能够看到刚刚创建的集成。

notion_integrations

点击blog_app_integration 集成,然后点击Invite ,将你的页面添加到这个新的概念集成中。

selected-integration

这样,你就可以使用集成生成的令牌访问你的工作区了。

设置Next.js应用程序

要设置Next.js项目,先创建一个项目文件夹,然后运行以下命令,在创建的目录内启动应用程序。

npx create-next-app blog_app --ts

--ts 标志允许你的应用程序使用Typescript运行。

这条命令将在文件夹blog_app 内创建一个基本的Next.js应用程序。一旦这个过程完成,使用cd blog_app 命令导航到blog_app 文件夹,并安装Notion客户端包。

npm install @notionhq/client

在项目根目录下,创建一个.env 文件。这个文件将承载Next.js访问和连接notion API所需的notion集成密钥。继续添加两个概念变量,集成令牌密钥和数据库ID。

NOTION_KEY=""
NOTION_DATABASE=""

粘贴之前复制的集成密钥,并将其添加到NOTION_KEY 。如果你没有复制这个密钥,导航到集成页面,在Secrets 下,点击Show ,然后点击Copy ,把它粘贴到NOTION_KEY 的条目中。

要获得NOTION_DATABASE ID,请检查你的工作区页面URL。复制查询参数前的第一个路径参数,如下图所示。

notion-api

在这种情况下,ID将是53905ad838f04731b48fb1e40c25766a 。假设你的工作空间URL是https://www.notion.so/your_database_id?v=some_long_hash 。参数your_database_id 应该是NOTION_DATABASE

启动开发服务器来测试该应用程序。

npm run dev

导航到http://localhost:3000 ;你应该可以看到默认的Next.js页面。

查询多个帖子

在项目根目录下,创建一个名为lib 的文件夹。在lib ,创建一个文件notion.ts 。然后添加以下代码,从notion API中查询多个帖子。

首先,导入notion客户端包。

import {Client} from '@notionhq/client';

使用你的notion集成密钥实例化notion客户端。

const client = new Client({
   auth: process.env.NOTION_KEY,
});

定义一个函数来获取帖子。这个函数处理并查询来自notion数据库的帖子列表。

async function posts() {
   const myPosts = await client.databases.query({
     database_id: `${process.env.NOTION_DATABASE}`,
   });
   return myPosts;
}

输出一个带有该函数的对象。这个导出将使该函数可以被项目中的其他文件访问。

export {
   posts
}

pages/index.tsx ,导入你上面定义的函数和Next.js链接的依赖项。

import Link from 'next/link';
import {posts} from '../lib/notion'

然后,使用Next.jsgetServerSideProps() 函数从服务器端获取帖子。

export async function getServerSideProps() {
   // Get the posts
   let { results } = await posts();
   // Return the result
   return {
     props: {
       posts: results
     }
   }
}

为props定义一个接口。这个接口创建posts 的结构,并持有帖子的数组。

interface Props {
   posts: [any]
}

为了显示帖子列表,将帖子渲染到视图中,列出从服务器端获取的帖子。

const Home: NextPage<Props> = (props ) => {
   return (
     <div className={styles.container}>
     <Head>
       <title>Latest posts</title>
     </Head>

     <main className={styles.main}>
       <h1 className={styles.title}>
       Latest posts
       </h1>
       {
         props.posts.map((result,index) => {
         return (
           <div className={styles.cardHolder} key={index}>
           <Link href={`/posts/${result.id}`}>
             <Image src={result.cover.external.url} width={300} height={200} />
           </Link>
           <div className={styles.cardContent}>
             <Link href={`/posts/${result.id}`}>
             <a className={styles.cardTitle}>{
             result.properties.Name.title[0].plain_text
             }</a>
             </Link>
           </div>
           </div>
           )
         })
       }
     </main>

     <footer className={styles.footer}>
       <p>Blog application</p>
     </footer>
     </div>
   )
}

为了让Next.js应用程序加载图像,你必须在你的next.config.js ,在图像下配置图像主机名/domain。

在这个例子中,你从unsplash.com 。要添加这个域,请导航到next.config.js ,并配置unsplash.com 的图像源,如下所示。

const nextConfig = {
  reactStrictMode: true,
  images: {
    domains: ['images.unsplash.com']
  }
}

添加以下样式到styles/Home.module.css 。这将为获取的帖子提供样式。

.container {
  padding: 0 2rem;
}

.main {
  min-height: 100vh;
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.cardHolder {
   display: flex;
   width: 40%;
   margin: 10px auto;
   justify-content: space-between;
   padding: 10px;
   border: 1px solid #d4d4d4;
}

.cardContent {
   width: 100%;
   display: flex;
   align-items: center;
   justify-content: center;
}

.footer {
  display: flex;
  flex: 1;
  padding: 2rem 0;
  border-top: 1px solid #eaeaea;
  justify-content: center;
  align-items: center;
}

.footer a {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-grow: 1;
}

为了测试这段代码,确保开发服务器已经启动并运行。现在你应该可以在主页上查看帖子了。

posts_page

查询单个帖子

让我们创建一个函数,从notion数据库中获取一个帖子。

导航到lib/notion.ts 文件,添加一个函数,根据帖子ID获取一个帖子,如下图所示。

async function post(id: string) {
   const myPost = await client.pages.retrieve({
     page_id: id,
   });
   return myPost;
}

lib/notion.ts 文件中添加另一个函数。一个帖子还有其他的属性,如先决条件和结论,它们可以被称为特定帖子的子属性。

创建一个函数blocks() ,以获得一个特定帖子的子属性(块)。

async function blocks(id: string) {
   const myBlocks = await client.blocks.children.list({
     block_id: id
   });
   return myBlocks;
}

导出函数post()blocks() ,使它们可以被项目中的其他文件访问。

export {
   posts,
   post,
   blocks
} 

在根文件夹中,创建一个posts 文件夹。在该文件夹中,创建一个[id].tsx 文件。这个文件将根据参数id来提供动态帖子。在[id].tsx ,添加以下导入。

import { GetStaticProps, NextPage, GetStaticPaths } from 'next';
import Image from 'next/image';
import Head from 'next/head';
import Link from 'next/link';
import { ParsedUrlQuery } from 'querystring';
import { post, posts, blocks } from '../../lib/notion';
import styles from '../../styles/Home.module.css';

接下来,为这个上下文实现一个接口。这个接口将在获取动态ID时被应用。

interface IParams extends ParsedUrlQuery {
   id: string
}

从服务器端获取动态帖子和子属性。

export const getStaticProps: GetStaticProps = async (ctx) => {
   let { id } = ctx.params as IParams; 
   // Get the dynamic id
   let page_result = await post(id); 
   // Fetch the post
   let { results } = await blocks(id); 
   // Get the children
   return {
     props: {
       id,
       post: page_result,
       blocks: results
     }
   }
}

使用getStaticPaths ,实现获取所有帖子的路径。然后使用参数id映射结果。这将有助于Next.js浏览每一个取来的帖子,并根据其动态id显示。

export const getStaticPaths: GetStaticPaths = async () => {
   let { results } = await posts(); 
   // Get all posts
   return {
     paths: results.map((post) => { 
       // Go through every post
       return {
         params: { 
           // set a params object with an id in it
           id: post.id
         }
       }
     }),
     fallback: false
   }
} 

Props 实现一个接口。

interface Props {
   id: string,
   post: any,
   blocks: [any]
}

实现一个函数来渲染每一个孩子。例如,一个帖子有一个标题,一个英雄图像,帖子内容,和一个无序的项目列表。这个函数将帮助从服务器上渲染它们。

const renderBlock = (block: any) => {
   switch (block.type) {
     case 'heading_1': 
     // For a heading
       return <h1>{ block['heading_1'].text[0].plain_text } </h1> 
     case 'image': 
     // For an image
       return <Image src={ block['image'].external.url } width = { 650} height = { 400} />
       case 'bulleted_list_item': 
       // For an unordered list
       return <ul><li>{ block['bulleted_list_item'].text[0].plain_text } </li></ul >
       case 'paragraph': 
       // For a paragraph
       return <p>{ block['paragraph'].text[0]?.text?.content } </p>
     default: 
     // For an extra type
       return <p>Undefined type </p>
   }
}

一旦帖子被渲染,创建一个视图,将帖子显示给用户,如下所示。

const Post:NextPage<Props> = ({id,post,blocks}) => {
   return (
     <div className={styles.blogPageHolder}>
       <Head>
         <title>
           {post.properties.Name.title[0].plain_text}
         </title>
       </Head>
       <div className={styles.blogPageNav}>
         <nav>
           <Link href="/">
             <a>Home</a>
           </Link>
         </nav>
       </div>
       {
         blocks.map((block,index) => {
           return (
             <div key={index} className={styles.blogPageContent}>
               {
                 renderBlock(block)
               }
             </div>
           )})
       }
     </div>
   )
}

添加视图导出。

export default Post;

接下来,在blogPageHolder 类中添加一些样式,以格式化渲染后的帖子。

.blogPageHolder {
   display: flex;
   flex-direction: column;
   justify-content: left;
   width: 50%;
   margin: 10px auto;
}

@media (max-width: 600px) {
  .grid {
    width: 100%;
    flex-direction: column;
  }
}

确保开发服务器正在运行,然后点击主页上的任何帖子的标题。

single_post_page

结论

本指南帮助读者建立了一个概念性的数据库。然后我们用Next.js来使用这个数据库。