使用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 。一个样本骨架将被加载。继续前进,输入一个项目的标题。
导航到第一个默认页,page 1 ,并点击它。根据你的喜好改变标题。
你可以选择使用Unsplash的免费图片来改变图标和封面图片。将鼠标悬停在页面标题上,点击Add cover 按钮来添加封面图片。
每一个新创建的页面都是一张空白的画布,你可以在其中添加内容,如纯文本、列表和图片。要向页面添加内容,向下滚动到内容部分,并向你的博客页面添加一些prerequisites ,如下所示。

在文章正文中添加假的正文。
添加一张图片。
附上一些结论。
完成后点击模版外。现在该帖子应该如下图所示列出。
对page 2 和page 3 重复同样的过程。你可以用同样的方法添加其他帖子。
在Notion上设置一个集成
导航到你的Notion仪表板页面的Settings & Members 部分。在出现的模版上点击Integrations 下的Workspace 部分。然后按照下面的步骤设置Notion集成。
创建一个新的整合。要这样做,请点击Develop your Integration 。然后点击加号按钮来设置一个新的集成。
将整合命名为blog_app_integration ,然后点击Submit ,进行设置。一旦完成,你应该能够查看概念整合的设置,即整合令牌。
向下滚动并点击保存更改,以显示 notion-integration 密钥来保存这个集成。复制这个密钥,以便在连接Next.js时使用。
回到上一步创建的工作区,也就是你创建页面的地方,即Latest posts ,然后点击Share 。
单击 "邀请"。在出现的模式中,你应该能够看到刚刚创建的集成。
点击blog_app_integration 集成,然后点击Invite ,将你的页面添加到这个新的概念集成中。
这样,你就可以使用集成生成的令牌访问你的工作区了。
设置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。复制查询参数前的第一个路径参数,如下图所示。

在这种情况下,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;
}
为了测试这段代码,确保开发服务器已经启动并运行。现在你应该可以在主页上查看帖子了。
查询单个帖子
让我们创建一个函数,从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;
}
}
确保开发服务器正在运行,然后点击主页上的任何帖子的标题。
结论
本指南帮助读者建立了一个概念性的数据库。然后我们用Next.js来使用这个数据库。